diff --git a/packages/core/src/utils/__tests__/html-diff.test.js b/packages/core/src/utils/__tests__/html-diff.test.js index 9e3c39c52..49582bad5 100644 --- a/packages/core/src/utils/__tests__/html-diff.test.js +++ b/packages/core/src/utils/__tests__/html-diff.test.js @@ -26,28 +26,44 @@ const equalPairs = [ `
helloworld
`, "helloworld
"], [ - "ignore attributes", - `helloworld
`, - "helloworld
" + "ignore non-semantic attributes", + `hello world
`, + `hello world
` ], [ - "ignore empty tags", - "hello world
`, + `hello world
` ], - ["ignore br", "hello
world
helloworld
"], + [ + "same list structure", + `item one
item two
item one
item two
`,
`
`
],
[
- "link with same href",
- ``,
- ``
- ]
+ "link with same href and text",
+ `click here`,
+ `click here`
+ ],
+ ["case insensitive tags", `hello
`, `hello
`], + ["whitespace inside semantic tags", `hello \n\n
`, `hello
`] ]; describe("pairs should be equal", () => { @@ -72,7 +88,57 @@ const inequalPairs = [ ``, `` ], - ["non-string", {}, {}] + ["non-string", {}, {}], + // formatting differences + ["bold vs no bold", `helloworld
`, `helloworld
`], + ["bold vs italic", `hello
`, `hello
`], + ["strikethrough vs underline", `hello
hello
`], + ["code vs plain text", `hello
hello
`], + // structural differences + ["heading vs paragraph", `Title
`], + ["different heading level", `first
second
second
first
`, + `quoted
quoted
` + ], + [ + "br creates structural difference via text splitting", + `hello
world
helloworld
` + ], + [ + "hr is a meaningful structural element", + `above
below
`, + `above
below
` + ], + // separator prevents text-node concatenation collisions + [ + "adjacent paragraphs do not collide with single paragraph", + `foo
bar
`, + `foobar
` + ], + [ + "href value does not collide with adjacent text", + `defghi`, + `abcdef
ghi` + ], + // link text difference + [ + "link text changed with same href", + `click here`, + `go there` + ] ]; describe("pairs should not be equal", () => { diff --git a/packages/core/src/utils/html-diff.ts b/packages/core/src/utils/html-diff.ts index 3d03771c3..d706192fb 100644 --- a/packages/core/src/utils/html-diff.ts +++ b/packages/core/src/utils/html-diff.ts @@ -21,30 +21,113 @@ import { Parser } from "htmlparser2"; const ALLOWED_ATTRIBUTES = ["href", "src", "data-hash"]; +// Tags whose presence, absence, or nesting order constitutes a semantic +// difference — tracked as tokens so structural/formatting changes are detected. +const SEMANTIC_TAGS = new Set([ + // block structure + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "blockquote", + "pre", + "ul", + "ol", + "li", + "table", + "tr", + "td", + "th", + "thead", + "tbody", + "hr", + // inline formatting + "strong", + "b", + "em", + "i", + "u", + "s", + "del", + "mark", + "sup", + "sub", + "code", + "a" +]); + +// Void elements cannot have children. We emit only an open token for them so +// empty-pair removal never accidentally strips them (e.g.", "
"] → []. Iterates until stable so that nested + * empty tags collapse fully: