From 124080924f913951c411722af51963ad03e274f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 03:02:40 +0000 Subject: [PATCH 1/4] chore(deps): bump test/data/html5lib-tests from `a9f4496` to `8f43b7e` Bumps [test/data/html5lib-tests](https://github.com/html5lib/html5lib-tests) from `a9f4496` to `8f43b7e`. - [Commits](https://github.com/html5lib/html5lib-tests/compare/a9f44960a9fedf265093d22b2aa3c7ca123727b9...8f43b7ec8c9d02179f5f38e0ea08cb5000fb9c9e) --- updated-dependencies: - dependency-name: test/data/html5lib-tests dependency-version: 8f43b7ec8c9d02179f5f38e0ea08cb5000fb9c9e dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- test/data/html5lib-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data/html5lib-tests b/test/data/html5lib-tests index a9f44960a..8f43b7ec8 160000 --- a/test/data/html5lib-tests +++ b/test/data/html5lib-tests @@ -1 +1 @@ -Subproject commit a9f44960a9fedf265093d22b2aa3c7ca123727b9 +Subproject commit 8f43b7ec8c9d02179f5f38e0ea08cb5000fb9c9e From a9bde00e9e7cc6bc91a62c972007b17f3b9d8aa4 Mon Sep 17 00:00:00 2001 From: Felix <188768+fb55@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:02:29 +0000 Subject: [PATCH 2/4] implement --- package-lock.json | 168 ++++++-- .../test/parser-stream.test.ts | 21 + packages/parse5/lib/common/html.ts | 4 + packages/parse5/lib/parser/index.test.ts | 21 + packages/parse5/lib/parser/index.ts | 396 +++++++++--------- .../lib/parser/open-element-stack.test.ts | 17 - .../parse5/lib/parser/open-element-stack.ts | 24 +- 7 files changed, 360 insertions(+), 291 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8288f4d55..837925c73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1259,6 +1259,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -1638,6 +1639,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1744,10 +1746,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1773,6 +1776,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2115,6 +2119,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2397,23 +2402,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3335,6 +3323,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyrainbow": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", @@ -3436,10 +3454,11 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3464,6 +3483,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3505,8 +3525,7 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "peer": true + "dev": true }, "node_modules/update-browserslist-db": { "version": "1.1.3", @@ -3560,6 +3579,8 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3629,11 +3650,43 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -4588,6 +4641,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -4823,7 +4877,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -4904,9 +4959,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -4918,6 +4973,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5139,6 +5195,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5332,13 +5389,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} - }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6035,6 +6085,22 @@ "requires": { "fdir": "^6.5.0", "picomatch": "^4.0.3" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true + } } }, "tinyrainbow": { @@ -6094,9 +6160,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -6117,7 +6183,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "peer": true }, "typescript-eslint": { "version": "8.49.0", @@ -6141,8 +6208,7 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "peer": true + "dev": true }, "update-browserslist-db": { "version": "1.1.3", @@ -6174,6 +6240,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6182,6 +6249,22 @@ "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true + } } }, "vitest": { @@ -6189,6 +6272,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, + "peer": true, "requires": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", diff --git a/packages/parse5-parser-stream/test/parser-stream.test.ts b/packages/parse5-parser-stream/test/parser-stream.test.ts index 006603a59..90d64469f 100644 --- a/packages/parse5-parser-stream/test/parser-stream.test.ts +++ b/packages/parse5-parser-stream/test/parser-stream.test.ts @@ -17,6 +17,27 @@ generateParsingTests( '40.foreign-fragment', '47.foreign-fragment', '48.foreign-fragment', + // Select parsing was relaxed in the HTML spec. + // https://github.com/whatwg/html/pull/10548 + // The forked test suite still tests the old behaviour. + '13.menuitem-element', + '29.tests1', + '101.tests1', + '3.tests10', + '4.tests10', + '16.tests10', + '17.tests10', + '4.tests9', + '5.tests9', + '17.tests9', + '18.tests9', + '13.tests18', + '14.tests18', + '17.webkit02', + '30.tests7', + '79.tests_innerHTML_1', + '80.tests_innerHTML_1', + '81.tests_innerHTML_1', ], }, (test, opts) => parseChunked(test, opts), diff --git a/packages/parse5/lib/common/html.ts b/packages/parse5/lib/common/html.ts index d9d341243..fb6e6dbad 100644 --- a/packages/parse5/lib/common/html.ts +++ b/packages/parse5/lib/common/html.ts @@ -17,6 +17,7 @@ export enum ATTRS { COLOR = 'color', FACE = 'face', SIZE = 'size', + SELECTED = 'selected', } /** @@ -142,6 +143,7 @@ export enum TAG_NAMES { SEARCH = 'search', SECTION = 'section', SELECT = 'select', + SELECTEDCONTENT = 'selectedcontent', SOURCE = 'source', SMALL = 'small', SPAN = 'span', @@ -296,6 +298,7 @@ export enum TAG_ID { SEARCH, SECTION, SELECT, + SELECTEDCONTENT, SOURCE, SMALL, SPAN, @@ -428,6 +431,7 @@ const TAG_NAME_TO_ID = new Map([ [TAG_NAMES.SEARCH, TAG_ID.SEARCH], [TAG_NAMES.SECTION, TAG_ID.SECTION], [TAG_NAMES.SELECT, TAG_ID.SELECT], + [TAG_NAMES.SELECTEDCONTENT, TAG_ID.SELECTEDCONTENT], [TAG_NAMES.SOURCE, TAG_ID.SOURCE], [TAG_NAMES.SMALL, TAG_ID.SMALL], [TAG_NAMES.SPAN, TAG_ID.SPAN], diff --git a/packages/parse5/lib/parser/index.test.ts b/packages/parse5/lib/parser/index.test.ts index f45a93cf9..0d8f97f1c 100644 --- a/packages/parse5/lib/parser/index.test.ts +++ b/packages/parse5/lib/parser/index.test.ts @@ -18,6 +18,27 @@ generateParsingTests( '40.foreign-fragment', '47.foreign-fragment', '48.foreign-fragment', + // Select parsing was relaxed in the HTML spec. + // https://github.com/whatwg/html/pull/10548 + // The forked test suite still tests the old behaviour. + '13.menuitem-element', + '29.tests1', + '101.tests1', + '3.tests10', + '4.tests10', + '16.tests10', + '17.tests10', + '4.tests9', + '5.tests9', + '17.tests9', + '18.tests9', + '13.tests18', + '14.tests18', + '17.webkit02', + '30.tests7', + '79.tests_innerHTML_1', + '80.tests_innerHTML_1', + '81.tests_innerHTML_1', ], }, (test, opts) => ({ diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 9779f68f3..06a3ecac3 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -54,8 +54,6 @@ enum InsertionMode { IN_TABLE_BODY, IN_ROW, IN_CELL, - IN_SELECT, - IN_SELECT_IN_TABLE, IN_TEMPLATE, AFTER_BODY, IN_FRAMESET, @@ -252,6 +250,16 @@ export class Parser implements TokenHandler, Stack /** @internal */ fosterParentingEnabled = false; + // Selectedcontent cloning state + /** @internal */ + enabledSelectedcontent: T['element'] | null = null; + /** @internal */ + selectedcontentSelect: T['element'] | null = null; + /** @internal */ + firstOptionInSelect: T['element'] | null = null; + /** @internal */ + selectedOptionInSelect: T['element'] | null = null; + //Errors /** @internal */ @@ -288,6 +296,12 @@ export class Parser implements TokenHandler, Stack this.treeAdapter.onItemPop?.(node, this.openElements.current); + // Handle selectedcontent cloning when SELECT is popped + const tid = this.treeAdapter.isElementNode(node) ? getTagID(this.treeAdapter.getTagName(node)) : $.UNKNOWN; + if (tid === $.SELECT) { + this._finalizeSelectedcontent(); + } + if (isTop) { let current; let currentTagId; @@ -311,6 +325,84 @@ export class Parser implements TokenHandler, Stack !isHTML && current !== undefined && tid !== undefined && !this._isIntegrationPoint(tid, current); } + // Selectedcontent cloning helpers + + /** @internal */ + /** @internal */ + _finalizeSelectedcontent(): void { + if (this.enabledSelectedcontent) { + this._maybeCloneOptionIntoSelectedcontent(); + this.enabledSelectedcontent = null; + this.selectedcontentSelect = null; + this.firstOptionInSelect = null; + this.selectedOptionInSelect = null; + } + } + + protected _maybeCloneOptionIntoSelectedcontent(): void { + // Only clone if we have a selectedcontent and an option to clone + if (!this.enabledSelectedcontent) return; + + // Determine which option should be cloned: + // - If there's a selectedOptionInSelect (option with 'selected' attribute), clone that + // - Otherwise, clone the firstOptionInSelect (first option in the select) + const optionToClone = this.selectedOptionInSelect ?? this.firstOptionInSelect; + if (!optionToClone) return; + + // Clone the option's children into the selectedcontent + this._cloneChildrenInto(optionToClone, this.enabledSelectedcontent); + } + + /** @internal */ + protected _cloneChildrenInto(source: T['element'], target: T['element']): void { + // First, clear the target's children + for ( + let child = this.treeAdapter.getFirstChild(target); + child; + child = this.treeAdapter.getFirstChild(target) + ) { + this.treeAdapter.detachNode(child); + } + + // Clone each child from source to target + const children = this.treeAdapter.getChildNodes(source); + for (const child of children) { + const cloned = this._deepCloneNode(child); + this.treeAdapter.appendChild(target, cloned); + } + } + + /** @internal */ + protected _deepCloneNode(node: T['childNode']): T['childNode'] { + if (this.treeAdapter.isTextNode(node)) { + return this.treeAdapter.createTextNode(this.treeAdapter.getTextNodeContent(node)) as T['childNode']; + } + + if (this.treeAdapter.isCommentNode(node)) { + return this.treeAdapter.createCommentNode(this.treeAdapter.getCommentNodeContent(node)) as T['childNode']; + } + + if (this.treeAdapter.isElementNode(node)) { + const tagName = this.treeAdapter.getTagName(node); + const ns = this.treeAdapter.getNamespaceURI(node); + const attrs = this.treeAdapter.getAttrList(node); + const clone = this.treeAdapter.createElement(tagName, ns, attrs) as T['element']; + + // Recursively clone children + const children = this.treeAdapter.getChildNodes(node as T['parentNode']); + for (const child of children) { + const clonedChild = this._deepCloneNode(child); + this.treeAdapter.appendChild(clone, clonedChild); + } + + return clone as T['childNode']; + } + + // For other node types (like document type), we don't clone them + // This shouldn't happen in practice for option children + return node; + } + /** @protected */ _switchToTextParsing( currentToken: TagToken, @@ -708,10 +800,6 @@ export class Parser implements TokenHandler, Stack this.insertionMode = InsertionMode.IN_FRAMESET; return; } - case $.SELECT: { - this._resetInsertionModeForSelect(i); - return; - } case $.TEMPLATE: { this.insertionMode = this.tmplInsertionModeStack[0]; return; @@ -741,24 +829,6 @@ export class Parser implements TokenHandler, Stack this.insertionMode = InsertionMode.IN_BODY; } - /** @protected */ - _resetInsertionModeForSelect(selectIdx: number): void { - if (selectIdx > 0) { - for (let i = selectIdx - 1; i > 0; i--) { - const tn = this.openElements.tagIDs[i]; - - if (tn === $.TEMPLATE) { - break; - } else if (tn === $.TABLE) { - this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE; - return; - } - } - } - - this.insertionMode = InsertionMode.IN_SELECT; - } - //Foster parenting /** @protected */ @@ -865,9 +935,7 @@ export class Parser implements TokenHandler, Stack characterInBody(this, token); break; } - case InsertionMode.TEXT: - case InsertionMode.IN_SELECT: - case InsertionMode.IN_SELECT_IN_TABLE: { + case InsertionMode.TEXT: { this._insertCharacters(token); break; } @@ -980,8 +1048,6 @@ export class Parser implements TokenHandler, Stack case InsertionMode.IN_TABLE_BODY: case InsertionMode.IN_ROW: case InsertionMode.IN_CELL: - case InsertionMode.IN_SELECT: - case InsertionMode.IN_SELECT_IN_TABLE: case InsertionMode.IN_TEMPLATE: case InsertionMode.IN_FRAMESET: case InsertionMode.AFTER_FRAMESET: { @@ -1116,14 +1182,6 @@ export class Parser implements TokenHandler, Stack startTagInCell(this, token); break; } - case InsertionMode.IN_SELECT: { - startTagInSelect(this, token); - break; - } - case InsertionMode.IN_SELECT_IN_TABLE: { - startTagInSelectInTable(this, token); - break; - } case InsertionMode.IN_TEMPLATE: { startTagInTemplate(this, token); break; @@ -1226,14 +1284,6 @@ export class Parser implements TokenHandler, Stack endTagInCell(this, token); break; } - case InsertionMode.IN_SELECT: { - endTagInSelect(this, token); - break; - } - case InsertionMode.IN_SELECT_IN_TABLE: { - endTagInSelectInTable(this, token); - break; - } case InsertionMode.IN_TEMPLATE: { endTagInTemplate(this, token); break; @@ -1291,9 +1341,7 @@ export class Parser implements TokenHandler, Stack case InsertionMode.IN_COLUMN_GROUP: case InsertionMode.IN_TABLE_BODY: case InsertionMode.IN_ROW: - case InsertionMode.IN_CELL: - case InsertionMode.IN_SELECT: - case InsertionMode.IN_SELECT_IN_TABLE: { + case InsertionMode.IN_CELL: { eofInBody(this, token); break; } @@ -1346,8 +1394,6 @@ export class Parser implements TokenHandler, Stack case InsertionMode.AFTER_HEAD: case InsertionMode.TEXT: case InsertionMode.IN_COLUMN_GROUP: - case InsertionMode.IN_SELECT: - case InsertionMode.IN_SELECT_IN_TABLE: case InsertionMode.IN_FRAMESET: case InsertionMode.AFTER_FRAMESET: { this._insertCharacters(token); @@ -1568,6 +1614,9 @@ function appendCommentToDocument(p: Parser, tok } function stopParsing(p: Parser, token: EOFToken): void { + // Handle selectedcontent cloning before parsing stops (when select wasn't explicitly closed) + p._finalizeSelectedcontent(); + p.stopped = true; // NOTE: Set end locations for elements that remain on the open element stack. @@ -2144,6 +2193,12 @@ function isHiddenInput(token: TagToken): boolean { } function inputStartTagInBody(p: Parser, token: TagToken): void { + // If there's a element in scope, generate implied end tags + if (p.openElements.hasInScope($.SELECT)) { + p.openElements.generateImpliedEndTags(); + // Note: spec says it's a parse error if option or optgroup is in scope, but we don't emit parse errors + } + p._appendElement(token, NS.HTML); p.framesetOk = false; token.ackSelfClosing = true; @@ -2176,6 +2237,12 @@ function imageStartTagInBody(p: Parser, token: } function textareaStartTagInBody(p: Parser, token: TagToken): void { + // If there's a , ignore the token + if (p.fragmentContext && p.fragmentContextID === $.SELECT) { + return; + } + + // If there's a + + + + `; + + const doc = parse(html); + const htmlEl = doc.childNodes.find((n) => n.nodeName === 'html') as Element; + const body = htmlEl.childNodes.find((n) => n.nodeName === 'body') as Element; + const select = body.childNodes.find((n) => (n as Element).tagName === 'select') as Element; + const button = select.childNodes.find((n) => (n as Element).tagName === 'button') as Element; + const selectedcontent = button.childNodes.find( + (n) => (n as Element).tagName === 'selectedcontent', + ) as Element; + + expect(selectedcontent.childNodes.length).toBe(0); + }); + + it('should enable selectedcontent for select without multiple', () => { + const html = ` + + `; + + const doc = parse(html); + const htmlEl = doc.childNodes.find((n) => n.nodeName === 'html') as Element; + const body = htmlEl.childNodes.find((n) => n.nodeName === 'body') as Element; + const select = body.childNodes.find((n) => (n as Element).tagName === 'select') as Element; + const button = select.childNodes.find((n) => (n as Element).tagName === 'button') as Element; + const selectedcontent = button.childNodes.find( + (n) => (n as Element).tagName === 'selectedcontent', + ) as Element; + + expect(selectedcontent.childNodes.length).toBeGreaterThan(0); + expect((selectedcontent.childNodes[0] as TextNode).value).toBe('foo'); + }); + }); }); diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 06a3ecac3..c4f314114 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -2292,7 +2292,7 @@ function selectStartTagInBody(p: Parser, token: p.framesetOk = false; // Initialize selectedcontent state for this select - p.selectedcontentSelect = p.openElements.current; + p.selectedcontentSelect = getTokenAttr(token, 'multiple') === null ? p.openElements.current : null; p.enabledSelectedcontent = null; p.firstOptionInSelect = null; p.selectedOptionInSelect = null; From b2aa7ae07a06de74d0e33d7e2d9be548b26eca8e Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 4 May 2026 13:00:23 +0100 Subject: [PATCH 4/4] chore: remove duplicate jsdoc --- packages/parse5/lib/parser/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index c4f314114..09a6f2345 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -327,7 +327,6 @@ export class Parser implements TokenHandler, Stack // Selectedcontent cloning helpers - /** @internal */ /** @internal */ _finalizeSelectedcontent(): void { if (this.enabledSelectedcontent) { @@ -339,6 +338,7 @@ export class Parser implements TokenHandler, Stack } } + /** @internal */ protected _maybeCloneOptionIntoSelectedcontent(): void { // Only clone if we have a selectedcontent and an option to clone if (!this.enabledSelectedcontent) return;