redline-engine
v0.3.0
Published
Structural HTML redlines with validated before/after reconstruction, precise inline highlights and bounded failure.
Maintainers
Readme
redline-engine
Structural HTML redlines for Node.js and bundled browser applications. Compare two body fragments, highlight text and structural edits, and reconstruct either original parsed body from the merged result. Written in TypeScript, with no runtime DOM globals or network calls.
ESM-only, with TypeScript declarations. Requires Node.js 22+ or a browser bundler.
Why use it over node-htmldiff?
redline-engine is a structural alternative to node-htmldiff 0.9.4. It aligns HTML
subtrees and refines local text changes, making large documents with scattered edits practical
while validating that both original parsed bodies can be reconstructed.
On synthetic documents with 6,000 paragraphs per side, about 1 MB each, 0.3.0 records:
| Sparse-edit workload | 0.3.0 | 0.2.0 | 0.1.0 | node-htmldiff 0.9.4 | | ------------------------------------- | -----: | -----: | -----: | ------------------: | | Original pair | 393 ms | 438 ms | 433 ms | Timed out at 15 s | | Both bodies enclosed in a section | 408 ms | 447 ms | 465 ms | Timed out at 15 s | | Extra paragraph inserted at the start | 398 ms | 521 ms | 447 ms | Timed out at 15 s |
Measured on an Apple M1 Max with Node.js 26.7.0. Structural values are medians of three fresh-process calls including parsing, matching, rendering and both-side validation. The raw legacy renderer was run once per workload and terminated at 15 seconds, including process startup; its eventual completion time is unknown. 0.3.0 removes the 0.2.0 start-insertion slowdown and is faster than both earlier releases on all three workloads. Timing targets are advisory; all three workloads succeed with zero coarse replacements. See the 0.3.0 samples, 0.2.0 samples and original samples.
The gain is workload-dependent: simple insertion-only and repetitive cases were faster in node-htmldiff in the baseline benchmarks. This is a replacement API, not a drop-in adapter.
Beyond performance, the engine represents attribute-only changes and preserves whitespace, comments, empty elements and namespaces. Successful results pass reconstruction checks; unsupported inputs and exceeded budgets produce explicit outcomes. The compatibility suite covers adapted upstream behavior, intentional corrections, quality fixtures and independent projection checks. Version 0.3.0 passes all 532 compatibility checks, 143 unit tests, 87 unchanged upstream tests and 25 browser/playground tests.
Install and compare
Install version 0.3.0:
npm install [email protected]import { compareBodies, project, renderMerged } from "redline-engine";
const result = compareBodies({
beforeHtml: "<p>Hello <b>old</b> world.</p>",
afterHtml: "<p>Hello <b>new</b> world.</p>",
});
if (result.outcome === "success") {
const { html, diagnostics } = renderMerged(result.comparison);
const beforeBody = project(result.comparison, "before");
const afterBody = project(result.comparison, "after");
console.log(html, beforeBody, afterBody, diagnostics);
} else if (result.outcome === "limit") {
console.error("Comparison exceeded", result.limit);
} else {
console.error("Cannot represent this comparison", result.diagnostics);
}Success is returned only after both projections of reparsed merged HTML match the canonical input DOM trees, including namespaces, attributes, comments, whitespace and empty nodes. Source bytes, attribute order and equivalent entity spellings may normalize. Moves are represented as deletion/insertion. Unsupported and limit results contain no partial HTML.
Public API
| Export | Purpose |
| ------------------------------------------ | ---------------------------------------------------------------------- |
| compareBodies(input) | Synchronous comparison returning success, unsupported or limit. |
| renderMerged(comparison) | Returns { html, diagnostics } from the validated comparison. |
| project(comparison, "before" \| "after") | Reconstructs a canonical body string using owned markers. |
| DEFAULT_LIMITS | Default input, node, depth, work, output and cooperative time budgets. |
| MODEL_VERSION | Operation model version, currently 2. |
Types: CompareBodiesInput, ComparisonResult, Comparison, Operation, Diagnostic,
Limits, Side and Timings. Import through redline-engine; internal module paths are not
public API. Do not mutate comparison objects. During 0.x, incompatible API/marker changes
will require a minor version; fixes preserving the contract use patch versions.
Migration from model 1: accept MODEL_VERSION === 2, handle Operation.kind === "attributes",
and include [data-diff-attrs] and [data-diff-lead] in marker handling. A model-1 host may
continue to consume 0.1.x results, but must reject a model-2 comparison before rendering it.
Options are optional: className (default redline), dataPrefix (default diff),
atomicTags (additional exact lowercase atomic element names), and partial limits.
Invalid option values return unsupported. See CONTRACT.md for grammars,
precision boundaries, operation semantics and supported HTML content models.
Worker and display boundary
This library is not a sanitizer. Sanitize both bodies before comparison and isolate the rendered result. The host owns document heads, resource loading, security policy and styling. Run user-supplied documents in a worker and terminate it after 15 seconds or cancellation: the synchronous parser cannot be preempted by the cooperative engine timeout.
Browser host and worker examples
show a bundler-based worker with a deadline and cancellation. Copy both files into your
application. The worker sends HTML, diagnostics and timings rather than the operation journal;
retain the comparison in the worker if you need its public project function. Handle worker failures and cancellation in your application.
Default generated markers use data-diff-node="insert|delete", data-diff-op and optional
data-diff-wrapper. Insertions and deletions in lists, tables and block-level flow containers
may also carry data-diff-lead="", which gives the immediately preceding whitespace-only text
node to the marked element or generated wrapper. Several markers can share one data-diff-op
(a replacement's two sides, or whitespace next to a deleted first block); group by it when
counting changes. Paired attribute changes use data-diff-attrs, a JSON array containing the
complete before-side [name,value] list in source order. Cross-inline formatting shells use
data-diff-unwrap="before|after".
All data-diff-* input attributes are reserved and cause rejection; a custom prefix owns its
corresponding namespace. Original ins/del elements are not generated markers merely because
of their tag or class. Custom viewers must implement all marker semantics in the contract and run the shipped
redline-engine/conformance/model-2.json vectors against their projector.
[data-diff-node="insert"] {
background: #dcfce7;
}
[data-diff-node="delete"] {
background: #fee2e2;
}
[data-diff-unwrap] {
outline: 1px dashed #956600;
}
[data-diff-attrs] {
outline: 1px dashed #6b4f9a;
}Model 2 viewers must recognize node, wrapper, unwrap, lead and attribute markers. Projection
processes descendants before it restores data-diff-attrs on the before side or unwraps a
shell. On the side where a lead-owned marker is absent, its immediately preceding
whitespace-only text node is removed with it. In lists and tables, a re-indented child is
replaced on its own; changed trailing container whitespace still replaces the container.
Code blocks (pre) are compared by line, with word-level highlights inside similar lines. Attribute changes use an
attributes operation and the attribute-change diagnostic; the before projection restores
the full JSON attribute list and the after projection keeps the after-side attributes.
Bounds and measured limitations
Defaults: 2,000,000 combined UTF-16 input units; 200,000 nodes per side; depth 256; 50,000,000 work units; 8,000,000 output units; 15,000 ms cooperative time. These budgets interact: passing the input or node cap does not guarantee that work/output budgets fit. They do not impose a process memory ceiling or bound host layout.
The 0.2.0's synthetic 70,000-replacement case expands to 7.8 million output units and records 2.463 seconds warm Chromium render-ready p95 (20 warm calls). The original release recorded 2.403 seconds; both exceed the original 2-second target. This version is about 2.5% above the later 2.403-second ceiling, now advisory. Extreme expansion is memory intensive. See the current distributions, current memory snapshots, and measurement method and historical evidence.
Malformed namespace-changing repairs can return unsupported. Bounded local refinement may
produce diagnosed coarse replacements that preserve both sides. Document styling, hidden
content and duplicate IDs remain application concerns.
Develop and verify
pnpm install --frozen-lockfile
pnpm exec playwright install chromium
pnpm run typecheck
pnpm run test
pnpm run test:upstream
pnpm run test:compatibility
pnpm run test:browser
pnpm run test:package
pnpm run test:integration # needs the Redline plugin at ../redline (or PLUGIN_ROOT)
pnpm run playground
pnpm pack --pack-destination artifactstest:package installs the tarball in a temporary consumer, checks TypeScript resolution
and runs Node and browser-worker examples. pnpm run bench runs the isolated comparison
benchmarks. CONTRACT.md specifies the API and preservation guarantees.
MIT. LICENSE. THIRD-PARTY-NOTICES.md describes runtime dependencies and the separately retained test/benchmark provenance. The old engine and its fixtures are excluded from the npm package.
