From 7a1cf02bc78eedc4993f05b3fa1292cf7de18706 Mon Sep 17 00:00:00 2001 From: taoqf Date: Mon, 19 Oct 2026 13:37:23 +0800 Subject: [PATCH] Revert "Avoid repeated sibling scans when repairing unclosed tags" --- src/nodes/html.ts | 19 ++----- test/tests/issues/260-unclosed-tags.js | 78 -------------------------- 2 files changed, 6 insertions(+), 91 deletions(-) delete mode 100644 test/tests/issues/260-unclosed-tags.js diff --git a/src/nodes/html.ts b/src/nodes/html.ts index 78f283e..9c6d312 100644 --- a/src/nodes/html.ts +++ b/src/nodes/html.ts @@ -1319,7 +1319,9 @@ export function parse(data: string, options = {} as Partial) { // this is wrong, becouse this will put the H3 outside the current right position which should be inside the current Html Element, see issue 152 for more info if (options.parseNoneClosedTags !== true) { oneBefore.removeChild(last); - reparentChildren(last, oneBefore.parentNode); + last.childNodes.forEach((child) => { + oneBefore.parentNode.appendChild(child); + }); stack.pop(); } } else { @@ -1328,7 +1330,9 @@ export function parse(data: string, options = {} as Partial) { // eslint-disable-next-line no-lonely-if if (options.parseNoneClosedTags !== true) { oneBefore.removeChild(last); - reparentChildren(last, oneBefore); + last.childNodes.forEach((child) => { + oneBefore.appendChild(child); + }); } } } else { @@ -1343,17 +1347,6 @@ export function parse(data: string, options = {} as Partial) { return root; } -function reparentChildren(source: HTMLElement, parent: HTMLElement) { - const children = source.childNodes; - source.childNodes = []; - for (const child of children) { - // Detach in bulk: appendChild() would otherwise filter the shrinking source - // array once per child, making repair quadratic in the number of siblings. - child.parentNode = null; - parent.appendChild(child); - } -} - /** * Resolves a list of {@link NodeInsertable} to a list of nodes, * and removes nodes from any potential parent. diff --git a/test/tests/issues/260-unclosed-tags.js b/test/tests/issues/260-unclosed-tags.js deleted file mode 100644 index 1588502..0000000 --- a/test/tests/issues/260-unclosed-tags.js +++ /dev/null @@ -1,78 +0,0 @@ -const assert = require('node:assert/strict'); -const { spawnSync } = require('node:child_process'); -const { parse } = require('@test/test-target'); - -function checkParents(parent) { - parent.childNodes.forEach((child) => { - assert.equal(child.parentNode, parent); - checkParents(child); - }); -} - -describe('unclosed tag repair', function () { - const content = 'alphabetaomega'; - for (const sameTag of [false, true]) { - const opening = sameTag ? '
' : '
'; - const input = `
before${opening}${content}`; - const repaired = `
before${sameTag ? '
' : ''}${content}
`; - - for (const preserve of [undefined, false, true]) { - it(`preserves child order and parent links (${sameTag ? 'same' : 'different'} tags, parseNoneClosedTags=${preserve})`, function () { - const root = parse(input, { comment: true, parseNoneClosedTags: preserve }); - const expected = preserve ? `${input}${sameTag ? '
' : ''}
` : repaired; - assert.equal(root.toString(), expected); - checkParents(root); - const bold = root.querySelector('b'); - assert.equal(bold.previousSibling.toString(), ''); - assert.equal(bold.nextSibling.toString(), 'omega'); - }); - } - - it(`handles empty unclosed ${sameTag ? 'same' : 'different'} tags`, function () { - const root = parse(`
${opening}`); - assert.equal(root.toString(), `
${sameTag ? '
' : ''}
`); - checkParents(root); - }); - - it(`repairs many children of unclosed ${sameTag ? 'same' : 'different'} tags within a bounded process`, function () { - this.timeout(15000); - const args = []; - if (process.env.TEST_TARGET !== 'dist' && !process.env.CI) { - args.push('-r', require.resolve('ts-node/register')); - } - args.push('-e', ` - const assert = require('node:assert/strict'); - const { parse } = require(process.argv[1]); - const sameTag = process.argv[2] === 'true'; - const count = 32000; - const content = 'item'.repeat(count); - const opening = sameTag ? '
' : '
'; - const root = parse('
' + opening + content); - const parent = root.firstChild; - const items = sameTag ? parent.childNodes.slice(1) : parent.childNodes; - assert.equal(items.length, count); - for (const item of items) { - assert.equal(item.rawTagName, 'b'); - assert.equal(item.text, 'item'); - assert.equal(item.parentNode, parent); - assert.equal(item.firstChild.parentNode, item); - } - assert.equal(root.toString(), '
' + (sameTag ? '
' : '') + content + '
'); - process.stdout.write('ok'); - `, require.resolve('@test/test-target'), String(sameTag)); - // Isolate synchronous parsing so a regression cannot hang the test runner. - // The linear path has ample headroom; repeated sibling filtering times out. - const result = spawnSync(process.execPath, args, { timeout: 8000, encoding: 'utf8', windowsHide: true }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); - assert.equal(result.stdout, 'ok'); - }); - } - - it('keeps properly closed parents and their mixed children', function () { - const html = `
before
${content}
after
`; - const root = parse(html, { comment: true }); - assert.equal(root.toString(), html); - checkParents(root); - }); -});