Draft
Conversation
commit: |
🌿 Interactive Playground and Documentation PreviewA preview deployment has been built for this pull request. Try out the changes live in the interactive playground: 🌱 Grown from commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces a syntax tree diff engine that compares two parsed Herb syntax trees and produces the minimal set of HTML-semantic differences. The engine is implemented in C so it's available across all bindings (Ruby, Node.js/WASM, Rust, Java).
Motivation
The diff engine is designed to power reactivity and smart repopulation of affected nodes in HTML+ERB templates. By computing the minimal set of semantic changes between two ASTs, consumers can determine exactly which elements, attributes, or text content changed and apply targeted updates instead of reprocessing the entire document.
This enables hot-module reloading (HMR) for HTML+ERB templates, where a dev server can patch only what changed in the DOM and thus preserving element state, focus, scroll position, and event listeners. It also opens the door for incremental re-linting, incremental re-formatting, and language server diagnostics that only recompute affected regions.
How it works
The diff uses a multi-stage approach:
Merkle hashing: a bottom-up pass computes FNV-1a hashes for every node, incorporating all children. Identical subtrees share the same hash and are skipped in O(1). This is the same concept as Merkle trees, used in Git and other content-addressable systems.
LCS-based children diffing: child arrays are compared using the Longest Common Subsequence algorithm to find the minimal edit sequence of insertions, deletions, and keeps. This is the same algorithm behind
diffandgit diff, applied to AST node arrays instead of text lines.Move detection: after LCS, unmatched remove+insert pairs are checked for matching identity (same tag name + same attributes). Matches become
node_movedoperations instead of separate remove+insert. Elements are matched by tag name for the LCS pass, and by tag name + attributes (order-independent, using XOR of attribute hashes) for move detection. This approach is similar to React's reconciliation.Wrap/unwrap detection: detects when a node is wrapped in a new parent (e.g.,
<div></div>→<% if true? %><div></div><% end %>) or unwrapped from one. This uses Merkle hash matching to find removed nodes that appear as children of inserted nodes (wrap) or children of removed nodes that match inserted nodes (unwrap).Operation types
Usage
CLI:
Examples
Attribute and content changes
changed to:
produces:
Move detection
changed to:
produces:
Wrap/unwrap detection
changed to:
produces:
Playground
A new "Diff" tab is added to the playground with two modes. The Live mode diffs on every keystroke, showing a scrollable feed of changes with undo/rollback and in the checkpoint mode you take a snapshot, edit, then explicitly diff. The diff is paused when the parse result has errors.
CleanShot.2026-03-24.at.02.13.58.mp4