@x-oasis/html-fragment-diff 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
 
2
- > @x-oasis/html-fragment-diff@0.2.0 build /home/runner/work/x-oasis/x-oasis/packages/diff/html-fragment-diff
2
+ > @x-oasis/html-fragment-diff@0.2.1 build /home/runner/work/x-oasis/x-oasis/packages/diff/html-fragment-diff
3
3
  > tsdx build --tsconfig tsconfig.build.json
4
4
 
5
5
  @rollup/plugin-replace: 'preventAssignment' currently defaults to false. It is recommended to set this option to `true`, as the next major version will default this option to `true`.
6
6
  @rollup/plugin-replace: 'preventAssignment' currently defaults to false. It is recommended to set this option to `true`, as the next major version will default this option to `true`.
7
7
  ⠙ Creating entry file
8
8
  ⠙ Building modules
9
- ✓ Creating entry file 5.5 secs
9
+ ✓ Creating entry file 4.7 secs
10
10
  ⠹ Building modules
11
11
  ⠸ Building modules
12
12
  ⠼ Building modules
@@ -18,8 +18,9 @@
18
18
  ⠋ Building modules
19
19
  ⠙ Building modules
20
20
  ⠹ Building modules
21
+ ⠸ Building modules
22
+ ⠼ Building modules
21
23
  [tsdx]: Your rootDir is currently set to "./". Please change your rootDir to "./src".
22
24
  TSDX has deprecated setting tsconfig.compilerOptions.rootDir to "./" as it caused buggy output for declarationMaps and more.
23
25
  You may also need to change your include to remove "test", which also caused declarations to be unnecessarily created for test files.
24
-  Building modules
25
- ✓ Building modules 15.5 secs
26
+  Building modules 15.7 secs
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @x-oasis/html-fragment-diff
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - cfaacab: bump version diff html
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
@@ -124,12 +124,12 @@ function compareHtmlFragments(originalFragment, finalFragment) {
124
124
  var _final = parseFragmentToElement(finalFragment);
125
125
  var originalClasses = new Set((_original$classList = original == null ? void 0 : original.classList) != null ? _original$classList : []);
126
126
  var finalClasses = new Set((_final$classList = _final == null ? void 0 : _final.classList) != null ? _final$classList : []);
127
- var classAdded = ((_final$classList2 = _final == null ? void 0 : _final.classList) != null ? _final$classList2 : []).filter(function (c) {
127
+ var classAdded = [].concat(((_final$classList2 = _final == null ? void 0 : _final.classList) != null ? _final$classList2 : []).filter(function (c) {
128
128
  return !originalClasses.has(c);
129
- });
130
- var classRemoved = ((_original$classList2 = original == null ? void 0 : original.classList) != null ? _original$classList2 : []).filter(function (c) {
129
+ }));
130
+ var classRemoved = [].concat(((_original$classList2 = original == null ? void 0 : original.classList) != null ? _original$classList2 : []).filter(function (c) {
131
131
  return !finalClasses.has(c);
132
- });
132
+ }));
133
133
  var textOriginal = (_original$textContent = original == null ? void 0 : original.textContent) != null ? _original$textContent : '';
134
134
  var textFinal = (_final$textContent = _final == null ? void 0 : _final.textContent) != null ? _final$textContent : '';
135
135
  var textChanged = textOriginal !== textFinal;
@@ -1 +1 @@
1
- {"version":3,"file":"html-fragment-diff.cjs.development.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n const classAdded = (final?.classList ?? []).filter(\n (c) => !originalClasses.has(c)\n );\n const classRemoved = (original?.classList ?? []).filter(\n (c) => !finalClasses.has(c)\n );\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","nodeName","_node$value","childNodes","_node$childNodes","map","child","join","isElementNode","tagName","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","wrapped","fragmentNode","parseFragment","_unused","_fragmentNode$childNo","_fragmentNode","_iterator","_createForOfIteratorHelperLoose","_step","done","_child$tagName","classList","textContent","otherAttrs","_iterator2","_child$attrs","_step2","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","consumeGroupChangeResult","result","undefined","htmlDiff","_extends"],"mappings":"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"}
1
+ {"version":3,"file":"html-fragment-diff.cjs.development.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n // 明确返回全新 string[],避免被误用或序列化成 Set\n const classAdded: string[] = [\n ...(final?.classList ?? []).filter((c) => !originalClasses.has(c)),\n ];\n const classRemoved: string[] = [\n ...(original?.classList ?? []).filter((c) => !finalClasses.has(c)),\n ];\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","nodeName","_node$value","childNodes","_node$childNodes","map","child","join","isElementNode","tagName","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","wrapped","fragmentNode","parseFragment","_unused","_fragmentNode$childNo","_fragmentNode","_iterator","_createForOfIteratorHelperLoose","_step","done","_child$tagName","classList","textContent","otherAttrs","_iterator2","_child$attrs","_step2","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","consumeGroupChangeResult","result","undefined","htmlDiff","_extends"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("parse5");function n(){return(n=Object.assign?Object.assign.bind():function(t){for(var n=1;n<arguments.length;n++){var e=arguments[n];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])}return t}).apply(this,arguments)}function e(t,n){(null==n||n>t.length)&&(n=t.length);for(var e=0,r=new Array(n);e<n;e++)r[e]=t[e];return r}function r(t,n){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(r)return(r=r.call(t)).next.bind(r);if(Array.isArray(t)||(r=function(t,n){if(t){if("string"==typeof t)return e(t,void 0);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,void 0):void 0}}(t))||n&&t&&"number"==typeof t.length){r&&(t=r);var l=0;return function(){return l>=t.length?{done:!0}:{done:!1,value:t[l++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function l(t,n){var e,r=null!=(e=t.attrs)?e:[],l=n.toLowerCase(),a=r.find((function(t){var n;return(null==(n=t.name)?void 0:n.toLowerCase())===l}));return null==a?void 0:a.value}function a(t){var n,e;return t?"#text"===t.nodeName?null!=(e=t.value)?e:"":(null!=(n=t.childNodes)?n:[]).map((function(t){return a(t)})).join(""):""}function o(t){if(null==t||""===t)return[];var n=t.trim().split(/\s+/).filter(Boolean);return[].concat(new Set(n))}function i(n){var e,i;if(!n||"string"!=typeof n)return null;var u,s=n.trim();if(!s)return null;try{u=t.parseFragment(s)}catch(t){return null}for(var c,f,v=r(null!=(e=null==(i=u)?void 0:i.childNodes)?e:[]);!(c=v()).done;){var d=c.value;if((f=d)&&"string"==typeof f.tagName){for(var m,p,g=o(l(d,"class")),y=a(d).trim(),h={},b=r(null!=(x=d.attrs)?x:[]);!(p=b()).done;){var x,w,C,L=p.value;"class"!==(null==(w=L.name)?void 0:w.toLowerCase())&&(h[L.name]=null!=(C=L.value)?C:"")}return{tagName:(null!=(m=d.tagName)?m:"").toLowerCase(),classList:g,textContent:y,otherAttrs:h}}}return null}function u(t,n){var e,r,l,a,o,u,s=i(t),c=i(n),f=new Set(null!=(e=null==s?void 0:s.classList)?e:[]),v=new Set(null!=(r=null==c?void 0:c.classList)?r:[]),d=(null!=(l=null==c?void 0:c.classList)?l:[]).filter((function(t){return!f.has(t)})),m=(null!=(a=null==s?void 0:s.classList)?a:[]).filter((function(t){return!v.has(t)})),p=null!=(o=null==s?void 0:s.textContent)?o:"",g=null!=(u=null==c?void 0:c.textContent)?u:"",y=p!==g;return{original:s,final:c,classAdded:d,classRemoved:m,textOriginal:p,textFinal:g,textChanged:y,textSummary:y?"「"+(p||"(空)")+"」 → 「"+(g||"(空)")+"」":"无变更"}}exports.compareHtmlFragments=u,exports.consumeGroupChangeResult=function(t){if(null!=t)return n({},t,{htmlDiff:u(t.originalFragment,t.finalFragment)})},exports.parseFragmentToElement=i;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("parse5");function n(){return(n=Object.assign?Object.assign.bind():function(t){for(var n=1;n<arguments.length;n++){var e=arguments[n];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])}return t}).apply(this,arguments)}function e(t,n){(null==n||n>t.length)&&(n=t.length);for(var e=0,r=new Array(n);e<n;e++)r[e]=t[e];return r}function r(t,n){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(r)return(r=r.call(t)).next.bind(r);if(Array.isArray(t)||(r=function(t,n){if(t){if("string"==typeof t)return e(t,void 0);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?e(t,void 0):void 0}}(t))||n&&t&&"number"==typeof t.length){r&&(t=r);var l=0;return function(){return l>=t.length?{done:!0}:{done:!1,value:t[l++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function l(t,n){var e,r=null!=(e=t.attrs)?e:[],l=n.toLowerCase(),a=r.find((function(t){var n;return(null==(n=t.name)?void 0:n.toLowerCase())===l}));return null==a?void 0:a.value}function a(t){var n,e;return t?"#text"===t.nodeName?null!=(e=t.value)?e:"":(null!=(n=t.childNodes)?n:[]).map((function(t){return a(t)})).join(""):""}function o(t){if(null==t||""===t)return[];var n=t.trim().split(/\s+/).filter(Boolean);return[].concat(new Set(n))}function i(n){var e,i;if(!n||"string"!=typeof n)return null;var u,s=n.trim();if(!s)return null;try{u=t.parseFragment(s)}catch(t){return null}for(var c,f,v=r(null!=(e=null==(i=u)?void 0:i.childNodes)?e:[]);!(c=v()).done;){var d=c.value;if((f=d)&&"string"==typeof f.tagName){for(var m,p,g=o(l(d,"class")),y=a(d).trim(),h={},b=r(null!=(x=d.attrs)?x:[]);!(p=b()).done;){var x,w,C,L=p.value;"class"!==(null==(w=L.name)?void 0:w.toLowerCase())&&(h[L.name]=null!=(C=L.value)?C:"")}return{tagName:(null!=(m=d.tagName)?m:"").toLowerCase(),classList:g,textContent:y,otherAttrs:h}}}return null}function u(t,n){var e,r,l,a,o,u,s=i(t),c=i(n),f=new Set(null!=(e=null==s?void 0:s.classList)?e:[]),v=new Set(null!=(r=null==c?void 0:c.classList)?r:[]),d=[].concat((null!=(l=null==c?void 0:c.classList)?l:[]).filter((function(t){return!f.has(t)}))),m=[].concat((null!=(a=null==s?void 0:s.classList)?a:[]).filter((function(t){return!v.has(t)}))),p=null!=(o=null==s?void 0:s.textContent)?o:"",g=null!=(u=null==c?void 0:c.textContent)?u:"",y=p!==g;return{original:s,final:c,classAdded:d,classRemoved:m,textOriginal:p,textFinal:g,textChanged:y,textSummary:y?"「"+(p||"(空)")+"」 → 「"+(g||"(空)")+"」":"无变更"}}exports.compareHtmlFragments=u,exports.consumeGroupChangeResult=function(t){if(null!=t)return n({},t,{htmlDiff:u(t.originalFragment,t.finalFragment)})},exports.parseFragmentToElement=i;
2
2
  //# sourceMappingURL=html-fragment-diff.cjs.production.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"html-fragment-diff.cjs.production.min.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n const classAdded = (final?.classList ?? []).filter(\n (c) => !originalClasses.has(c)\n );\n const classRemoved = (original?.classList ?? []).filter(\n (c) => !finalClasses.has(c)\n );\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","_node$value","nodeName","_node$childNodes","childNodes","map","child","join","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","fragmentNode","wrapped","parseFragment","_unused","_step","_iterator","_createForOfIteratorHelperLoose","_fragmentNode$childNo","_fragmentNode","done","tagName","_child$tagName","_step2","classList","textContent","otherAttrs","_iterator2","_child$attrs","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","result","_extends","htmlDiff"],"mappings":"+nCAyCA,SAASA,EACPC,EACAC,SAEMC,SAAKC,EAAGH,EAAKE,OAAKC,EAAI,GACtBC,EAAQH,EAAKI,cACbC,EAAIJ,EAAMK,MAAK,SAACC,GAAC,IAAAC,EAAA,cAAKA,EAAAD,EAAEP,aAAFQ,EAAQJ,iBAAkBD,KACtD,aAAOE,SAAAA,EAAGI,MAMZ,SAASC,EAAeX,SAESY,EAD/B,OAAKZ,EACiB,UAAlBA,EAAKa,gBACPD,EAAOZ,EAAKU,OAAKE,EAAI,WAEPE,EAAGd,EAAKe,YAAUD,EAAI,IACpBE,KAAI,SAACC,GAAU,OAAKN,EAAeM,MAAQC,KAAK,IALhD,GAsBpB,SAASC,EAAeC,GACtB,GAAiB,MAAbA,GAAmC,KAAdA,EAAkB,MAAO,GAClD,IAAMC,EAAOD,EAAUE,OAAOC,MAAM,OAAOC,OAAOC,SAClD,SAAAC,OAAW,IAAIC,IAAIN,aASLO,EACdC,WAEA,IAAKA,GAAgC,iBAAbA,EAAuB,OAAO,KAEtD,IAGIC,EAHEC,EAAUF,EAASP,OACzB,IAAKS,EAAS,OAAO,KAGrB,IACED,EAAeE,gBAAcD,GAC7B,MAAAE,GACA,OAAO,KAIT,IADA,IAC8BC,EAvCTlC,EAuCrBmC,EAAAC,SADgBC,SAAAC,EAAGR,UAAAQ,EAAcvB,YAAUsB,EAAI,MACjBH,EAAAC,KAAAI,MAAE,CAAA,IAArBtB,EAAKiB,EAAAxB,MACd,IAxCmBV,EAwCDiB,IAnC4B,iBAAzBjB,EAAawC,QAmCR,CAKxB,IALwB,IAAAC,EAKSC,EAH3BC,EAAYxB,EADApB,EAAQkB,EAAO,UAE3B2B,EAAcjC,EAAeM,GAAOK,OACpCuB,EAAqC,GAC3CC,EAAAV,SAAAW,EAAgB9B,EAAMf,OAAK6C,EAAI,MAAEL,EAAAI,KAAAP,MAAE,CAAA,IAAAQ,EAAAC,EACMC,EAD9B3C,EAACoC,EAAAhC,MACoB,kBAA1BsC,EAAA1C,EAAEL,aAAF+C,EAAQ3C,iBACVwC,EAAWvC,EAAEL,aAAKgD,EAAG3C,EAAEI,OAAKuC,EAAI,IAGpC,MAAO,CACLT,gBAASC,EAACxB,EAAMuB,SAAOC,EAAI,IAAIpC,cAC/BsC,UAAAA,EACAC,YAAAA,EACAC,WAAAA,IAIN,OAAO,cAUOK,EACdC,EACAC,mBAEMC,EAAWzB,EAAuBuB,GAClCG,EAAQ1B,EAAuBwB,GAE/BG,EAAkB,IAAI5B,WAAG6B,QAACH,SAAAA,EAAUV,WAASa,EAAI,IACjDC,EAAe,IAAI9B,WAAG+B,QAACJ,SAAAA,EAAOX,WAASe,EAAI,IAC3CC,UAAaC,QAACN,SAAAA,EAAOX,WAASiB,EAAI,IAAIpC,QAC1C,SAACqC,GAAC,OAAMN,EAAgBO,IAAID,MAExBE,UAAeC,QAACX,SAAAA,EAAUV,WAASqB,EAAI,IAAIxC,QAC/C,SAACqC,GAAC,OAAMJ,EAAaK,IAAID,MAGrBI,SAAYC,QAAGb,SAAAA,EAAUT,aAAWsB,EAAI,GACxCC,SAASC,QAAGd,SAAAA,EAAOV,aAAWwB,EAAI,GAClCC,EAAcJ,IAAiBE,EAKrC,MAAO,CACLd,SAAAA,EACAC,MAAAA,EACAK,WAAAA,EACAI,aAAAA,EACAE,aAAAA,EACAE,UAAAA,EACAE,YAAAA,EACAC,YAZkBD,OACZJ,GAAgB,gBAAaE,GAAa,WAC9C,gFAuBJI,GACA,GAAc,MAAVA,EAKJ,OAAAC,KAAYD,GAAQE,SAJHvB,EACfqB,EAAOpB,iBACPoB,EAAOnB"}
1
+ {"version":3,"file":"html-fragment-diff.cjs.production.min.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n // 明确返回全新 string[],避免被误用或序列化成 Set\n const classAdded: string[] = [\n ...(final?.classList ?? []).filter((c) => !originalClasses.has(c)),\n ];\n const classRemoved: string[] = [\n ...(original?.classList ?? []).filter((c) => !finalClasses.has(c)),\n ];\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","_node$value","nodeName","_node$childNodes","childNodes","map","child","join","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","fragmentNode","wrapped","parseFragment","_unused","_step","_iterator","_createForOfIteratorHelperLoose","_fragmentNode$childNo","_fragmentNode","done","tagName","_child$tagName","_step2","classList","textContent","otherAttrs","_iterator2","_child$attrs","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","result","_extends","htmlDiff"],"mappings":"+nCAyCA,SAASA,EACPC,EACAC,SAEMC,SAAKC,EAAGH,EAAKE,OAAKC,EAAI,GACtBC,EAAQH,EAAKI,cACbC,EAAIJ,EAAMK,MAAK,SAACC,GAAC,IAAAC,EAAA,cAAKA,EAAAD,EAAEP,aAAFQ,EAAQJ,iBAAkBD,KACtD,aAAOE,SAAAA,EAAGI,MAMZ,SAASC,EAAeX,SAESY,EAD/B,OAAKZ,EACiB,UAAlBA,EAAKa,gBACPD,EAAOZ,EAAKU,OAAKE,EAAI,WAEPE,EAAGd,EAAKe,YAAUD,EAAI,IACpBE,KAAI,SAACC,GAAU,OAAKN,EAAeM,MAAQC,KAAK,IALhD,GAsBpB,SAASC,EAAeC,GACtB,GAAiB,MAAbA,GAAmC,KAAdA,EAAkB,MAAO,GAClD,IAAMC,EAAOD,EAAUE,OAAOC,MAAM,OAAOC,OAAOC,SAClD,SAAAC,OAAW,IAAIC,IAAIN,aASLO,EACdC,WAEA,IAAKA,GAAgC,iBAAbA,EAAuB,OAAO,KAEtD,IAGIC,EAHEC,EAAUF,EAASP,OACzB,IAAKS,EAAS,OAAO,KAGrB,IACED,EAAeE,gBAAcD,GAC7B,MAAAE,GACA,OAAO,KAIT,IADA,IAC8BC,EAvCTlC,EAuCrBmC,EAAAC,SADgBC,SAAAC,EAAGR,UAAAQ,EAAcvB,YAAUsB,EAAI,MACjBH,EAAAC,KAAAI,MAAE,CAAA,IAArBtB,EAAKiB,EAAAxB,MACd,IAxCmBV,EAwCDiB,IAnC4B,iBAAzBjB,EAAawC,QAmCR,CAKxB,IALwB,IAAAC,EAKSC,EAH3BC,EAAYxB,EADApB,EAAQkB,EAAO,UAE3B2B,EAAcjC,EAAeM,GAAOK,OACpCuB,EAAqC,GAC3CC,EAAAV,SAAAW,EAAgB9B,EAAMf,OAAK6C,EAAI,MAAEL,EAAAI,KAAAP,MAAE,CAAA,IAAAQ,EAAAC,EACMC,EAD9B3C,EAACoC,EAAAhC,MACoB,kBAA1BsC,EAAA1C,EAAEL,aAAF+C,EAAQ3C,iBACVwC,EAAWvC,EAAEL,aAAKgD,EAAG3C,EAAEI,OAAKuC,EAAI,IAGpC,MAAO,CACLT,gBAASC,EAACxB,EAAMuB,SAAOC,EAAI,IAAIpC,cAC/BsC,UAAAA,EACAC,YAAAA,EACAC,WAAAA,IAIN,OAAO,cAUOK,EACdC,EACAC,mBAEMC,EAAWzB,EAAuBuB,GAClCG,EAAQ1B,EAAuBwB,GAE/BG,EAAkB,IAAI5B,WAAG6B,QAACH,SAAAA,EAAUV,WAASa,EAAI,IACjDC,EAAe,IAAI9B,WAAG+B,QAACJ,SAAAA,EAAOX,WAASe,EAAI,IAE3CC,KAAUjC,eACXkC,QAACN,SAAAA,EAAOX,WAASiB,EAAI,IAAIpC,QAAO,SAACqC,GAAC,OAAMN,EAAgBO,IAAID,OAE3DE,KAAYrC,eACbsC,QAACX,SAAAA,EAAUV,WAASqB,EAAI,IAAIxC,QAAO,SAACqC,GAAC,OAAMJ,EAAaK,IAAID,OAG3DI,SAAYC,QAAGb,SAAAA,EAAUT,aAAWsB,EAAI,GACxCC,SAASC,QAAGd,SAAAA,EAAOV,aAAWwB,EAAI,GAClCC,EAAcJ,IAAiBE,EAKrC,MAAO,CACLd,SAAAA,EACAC,MAAAA,EACAK,WAAAA,EACAI,aAAAA,EACAE,aAAAA,EACAE,UAAAA,EACAE,YAAAA,EACAC,YAZkBD,OACZJ,GAAgB,gBAAaE,GAAa,WAC9C,gFAuBJI,GACA,GAAc,MAAVA,EAKJ,OAAAC,KAAYD,GAAQE,SAJHvB,EACfqB,EAAOpB,iBACPoB,EAAOnB"}
@@ -120,12 +120,12 @@ function compareHtmlFragments(originalFragment, finalFragment) {
120
120
  var _final = parseFragmentToElement(finalFragment);
121
121
  var originalClasses = new Set((_original$classList = original == null ? void 0 : original.classList) != null ? _original$classList : []);
122
122
  var finalClasses = new Set((_final$classList = _final == null ? void 0 : _final.classList) != null ? _final$classList : []);
123
- var classAdded = ((_final$classList2 = _final == null ? void 0 : _final.classList) != null ? _final$classList2 : []).filter(function (c) {
123
+ var classAdded = [].concat(((_final$classList2 = _final == null ? void 0 : _final.classList) != null ? _final$classList2 : []).filter(function (c) {
124
124
  return !originalClasses.has(c);
125
- });
126
- var classRemoved = ((_original$classList2 = original == null ? void 0 : original.classList) != null ? _original$classList2 : []).filter(function (c) {
125
+ }));
126
+ var classRemoved = [].concat(((_original$classList2 = original == null ? void 0 : original.classList) != null ? _original$classList2 : []).filter(function (c) {
127
127
  return !finalClasses.has(c);
128
- });
128
+ }));
129
129
  var textOriginal = (_original$textContent = original == null ? void 0 : original.textContent) != null ? _original$textContent : '';
130
130
  var textFinal = (_final$textContent = _final == null ? void 0 : _final.textContent) != null ? _final$textContent : '';
131
131
  var textChanged = textOriginal !== textFinal;
@@ -1 +1 @@
1
- {"version":3,"file":"html-fragment-diff.esm.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n const classAdded = (final?.classList ?? []).filter(\n (c) => !originalClasses.has(c)\n );\n const classRemoved = (original?.classList ?? []).filter(\n (c) => !finalClasses.has(c)\n );\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","nodeName","_node$value","childNodes","_node$childNodes","map","child","join","isElementNode","tagName","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","wrapped","fragmentNode","parseFragment","_unused","_fragmentNode$childNo","_fragmentNode","_iterator","_createForOfIteratorHelperLoose","_step","done","_child$tagName","classList","textContent","otherAttrs","_iterator2","_child$attrs","_step2","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","consumeGroupChangeResult","result","undefined","htmlDiff","_extends"],"mappings":"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"}
1
+ {"version":3,"file":"html-fragment-diff.esm.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * 对两个 HTML 片段做 parse 级别的对比:解析出标签、class、文本等,并输出 class 增删与文本变更。\n * 用于消费 resolveGroupChangeFragments 得到的 originalFragment / finalFragment。\n */\n\nimport { parseFragment } from 'parse5';\n\n/** 解析出的单个根元素信息(只关心第一个根元素) */\nexport interface ParsedFragmentElement {\n tagName: string;\n /** class 属性按空白切分后的列表 */\n classList: string[];\n /** 元素内直接+间接文本拼接(不含子标签的 tag,只取文本) */\n textContent: string;\n /** 除 class 外的其他属性(name -> value) */\n otherAttrs: Record<string, string>;\n}\n\n/** 两个 HTML 片段的对比结果 */\nexport interface HtmlFragmentDiff {\n /** 原始片段解析结果(若解析失败为 null) */\n original: ParsedFragmentElement | null;\n /** 最终片段解析结果(若解析失败为 null) */\n final: ParsedFragmentElement | null;\n /** class:最终相对原始新增的 class 列表 */\n classAdded: string[];\n /** class:最终相对原始删除的 class 列表 */\n classRemoved: string[];\n /** 文本:原始片段根元素文本 */\n textOriginal: string;\n /** 文本:最终片段根元素文本 */\n textFinal: string;\n /** 文本是否发生变更 */\n textChanged: boolean;\n /** 文本变更的简短描述(便于展示) */\n textSummary: string;\n}\n\n/**\n * 从 parse5 的节点中取属性值\n */\nfunction getAttr(\n node: { attrs?: Array<{ name: string; value: string }> },\n name: string\n): string | undefined {\n const attrs = node.attrs ?? [];\n const lower = name.toLowerCase();\n const a = attrs.find((x) => x.name?.toLowerCase() === lower);\n return a?.value;\n}\n\n/**\n * 递归收集元素的文本内容(不含标签名,只取文本节点)\n */\nfunction getTextContent(node: any): string {\n if (!node) return '';\n if (node.nodeName === '#text') {\n return node.value ?? '';\n }\n const childNodes = node.childNodes ?? [];\n return childNodes.map((child: any) => getTextContent(child)).join('');\n}\n\n/**\n * 判断是否为元素节点(有 tagName)\n */\nfunction isElementNode(node: any): node is {\n tagName: string;\n attrs: Array<{ name: string; value: string }>;\n childNodes?: any[];\n} {\n return node && typeof (node as any).tagName === 'string';\n}\n\n/**\n * 将 class 属性字符串按空白切分为有序列表,去重保留顺序\n */\nfunction splitClassList(classAttr: string | undefined): string[] {\n if (classAttr == null || classAttr === '') return [];\n const list = classAttr.trim().split(/\\s+/).filter(Boolean);\n return [...new Set(list)];\n}\n\n/**\n * 从 HTML 片段中解析出第一个根元素的信息\n *\n * @param fragment 单段 HTML,如 `<h1 class=\"...\">姓名</h1>`\n * @returns 第一个根元素的信息;若无元素或解析失败则返回 null\n */\nexport function parseFragmentToElement(\n fragment: string\n): ParsedFragmentElement | null {\n if (!fragment || typeof fragment !== 'string') return null;\n\n const wrapped = fragment.trim();\n if (!wrapped) return null;\n\n let fragmentNode: any;\n try {\n fragmentNode = parseFragment(wrapped);\n } catch {\n return null;\n }\n\n const childNodes = fragmentNode?.childNodes ?? [];\n for (const child of childNodes) {\n if (isElementNode(child)) {\n const classAttr = getAttr(child, 'class');\n const classList = splitClassList(classAttr);\n const textContent = getTextContent(child).trim();\n const otherAttrs: Record<string, string> = {};\n for (const a of child.attrs ?? []) {\n if (a.name?.toLowerCase() !== 'class') {\n otherAttrs[a.name] = a.value ?? '';\n }\n }\n return {\n tagName: (child.tagName ?? '').toLowerCase(),\n classList,\n textContent,\n otherAttrs,\n };\n }\n }\n return null;\n}\n\n/**\n * 对比两个 HTML 片段:解析后比较 class 增删与根元素文本变更\n *\n * @param originalFragment 原始片段(如 resolveGroupChangeFragments 的 originalFragment)\n * @param finalFragment 最终片段(如 resolveGroupChangeFragments 的 finalFragment)\n * @returns 结构化对比结果,便于展示「class 多了啥、少了啥」和「text 变更了啥」\n */\nexport function compareHtmlFragments(\n originalFragment: string,\n finalFragment: string\n): HtmlFragmentDiff {\n const original = parseFragmentToElement(originalFragment);\n const final = parseFragmentToElement(finalFragment);\n\n const originalClasses = new Set(original?.classList ?? []);\n const finalClasses = new Set(final?.classList ?? []);\n // 明确返回全新 string[],避免被误用或序列化成 Set\n const classAdded: string[] = [\n ...(final?.classList ?? []).filter((c) => !originalClasses.has(c)),\n ];\n const classRemoved: string[] = [\n ...(original?.classList ?? []).filter((c) => !finalClasses.has(c)),\n ];\n\n const textOriginal = original?.textContent ?? '';\n const textFinal = final?.textContent ?? '';\n const textChanged = textOriginal !== textFinal;\n const textSummary = textChanged\n ? `「${textOriginal || '(空)'}」 → 「${textFinal || '(空)'}」`\n : '无变更';\n\n return {\n original,\n final,\n classAdded,\n classRemoved,\n textOriginal,\n textFinal,\n textChanged,\n textSummary,\n };\n}\n\n/**\n * 消费 resolveGroupChangeFragments 的返回值:在原有片段级 diff 基础上,再解析两个 HTML 片段,\n * 得到 class 增删与文本变更的结构化结果。\n *\n * @param result resolveGroupChangeMessage / resolveGroupChangeFragments 的返回值\n * @returns 原 result 与 HTML 解析对比结果;若 result 为 undefined 则返回 undefined\n */\nexport function consumeGroupChangeResult<\n T extends { originalFragment: string; finalFragment: string }\n>(result: T | undefined): (T & { htmlDiff: HtmlFragmentDiff }) | undefined {\n if (result == null) return undefined;\n const htmlDiff = compareHtmlFragments(\n result.originalFragment,\n result.finalFragment\n );\n return { ...result, htmlDiff };\n}\n"],"names":["getAttr","node","name","attrs","_node$attrs","lower","toLowerCase","a","find","x","_x$name","value","getTextContent","nodeName","_node$value","childNodes","_node$childNodes","map","child","join","isElementNode","tagName","splitClassList","classAttr","list","trim","split","filter","Boolean","concat","Set","parseFragmentToElement","fragment","wrapped","fragmentNode","parseFragment","_unused","_fragmentNode$childNo","_fragmentNode","_iterator","_createForOfIteratorHelperLoose","_step","done","_child$tagName","classList","textContent","otherAttrs","_iterator2","_child$attrs","_step2","_a$name","_a$value","compareHtmlFragments","originalFragment","finalFragment","original","final","originalClasses","_original$classList","finalClasses","_final$classList","classAdded","_final$classList2","c","has","classRemoved","_original$classList2","textOriginal","_original$textContent","textFinal","_final$textContent","textChanged","textSummary","consumeGroupChangeResult","result","undefined","htmlDiff","_extends"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
 
2
- > html-fragment-diff-example@0.2.0 build /home/runner/work/x-oasis/x-oasis/packages/diff/html-fragment-diff/examples
2
+ > html-fragment-diff-example@0.2.1 build /home/runner/work/x-oasis/x-oasis/packages/diff/html-fragment-diff/examples
3
3
  > vite build
4
4
 
5
5
  vite v4.1.4 building for production...
@@ -9,4 +9,4 @@ rendering chunks...
9
9
  computing gzip size...
10
10
  dist/index.html  0.41 kB
11
11
  dist/assets/index-41af7777.css  2.44 kB │ gzip: 0.89 kB
12
- dist/assets/index-90f7e9a9.js 296.32 kB │ gzip: 95.11 kB
12
+ dist/assets/index-4b420c41.js 296.33 kB │ gzip: 95.12 kB
@@ -1,5 +1,13 @@
1
1
  # html-fragment-diff-example
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - cfaacab: bump version diff html
8
+ - Updated dependencies [cfaacab]
9
+ - @x-oasis/html-fragment-diff@0.2.1
10
+
3
11
  ## 0.2.0
4
12
 
5
13
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-fragment-diff-example",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-oasis/html-fragment-diff",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Parse and compare HTML fragments to detect class changes and text changes",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -141,12 +141,13 @@ export function compareHtmlFragments(
141
141
 
142
142
  const originalClasses = new Set(original?.classList ?? []);
143
143
  const finalClasses = new Set(final?.classList ?? []);
144
- const classAdded = (final?.classList ?? []).filter(
145
- (c) => !originalClasses.has(c)
146
- );
147
- const classRemoved = (original?.classList ?? []).filter(
148
- (c) => !finalClasses.has(c)
149
- );
144
+ // 明确返回全新 string[],避免被误用或序列化成 Set
145
+ const classAdded: string[] = [
146
+ ...(final?.classList ?? []).filter((c) => !originalClasses.has(c)),
147
+ ];
148
+ const classRemoved: string[] = [
149
+ ...(original?.classList ?? []).filter((c) => !finalClasses.has(c)),
150
+ ];
150
151
 
151
152
  const textOriginal = original?.textContent ?? '';
152
153
  const textFinal = final?.textContent ?? '';
@@ -65,7 +65,9 @@ describe('compareHtmlFragments', () => {
65
65
  const final = '<h1 class="title active">Hello</h1>';
66
66
  const result = compareHtmlFragments(original, final);
67
67
 
68
+ expect(Array.isArray(result.classAdded)).toBe(true);
68
69
  expect(result.classAdded).toContain('active');
70
+ expect(new Set(result.classAdded).size).toBe(result.classAdded.length);
69
71
  expect(result.classRemoved).toEqual([]);
70
72
  });
71
73
 
@@ -84,7 +86,20 @@ describe('compareHtmlFragments', () => {
84
86
  const result = compareHtmlFragments(original, final);
85
87
 
86
88
  expect(result.classRemoved).toContain('primary');
89
+ expect(Array.isArray(result.classAdded)).toBe(true);
87
90
  expect(result.classAdded).toContain('secondary');
91
+ expect(new Set(result.classAdded).size).toBe(result.classAdded.length);
92
+ });
93
+
94
+ test('classAdded should be array with no duplicates', () => {
95
+ // final 中 class 重复时,classAdded 仍应去重
96
+ const original = '<h1 class="a">x</h1>';
97
+ const final = '<h1 class="a b b c c c">x</h1>';
98
+ const result = compareHtmlFragments(original, final);
99
+
100
+ expect(Array.isArray(result.classAdded)).toBe(true);
101
+ expect(result.classAdded).toEqual(['b', 'c']);
102
+ expect(new Set(result.classAdded).size).toBe(result.classAdded.length);
88
103
  });
89
104
 
90
105
  test('should detect text changes', () => {
@@ -121,12 +136,16 @@ describe('compareHtmlFragments', () => {
121
136
  });
122
137
 
123
138
  test('should handle parsing failures gracefully', () => {
139
+ // parse5 仍会解析 <invalid> 为元素(tagName 为 'invalid'),只有无根元素或空片段时才为 null
124
140
  const original = '<invalid>';
125
141
  const final = '<h1>Valid</h1>';
126
142
  const result = compareHtmlFragments(original, final);
127
143
 
128
- expect(result.original).toBeNull();
129
144
  expect(result.final).not.toBeNull();
145
+ expect(result.final?.tagName).toBe('h1');
146
+ // classAdded/classRemoved 应为 string[],且无 class 时为空数组
147
+ expect(Array.isArray(result.classAdded)).toBe(true);
148
+ expect(Array.isArray(result.classRemoved)).toBe(true);
130
149
  expect(result.classAdded).toEqual([]);
131
150
  expect(result.classRemoved).toEqual([]);
132
151
  });
@@ -145,7 +164,11 @@ describe('consumeGroupChangeResult', () => {
145
164
 
146
165
  expect(consumed).toBeDefined();
147
166
  expect(consumed?.htmlDiff).toBeDefined();
167
+ expect(Array.isArray(consumed?.htmlDiff.classAdded)).toBe(true);
148
168
  expect(consumed?.htmlDiff.classAdded).toContain('active');
169
+ expect(new Set(consumed?.htmlDiff.classAdded).size).toBe(
170
+ consumed?.htmlDiff.classAdded.length ?? 0
171
+ );
149
172
  expect(consumed?.originalRange).toEqual(result.originalRange);
150
173
  expect(consumed?.finalRange).toEqual(result.finalRange);
151
174
  });