From 99077f502a6e282a8a731581b239b7bbde7252c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Sat, 8 Feb 2025 19:18:19 +0100 Subject: [PATCH 01/21] Add experimental support to build using Bikeshed --- build.sh | 58 +++++++++++++++++++++++++++++++++-------------------- src/main.rs | 14 +++++++++++++ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/build.sh b/build.sh index 063f9f0..74d913f 100755 --- a/build.sh +++ b/build.sh @@ -15,6 +15,7 @@ declare -r WATTSI_LATEST=140 # Shared state variables throughout this script LOCAL_WATTSI=true WATTSI_RESULT=0 +USE_BIKESHED=false DO_UPDATE=true DO_LINT=true DO_HIGHLIGHT=true @@ -37,6 +38,7 @@ HTML_GIT_CLONE_OPTIONS=${HTML_GIT_CLONE_OPTIONS:-"--depth=2"} # This is used by child scripts, and so we export it export HTML_CACHE +export USE_BIKESHED # Used specifically when the Dockerfile calls this script SKIP_BUILD_UPDATE_CHECK=${SKIP_BUILD_UPDATE_CHECK:-false} @@ -85,14 +87,16 @@ function main { exit 0 fi - checkWattsi - ensureHighlighterInstalled + if [[ $USE_BIKESHED != "true" ]]; then + checkWattsi + ensureHighlighterInstalled - doLint + doLint - updateRemoteDataFiles + updateRemoteDataFiles - startHighlightServer + startHighlightServer + fi processSource "source" "default" @@ -146,6 +150,7 @@ function processCommandLineArgs { echo " $0 help Show this usage statement." echo echo "Build options:" + echo " -b|--bikeshed Use Bikeshed instead of Wattsi. (experimental)" echo " -d|--docker Use Docker to build in a container." echo " -r|--remote Use the build server." echo " -s|--serve After building, serve the results on http://localhost:$SERVE_PORT." @@ -176,6 +181,9 @@ function processCommandLineArgs { DO_HIGHLIGHT=false SINGLE_PAGE_ONLY=true ;; + -b|--bikeshed) + USE_BIKESHED=true + ;; -d|--docker) USE_DOCKER=true ;; @@ -663,27 +671,33 @@ function processSource { cargo run "${cargo_args[@]}" <"$HTML_SOURCE/$source_location" >"$HTML_TEMP/source-whatwg-complete" fi - runWattsi "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/wattsi-output" - if [[ $WATTSI_RESULT == "0" ]]; then - if [[ $LOCAL_WATTSI != "true" ]]; then - "$QUIET" || grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines - fi + if [[ $USE_BIKESHED == "true" ]]; then + echo "BIKESHED!!!" + bikeshed spec --byos "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/bikeshed-output" --md-Text-Macro="SHA $HTML_SHA" + exit 0 else - if [[ $LOCAL_WATTSI != "true" ]]; then - "$QUIET" || grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines - fi - if [[ $WATTSI_RESULT == "65" ]]; then - echo - echo "There were errors. Running again to show the original line numbers." - echo - runWattsi "$HTML_SOURCE/$source_location" "$HTML_TEMP/wattsi-raw-source-output" + runWattsi "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/wattsi-output" + if [[ $WATTSI_RESULT == "0" ]]; then if [[ $LOCAL_WATTSI != "true" ]]; then - grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines + "$QUIET" || grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines fi + else + if [[ $LOCAL_WATTSI != "true" ]]; then + "$QUIET" || grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines + fi + if [[ $WATTSI_RESULT == "65" ]]; then + echo + echo "There were errors. Running again to show the original line numbers." + echo + runWattsi "$HTML_SOURCE/$source_location" "$HTML_TEMP/wattsi-raw-source-output" + if [[ $LOCAL_WATTSI != "true" ]]; then + grep -v '^$' "$HTML_TEMP/wattsi-output.txt" # trim blank lines + fi + fi + echo + echo "There were errors. Stopping." + exit "$WATTSI_RESULT" fi - echo - echo "There were errors. Stopping." - exit "$WATTSI_RESULT" fi # Keep the list of files copied from $HTML_SOURCE in sync with `doServerBuild` diff --git a/src/main.rs b/src/main.rs index 14ee301..a02ed13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,14 @@ mod rcdom_with_line_numbers; mod represents; mod tag_omission; +const BIKEPLATE: &str = "
+Group: WHATWG
+H1: HTML
+Shortname: html
+Abstract: HTML is Bikeshed.
+Indent: 1
+
"; + #[tokio::main] async fn main() -> io::Result<()> { // This gives slightly prettier error-printing. @@ -35,6 +43,12 @@ async fn run() -> io::Result<()> { // Find the paths we need. let cache_dir = path_from_env("HTML_CACHE", ".cache"); let source_dir = path_from_env("HTML_SOURCE", "../html"); + let use_bikeshed_str = env::var_os("USE_BIKESHED"); + let use_bikeshed = use_bikeshed_str.is_some_and(|s| s.eq_ignore_ascii_case("TRUE")); + if use_bikeshed { + eprintln!("BIKESHED MODE"); + println!("{}", BIKEPLATE); + } // Because parsing can jump around the tree a little, it's most reasonable // to just parse the whole document before doing any processing. Even for From 119d1a985f675104a88be46899ebbc2191346a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Tue, 4 Mar 2025 16:55:13 +0100 Subject: [PATCH 02/21] Quickly hack up the conversion in JS --- .gitignore | 1 + build.sh | 22 +- package-lock.json | 686 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 + src/bikeshed.rs | 21 ++ src/dom_utils.rs | 2 +- src/main.rs | 17 +- wattsi2bikeshed.js | 69 +++++ 8 files changed, 807 insertions(+), 18 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/bikeshed.rs create mode 100644 wattsi2bikeshed.js diff --git a/.gitignore b/.gitignore index 5bbd5ad..9d61d2a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ html/ output/ mdn/.id-list mdn/developer.mozilla.org/ +node_modules/ highlighter/ diff --git a/build.sh b/build.sh index 74d913f..7d6f21d 100755 --- a/build.sh +++ b/build.sh @@ -183,6 +183,7 @@ function processCommandLineArgs { ;; -b|--bikeshed) USE_BIKESHED=true + SINGLE_PAGE_ONLY=true ;; -d|--docker) USE_DOCKER=true @@ -672,9 +673,14 @@ function processSource { fi if [[ $USE_BIKESHED == "true" ]]; then - echo "BIKESHED!!!" - bikeshed spec --byos "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/bikeshed-output" --md-Text-Macro="SHA $HTML_SHA" - exit 0 + clearDir "$HTML_TEMP/bikeshed-output" + + # TODO: port to html-build Rust code + node wattsi2bikeshed.js "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/source-whatwg-complete.bs" + + local bikeshed_args=( --force ) + $DO_UPDATE || bikeshed_args+=( --no-update ) + bikeshed "${bikeshed_args[@]}" spec "$HTML_TEMP/source-whatwg-complete.bs" "$HTML_TEMP/bikeshed-output/index.html" --md-Text-Macro="SHA $HTML_SHA" --md-Text-Macro="COMMIT-SHA $HTML_SHA" else runWattsi "$HTML_TEMP/source-whatwg-complete" "$HTML_TEMP/wattsi-output" if [[ $WATTSI_RESULT == "0" ]]; then @@ -704,7 +710,11 @@ function processSource { if [[ $build_type == "default" ]]; then # Singlepage HTML - mv "$HTML_TEMP/wattsi-output/index-html" "$HTML_OUTPUT/index.html" + if [[ $USE_BIKESHED == "true" ]]; then + mv "$HTML_TEMP/bikeshed-output/index.html" "$HTML_OUTPUT/index.html" + else + mv "$HTML_TEMP/wattsi-output/index-html" "$HTML_OUTPUT/index.html" + fi if [[ $SINGLE_PAGE_ONLY == "false" ]]; then # Singlepage Commit Snapshot @@ -720,7 +730,9 @@ function processSource { fi cp -p entities/out/entities.json "$HTML_OUTPUT" - cp -p "$HTML_TEMP/wattsi-output/xrefs.json" "$HTML_OUTPUT" + if [[ $USE_BIKESHED == "false" ]]; then + cp -p "$HTML_TEMP/wattsi-output/xrefs.json" "$HTML_OUTPUT" + fi clearDir "$HTML_TEMP" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e8828dc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,686 @@ +{ + "name": "html-build", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "jsdom": "^26.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", + "integrity": "sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==", + "dependencies": { + "@csstools/css-calc": "^2.1.1", + "@csstools/css-color-parser": "^3.0.7", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", + "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", + "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cssstyle": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", + "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "dependencies": { + "@asamuzakjp/css-color": "^2.8.2", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/jsdom": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", + "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.1", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==" + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/tldts": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.82.tgz", + "integrity": "sha512-KCTjNL9F7j8MzxgfTgjT+v21oYH38OidFty7dH00maWANAI2IsLw2AnThtTJi9HKALHZKQQWnNebYheadacD+g==", + "dependencies": { + "tldts-core": "^6.1.82" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.82", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.82.tgz", + "integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w==" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e88d15 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "jsdom": "^26.0.0" + } +} diff --git a/src/bikeshed.rs b/src/bikeshed.rs new file mode 100644 index 0000000..e4a4a07 --- /dev/null +++ b/src/bikeshed.rs @@ -0,0 +1,21 @@ +//! Convert source to Bikeshed syntax. + +use crate::dom_utils::NodeHandleExt; +use markup5ever_rcdom::Handle; + +pub struct Processor { +} + +impl Processor { + pub fn new() -> Self { + Processor { + } + } + + pub fn visit(&mut self, node: &Handle) { + // Remove the
+ if node.has_id("ref-list") { + // node.remove_from_parent(); + } + } +} diff --git a/src/dom_utils.rs b/src/dom_utils.rs index a7ca9cd..f306b43 100644 --- a/src/dom_utils.rs +++ b/src/dom_utils.rs @@ -5,7 +5,7 @@ use html5ever::tendril::StrTendril; use html5ever::{local_name, namespace_url, ns, Attribute, LocalName, QualName}; use markup5ever_rcdom::{Handle, Node, NodeData}; -/// Extensions to the DOM interface to make manipulation more ergonimc. +/// Extensions to the DOM interface to make manipulation more ergonomic. pub trait NodeHandleExt { /// Returns a handle to the parent node, if there is one. fn parent_node(&self) -> Option diff --git a/src/main.rs b/src/main.rs index a02ed13..e8e9b9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,14 +17,7 @@ mod parser; mod rcdom_with_line_numbers; mod represents; mod tag_omission; - -const BIKEPLATE: &str = ""; +mod bikeshed; #[tokio::main] async fn main() -> io::Result<()> { @@ -45,10 +38,6 @@ async fn run() -> io::Result<()> { let source_dir = path_from_env("HTML_SOURCE", "../html"); let use_bikeshed_str = env::var_os("USE_BIKESHED"); let use_bikeshed = use_bikeshed_str.is_some_and(|s| s.eq_ignore_ascii_case("TRUE")); - if use_bikeshed { - eprintln!("BIKESHED MODE"); - println!("{}", BIKEPLATE); - } // Because parsing can jump around the tree a little, it's most reasonable // to just parse the whole document before doing any processing. Even for @@ -60,6 +49,7 @@ async fn run() -> io::Result<()> { let mut annotate_attributes = annotate_attributes::Processor::new(); let mut tag_omission = tag_omission::Processor::new(); let mut interface_index = interface_index::Processor::new(); + let mut bikeshed = bikeshed::Processor::new(); // We do exactly one pass to identify the changes that need to be made. dom_utils::scan_dom(&document, &mut |h| { @@ -68,6 +58,9 @@ async fn run() -> io::Result<()> { annotate_attributes.visit(h); tag_omission.visit(h); interface_index.visit(h); + if use_bikeshed { + bikeshed.visit(h); + } }); // And then we apply all of the changes. These different processors mostly diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js new file mode 100644 index 0000000..6ed93e1 --- /dev/null +++ b/wattsi2bikeshed.js @@ -0,0 +1,69 @@ +import { JSDOM } from "jsdom"; +import { readFileSync, writeFileSync } from "node:fs"; + +const boilerplate = ``; + +function convert(infile, outfile) { + const source = readFileSync(infile, 'utf-8'); + const dom = new JSDOM(source); + const document = dom.window.document; + + document.body.prepend(JSDOM.fragment(boilerplate)); + + document.getElementById('ref-list').remove(); + + for (const elem of document.querySelectorAll('[data-x]')) { + const value = elem.getAttribute('data-x'); + if (value) { + if (elem.hasAttribute('lt')) { + console.warn('Overwriting existing lt attribute:', elem.outerHTML); + } + elem.setAttribute('lt', value); + } else { + // TODO: what is an empty data-x attribute for? + // console.warn('Empty data-x attribute:', elem.outerHTML); + } + elem.removeAttribute('data-x'); + } + + for (const elem of document.querySelectorAll('[data-x-href]')) { + // TODO + elem.removeAttribute('data-x-href'); + } + + for (const elem of document.querySelectorAll('*')) { + for (const attrName of elem.getAttributeNames()) { + if (!attrName.startsWith('data-')) { + continue; + } + switch (attrName) { + case 'data-lt': + // TODO, handle these somehow + break; + case 'data-noexport': + // Leave alone, see comment in source. + break; + default: + console.warn('Unhandled data attribute:', elem.outerHTML); + } + } + } + + const output = document.body.innerHTML + .replaceAll('[[', '\\[['); + + writeFileSync(outfile, output, 'utf-8'); +} + +convert(process.argv[2], process.argv[3]); From a2b84bcce098736aca6678f70c0b0be9978009f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Fri, 7 Mar 2025 18:50:04 +0100 Subject: [PATCH 03/21] Work towards working s and their use --- wattsi2bikeshed.js | 267 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 259 insertions(+), 8 deletions(-) diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js index 6ed93e1..937cf9d 100644 --- a/wattsi2bikeshed.js +++ b/wattsi2bikeshed.js @@ -11,9 +11,53 @@ Text Macro: LATESTRD 2025-01 Abstract: HTML is Bikeshed. Indent: 1 Markup Shorthands: css off +Complain About: accidental-2119 off, missing-example-ids off Include MDN Panels: false `; +const kCrossRefAttribute = 'data-x'; + +// Hoist data-x attributes to or , to match how Wattsi uses the +// data-x attribute of a single child element when present: +// https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L888 +function hoistDataX(from, to) { + const value = from.getAttribute(kCrossRefAttribute); + if (from.parentNode === to && to.firstChild === to.lastChild) { + to.setAttribute(kCrossRefAttribute, value); + } else if (value) { + // console.warn('Ineffectual data-x in source:', to.outerHTML); + } + from.removeAttribute(kCrossRefAttribute); +} + +function isElement(node) { + return node?.nodeType === 1; +} + +function isText(node) { + return node?.nodeType === 3; +} + +// Get the "topic identifier" for cross-references like Wattsi: +// https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L882-L894 +function getTopicIdentifier(elem) { + let result; + if (elem.hasAttribute(kCrossRefAttribute)) { + result = elem.getAttribute(kCrossRefAttribute); + } else if (isElement(elem.firstChild) && elem.firstChild === elem.lastChild) { + result = getTopicIdentifier(elem.firstChild); + } else { + result = elem.textContent; + } + // This matches Wattsi's MungeStringToTopic in spirit, + // but perhaps not in every detail: + return result + .replaceAll('#', '') + .replaceAll(/\s+/g, ' ') + .toLowerCase() + .trim(); +} + function convert(infile, outfile) { const source = readFileSync(infile, 'utf-8'); const dom = new JSDOM(source); @@ -21,18 +65,225 @@ function convert(infile, outfile) { document.body.prepend(JSDOM.fragment(boilerplate)); - document.getElementById('ref-list').remove(); + for (const dt of document.querySelectorAll('#ref-list dt')) { + const node = dt.firstChild; + if (isText(node) && node.data.startsWith('[')) { + node.data = '\\' + node.data; + } + } + + // TODO: handle w-* variant attributes like Wattsi does: + // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L735-L759 + + // Scan all definitions + const crossRefs = new Map(); + for (const dfn of document.querySelectorAll('dfn')) { + if (dfn.getAttribute(kCrossRefAttribute) === '') { + continue; + } + const topic = getTopicIdentifier(dfn); + if (crossRefs.has(topic)) { + console.warn('Duplicate topic:', topic); + } + crossRefs.set(topic, dfn); + // for (const elem of dfn.querySelectorAll('[data-x]')) { + // hoistDataX(elem, dfn); + // } + + // Remove all data-x attributes. If this changes the topic, then + // it came from data-x and is copied over to lt for Bikeshed. + dfn.removeAttribute(kCrossRefAttribute); + for (const elem of dfn.querySelectorAll('[data-x]')) { + elem.removeAttribute(kCrossRefAttribute); + } + if (getTopicIdentifier(dfn) !== topic) { + dfn.setAttribute('lt', topic); + } + } + + // Replace with the inner or a new . + const spans = document.querySelectorAll('span'); + for (const [i, span] of Object.entries(spans)) { + // Don't touch any span with a descendent span. + if (span.contains(spans[+i + 1])) { + // TODO: vet for weird cases that need fixing + continue; + } + // Leave dev/nodev alone here. + if (span.hasAttribute('w-dev') || span.hasAttribute('w-nodev')) { + continue; + } + // Leave in SVG alone. + if (span.hasAttribute('xmlns')) { + continue; + } + + if (span.hasAttribute('subdfn')) { + // TODO: transform to a regular ? + // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L86 + continue; + } + + // Empty data-x="" means it's not a link. + if (span.getAttribute(kCrossRefAttribute) === '') { + continue; + } + + // An empty span with an ID is used to preserve old IDs. + // TODO: hoist to oldids attribute for Bikeshed + if (span.hasAttribute('id') && span.firstChild === null) { + continue; + } + + // for (const elem of span.querySelectorAll('[data-x]')) { + // hoistDataX(elem, span); + // } + + const topic = getTopicIdentifier(span); + const dfn = crossRefs.get(topic); + if (!dfn) { + // TODO: vet these cases for any that should actually be linked + // console.log(span.outerHTML); + continue; + } + + // For foo and "SyntaxError", + // drop the outer and depend on the linking logic. Note that this + // excludes the surrounding quotes from the link text, which is a minor change. + // The element is further transformed in a following step. + function isQuote(node) { + return isText(node) && node.data === '"'; + } + const code = span.querySelector('code'); + if (code && ( + // is the single child + (span.childNodes.length === 1 && span.firstChild === code) || + // has surrounding " text nodes + (span.childNodes.length === 3 && + isQuote(span.firstChild) && isQuote(span.lastChild) && + span.firstChild.nextSibling === code))) { + if (span.hasAttributes()) { + console.warn('Discarding attributes:', span.outerHTML); + } + // Move children to replace span. + while (span.firstChild) { + span.parentNode.insertBefore(span.firstChild, span); + } + span.remove(); + continue; + } + + // Output a instead of . + const a = document.createElement('a'); + + // Remove all data-x attributes. This might change the computed topic. + span.removeAttribute(kCrossRefAttribute); // not actually needed + for (const elem of span.querySelectorAll('[data-x]')) { + elem.removeAttribute(kCrossRefAttribute); + } + + + for (const name of span.getAttributeNames()) { + const value = span.getAttribute(name); + switch (name) { + case 'id': + // Copy over. + a.setAttribute(name, value); + break; + default: + console.warn('Unhandled attribute:', name); + } + } + // Move the children over to replace itself. + while (span.firstChild) { + a.appendChild(span.firstChild); + } + span.replaceWith(a); + + // If the computed topic isn't + if (getTopicIdentifier(a) !== topic) { + a.setAttribute('lt', topic); + } + } + + for (const code of document.querySelectorAll('code')) { + // inside or should be left untouched. + if (code.closest('a, dfn')) { + continue; + } + + let dataX; + let skip = false; + + for (const name of code.getAttributeNames()) { + const value = code.getAttribute(name); + switch (name) { + case 'data-x': + // handled below + dataX = value; + break; + case 'class': + // TODO: transform
 etc.
+                case 'id':
+                    // Used to preserve old IDs. TODO: transform to oldids, confirming that
+                    // it actually works: https://github.com/speced/bikeshed/issues/2033
+                case 'subdfn':
+                    // TODO: transform to a regular ?
+                    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L86
+                case 'undefined':
+                    // TODO: used in Wattsi to allow use of undefined terms?
+                    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L87C4-L87C23
+                    skip = true;
+                    break;
+                default:
+                    console.warn('Unhandled  attribute:', name);
+                    skip = true;
+            }
+        }
+
+        if (skip || dataX === '') {
+            continue;
+        }
+
+        const hasSingleTextChild = isText(code.firstChild) && code.firstChild === code.lastChild;
+        if (false && hasSingleTextChild && !dataX) {
+            // Replace with {{foo}} autolink syntax.
+            const text = code.firstChild.nodeValue;
+            code.replaceWith(`{{${text}}}`);
+        } else {
+            // TODO: Transform to {{Foo/bar()}} where possible, and fall
+            // back to . This is just the fallback:
+            const a = document.createElement('a');
+            if (dataX) {
+                a.setAttribute('lt', dataX);
+                code.removeAttribute('data-x');
+            }
+            code.replaceWith(a);
+            a.appendChild(code);
+        }
+    }
 
     for (const elem of document.querySelectorAll('[data-x]')) {
-        const value = elem.getAttribute('data-x');
-        if (value) {
-            if (elem.hasAttribute('lt')) {
-                console.warn('Overwriting existing lt attribute:', elem.outerHTML);
+        const dataX = elem.getAttribute('data-x');
+        if (dataX) {
+            if (elem.parentNode.localName == 'dfn') {
+                // console.warn(elem.parentNode.outerHTML);
             }
-            elem.setAttribute('lt', value);
+            elem.setAttribute('lt', dataX);
         } else {
-            // TODO: what is an empty data-x attribute for?
-            // console.warn('Empty data-x attribute:', elem.outerHTML);
+            // An empty data-x attribute is for when  or  shouldn't
+            // link to anything, or a bare  in the output.
+            switch (elem.localName) {
+                case 'code':
+                case 'span':
+                    // Bikeshed will not change bare  or .
+                    break;
+                case 'dfn':
+                    // TODO: to make Bikeshed output a bare 
+                    break;
+                default:
+                    console.warn('Empty data-x attribute:', elem.outerHTML);
+            }
         }
         elem.removeAttribute('data-x');
     }

From b03fbd6491d4ea0dbe34ea0453d8d4cc92a737f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Mon, 10 Mar 2025 18:22:04 +0100
Subject: [PATCH 04/21] Improve cross-linking further (still broken)

---
 wattsi2bikeshed.js | 131 +++++++++++++++++++++------------------------
 1 file changed, 60 insertions(+), 71 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 937cf9d..3d50be3 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -17,17 +17,12 @@ Include MDN Panels: false
 
 const kCrossRefAttribute = 'data-x';
 
-// Hoist data-x attributes to  or , to match how Wattsi uses the
-// data-x attribute of a single child element when present:
-// https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L888
-function hoistDataX(from, to) {
-    const value = from.getAttribute(kCrossRefAttribute);
-    if (from.parentNode === to && to.firstChild === to.lastChild) {
-        to.setAttribute(kCrossRefAttribute, value);
-    } else if (value) {
-        // console.warn('Ineffectual data-x in source:', to.outerHTML);
+// Remove data-x attributes recursively.
+function removeDataX(elem) {
+    elem.removeAttribute(kCrossRefAttribute);
+    for (const descendent of elem.querySelectorAll('[data-x]')) {
+        descendent.removeAttribute(kCrossRefAttribute);
     }
-    from.removeAttribute(kCrossRefAttribute);
 }
 
 function isElement(node) {
@@ -86,16 +81,10 @@ function convert(infile, outfile) {
             console.warn('Duplicate  topic:', topic);
         }
         crossRefs.set(topic, dfn);
-        // for (const elem of dfn.querySelectorAll('[data-x]')) {
-        //     hoistDataX(elem, dfn);
-        // }
 
-        // Remove all data-x attributes. If this changes the topic, then
+        // Remove data-x attributes. If this changes the topic, then
         // it came from data-x and is copied over to lt for Bikeshed.
-        dfn.removeAttribute(kCrossRefAttribute);
-        for (const elem of dfn.querySelectorAll('[data-x]')) {
-            elem.removeAttribute(kCrossRefAttribute);
-        }
+        removeDataX(dfn);
         if (getTopicIdentifier(dfn) !== topic) {
             dfn.setAttribute('lt', topic);
         }
@@ -118,12 +107,6 @@ function convert(infile, outfile) {
             continue;
         }
 
-        if (span.hasAttribute('subdfn')) {
-            // TODO: transform to a regular ?
-            // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L86
-            continue;
-        }
-
         // Empty data-x="" means it's not a link.
         if (span.getAttribute(kCrossRefAttribute) === '') {
             continue;
@@ -135,10 +118,6 @@ function convert(infile, outfile) {
             continue;
         }
 
-        // for (const elem of span.querySelectorAll('[data-x]')) {
-        //     hoistDataX(elem, span);
-        // }
-
         const topic = getTopicIdentifier(span);
         const dfn = crossRefs.get(topic);
         if (!dfn) {
@@ -147,11 +126,18 @@ function convert(infile, outfile) {
             continue;
         }
 
+        if (span.hasAttribute('subdfn')) {
+            // TODO: generate an ID based on the linked term, like Wattsi:
+            // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L943-L961
+            span.removeAttribute('subdfn');
+        }
+
         // For foo and "SyntaxError",
         // drop the outer  and depend on the  linking logic. Note that this
         // excludes the surrounding quotes from the link text, which is a minor change.
         // The  element is further transformed in a following step.
         function isQuote(node) {
+            return false; // <- hack to disable unwrapping
             return isText(node) && node.data === '"';
         }
         const code = span.querySelector('code');
@@ -173,16 +159,13 @@ function convert(infile, outfile) {
             continue;
         }
 
+        // Remove data-x attributes. This might change the topic.
+        removeDataX(span);
+        const needLt = getTopicIdentifier(span) !== topic;
+
         // Output a  instead of .
         const a = document.createElement('a');
 
-        // Remove all data-x attributes. This might change the computed topic.
-        span.removeAttribute(kCrossRefAttribute); // not actually needed
-        for (const elem of span.querySelectorAll('[data-x]')) {
-            elem.removeAttribute(kCrossRefAttribute);
-        }
-
-
         for (const name of span.getAttributeNames()) {
             const value = span.getAttribute(name);
             switch (name) {
@@ -200,63 +183,69 @@ function convert(infile, outfile) {
         }
         span.replaceWith(a);
 
-        // If the computed topic isn't
-        if (getTopicIdentifier(a) !== topic) {
+        if (needLt) {
             a.setAttribute('lt', topic);
         }
     }
 
+    // Link  to the right thing.
     for (const code of document.querySelectorAll('code')) {
-        //  inside  or  should be left untouched.
-        if (code.closest('a, dfn')) {
+        //  shouldn't be linked.
+        if (code.hasAttribute('undefined')) {
+            code.removeAttribute('undefined');
             continue;
         }
 
-        let dataX;
-        let skip = false;
+        if (code.parentNode.localName == 'pre') {
+            // TODO: unwrap
+            continue;
+        }
 
-        for (const name of code.getAttributeNames()) {
-            const value = code.getAttribute(name);
-            switch (name) {
-                case 'data-x':
-                    // handled below
-                    dataX = value;
-                    break;
-                case 'class':
-                    // TODO: transform 
 etc.
-                case 'id':
-                    // Used to preserve old IDs. TODO: transform to oldids, confirming that
-                    // it actually works: https://github.com/speced/bikeshed/issues/2033
-                case 'subdfn':
-                    // TODO: transform to a regular ?
-                    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L86
-                case 'undefined':
-                    // TODO: used in Wattsi to allow use of undefined terms?
-                    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L87C4-L87C23
-                    skip = true;
-                    break;
-                default:
-                    console.warn('Unhandled  attribute:', name);
-                    skip = true;
-            }
+        //  inside  or  should be left untouched.
+        if (code.closest('a, dfn')) {
+            continue;
         }
 
-        if (skip || dataX === '') {
+        const topic = getTopicIdentifier(code);
+        if (topic === '') {
+            continue;
+        }
+        // if (code.textContent == 'video/mpeg' && code.parentNode.localName == 'p') {
+        //     console.log(code.outerHTML);
+        //     throw 'found it ' + topic;
+        // }
+        const dfn = crossRefs.get(topic);
+        if (!dfn) {
+            console.log(code.parentNode.outerHTML)
+            console.log('topic', topic)
             continue;
         }
 
+        if (code.hasAttribute('subdfn')) {
+            // TODO: generate an ID based on the linked term, like Wattsi:
+            // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L943-L961
+            code.removeAttribute('subdfn');
+        }
+
+        // Remove data-x attributes. This might change the topic.
+        removeDataX(code);
+        const needLt = getTopicIdentifier(code) !== topic;
+
         const hasSingleTextChild = isText(code.firstChild) && code.firstChild === code.lastChild;
-        if (false && hasSingleTextChild && !dataX) {
+        if (false && hasSingleTextChild && !code.hasAttributes() && !needLt) {
             // Replace with {{foo}} autolink syntax.
-            const text = code.firstChild.nodeValue;
+            const text = code.firstChild.data;
             code.replaceWith(`{{${text}}}`);
         } else {
             // TODO: Transform to {{Foo/bar()}} where possible, and fall
             // back to . This is just the fallback:
             const a = document.createElement('a');
-            if (dataX) {
-                a.setAttribute('lt', dataX);
-                code.removeAttribute('data-x');
+            if (needLt) {
+                a.setAttribute('lt', topic);
+            }
+            for (const name of code.getAttributeNames()) {
+                a.setAttribute(name, code.getAttribute(name));
+                code.removeAttribute(name);
             }
             code.replaceWith(a);
             a.appendChild(code);

From c49c398c216d8f35fec24afea368491d47a4fb00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Mon, 10 Mar 2025 20:31:53 +0100
Subject: [PATCH 05/21] Unwrap 
 to just 

---
 wattsi2bikeshed.js | 54 +++++++++++++++++++++++++++++++++++-----------
 1 file changed, 41 insertions(+), 13 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 3d50be3..fd94ac2 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -25,6 +25,13 @@ function removeDataX(elem) {
     }
 }
 
+function replaceWithChildren(elem) {
+    while (elem.firstChild) {
+        elem.parentNode.insertBefore(elem.firstChild, elem);
+    }
+    elem.remove();
+}
+
 function isElement(node) {
     return node?.nodeType === 1;
 }
@@ -151,11 +158,7 @@ function convert(infile, outfile) {
             if (span.hasAttributes()) {
                 console.warn('Discarding  attributes:', span.outerHTML);
             }
-            // Move children to replace span.
-            while (span.firstChild) {
-                span.parentNode.insertBefore(span.firstChild, span);
-            }
-            span.remove();
+            replaceWithChildren(span);
             continue;
         }
 
@@ -177,7 +180,7 @@ function convert(infile, outfile) {
                     console.warn('Unhandled  attribute:', name);
             }
         }
-        // Move the  children over to replace itself.
+        // Move the  children over to .
         while (span.firstChild) {
             a.appendChild(span.firstChild);
         }
@@ -188,6 +191,36 @@ function convert(infile, outfile) {
         }
     }
 
+    for (const code of document.querySelectorAll('pre > code')) {
+        const pre = code.parentNode;
+        if (code.hasAttribute('class')) {
+            switch (code.className) {
+                case 'idl':
+                    pre.className = 'idl';
+                    break;
+                case 'js':
+                    pre.className = 'lang-javascript';
+                    break;
+                case 'abnf':
+                case 'css':
+                case 'html':
+                    case 'json':
+                    pre.className = `lang-${code.className}`;
+                    break;
+                default:
+                    console.warn('Unhandled 
 class:', code.className);
+            }
+            code.removeAttribute('class');
+        }
+        if (code.getAttribute(kCrossRefAttribute) === '') {
+            code.removeAttribute(kCrossRefAttribute);
+        }
+        if (code.hasAttributes()) {
+            console.warn('Discarding  attributes:', code.outerHTML);
+        }
+        replaceWithChildren(code);
+    }
+
     // Link  to the right thing.
     for (const code of document.querySelectorAll('code')) {
         //  shouldn't be linked.
@@ -196,11 +229,6 @@ function convert(infile, outfile) {
             continue;
         }
 
-        if (code.parentNode.localName == 'pre') {
-            // TODO: unwrap
-            continue;
-        }
-
         //  inside  or  should be left untouched.
         if (code.closest('a, dfn')) {
             continue;
@@ -253,7 +281,7 @@ function convert(infile, outfile) {
     }
 
     for (const elem of document.querySelectorAll('[data-x]')) {
-        const dataX = elem.getAttribute('data-x');
+        const dataX = elem.getAttribute(kCrossRefAttribute);
         if (dataX) {
             if (elem.parentNode.localName == 'dfn') {
                 // console.warn(elem.parentNode.outerHTML);
@@ -274,7 +302,7 @@ function convert(infile, outfile) {
                     console.warn('Empty data-x attribute:', elem.outerHTML);
             }
         }
-        elem.removeAttribute('data-x');
+        elem.removeAttribute(kCrossRefAttribute);
     }
 
     for (const elem of document.querySelectorAll('[data-x-href]')) {

From 9660bdc5acbc81e39cce1d9b4b77dc91974d7d6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Wed, 12 Mar 2025 11:17:32 +0100
Subject: [PATCH 06/21] Move  simplification to a final pass

---
 wattsi2bikeshed.js | 53 +++++++++++++++++++++++++---------------------
 1 file changed, 29 insertions(+), 24 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index fd94ac2..54a305c 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -40,6 +40,11 @@ function isText(node) {
     return node?.nodeType === 3;
 }
 
+const markup = /[\[\]{}<>&]/g;
+function hasMarkup(text) {
+    return markup.test(text);
+}
+
 // Get the "topic identifier" for cross-references like Wattsi:
 // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L882-L894
 function getTopicIdentifier(elem) {
@@ -238,14 +243,10 @@ function convert(infile, outfile) {
         if (topic === '') {
             continue;
         }
-        // if (code.textContent == 'video/mpeg' && code.parentNode.localName == 'p') {
-        //     console.log(code.outerHTML);
-        //     throw 'found it ' + topic;
-        // }
+
         const dfn = crossRefs.get(topic);
         if (!dfn) {
-            console.log(code.parentNode.outerHTML)
-            console.log('topic', topic)
+            console.warn('No  found for topic:', topic);
             continue;
         }
 
@@ -259,25 +260,16 @@ function convert(infile, outfile) {
         removeDataX(code);
         const needLt = getTopicIdentifier(code) !== topic;
 
-        const hasSingleTextChild = isText(code.firstChild) && code.firstChild === code.lastChild;
-        if (false && hasSingleTextChild && !code.hasAttributes() && !needLt) {
-            // Replace with {{foo}} autolink syntax.
-            const text = code.firstChild.data;
-            code.replaceWith(`{{${text}}}`);
-        } else {
-            // TODO: Transform to {{Foo/bar()}} where possible, and fall
-            // back to . This is just the fallback:
-            const a = document.createElement('a');
-            if (needLt) {
-                a.setAttribute('lt', topic);
-            }
-            for (const name of code.getAttributeNames()) {
-                a.setAttribute(name, code.getAttribute(name));
-                code.removeAttribute(name);
-            }
-            code.replaceWith(a);
-            a.appendChild(code);
+        const a = document.createElement('a');
+        if (needLt) {
+            a.setAttribute('lt', topic);
         }
+        for (const name of code.getAttributeNames()) {
+            a.setAttribute(name, code.getAttribute(name));
+            code.removeAttribute(name);
+        }
+        code.replaceWith(a);
+        a.appendChild(code);
     }
 
     for (const elem of document.querySelectorAll('[data-x]')) {
@@ -310,6 +302,19 @@ function convert(infile, outfile) {
         elem.removeAttribute('data-x-href');
     }
 
+    // Simplify  to Bikeshed autolinks.
+    for (const a of document.querySelectorAll('a')) {
+        break;
+        const hasSingleTextNode = isText(a.firstChild) && a.firstChild === a.lastChild;
+        if (hasSingleTextNode && !a.hasAttributes()) {
+            const text = a.firstChild.data;
+            if (!hasMarkup(text)) {
+                a.replaceWith(`[=${text}=]`);
+            }
+        }
+        // TODO: handle .
+    }
+
     for (const elem of document.querySelectorAll('*')) {
         for (const attrName of elem.getAttributeNames()) {
             if (!attrName.startsWith('data-')) {

From 2e7888da8d47cef93c9114a5fd948430e2f0db47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Wed, 12 Mar 2025 17:09:26 +0100
Subject: [PATCH 07/21] Preserve Wattsi IDs

---
 wattsi2bikeshed.js | 46 ++++++++++++++++++++++++++++++++--------------
 1 file changed, 32 insertions(+), 14 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 54a305c..ae37952 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -45,16 +45,21 @@ function hasMarkup(text) {
     return markup.test(text);
 }
 
-// Get the "topic identifier" for cross-references like Wattsi:
+// Get the "topic" for cross-references like Wattsi:
 // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L882-L894
-function getTopicIdentifier(elem) {
+function getTopic(elem) {
     let result;
-    if (elem.hasAttribute(kCrossRefAttribute)) {
-        result = elem.getAttribute(kCrossRefAttribute);
-    } else if (isElement(elem.firstChild) && elem.firstChild === elem.lastChild) {
-        result = getTopicIdentifier(elem.firstChild);
-    } else {
-        result = elem.textContent;
+    while (true) {
+        if (elem.hasAttribute(kCrossRefAttribute)) {
+            result = elem.getAttribute(kCrossRefAttribute);
+            break;
+        } else if (isElement(elem.firstChild) && elem.firstChild === elem.lastChild) {
+            elem = elem.firstChild;
+            continue;
+        } else {
+            result = elem.textContent;
+            break;
+        }
     }
     // This matches Wattsi's MungeStringToTopic in spirit,
     // but perhaps not in every detail:
@@ -65,6 +70,15 @@ function getTopicIdentifier(elem) {
         .trim();
 }
 
+// Convert a topic to an ID like Wattsi:
+// https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L786-L832
+function getId(topic) {
+    // Note: no toLowerCase() because this is already done in getTopic().
+    return topic
+        .replaceAll(/["?`]/g, '')
+        .replaceAll(/[\s<>\[\\\]^{|}%]+/g, '-');
+}
+
 function convert(infile, outfile) {
     const source = readFileSync(infile, 'utf-8');
     const dom = new JSDOM(source);
@@ -88,7 +102,7 @@ function convert(infile, outfile) {
         if (dfn.getAttribute(kCrossRefAttribute) === '') {
             continue;
         }
-        const topic = getTopicIdentifier(dfn);
+        const topic = getTopic(dfn);
         if (crossRefs.has(topic)) {
             console.warn('Duplicate  topic:', topic);
         }
@@ -97,9 +111,13 @@ function convert(infile, outfile) {
         // Remove data-x attributes. If this changes the topic, then
         // it came from data-x and is copied over to lt for Bikeshed.
         removeDataX(dfn);
-        if (getTopicIdentifier(dfn) !== topic) {
+        if (getTopic(dfn) !== topic) {
             dfn.setAttribute('lt', topic);
         }
+
+        if (!dfn.hasAttribute('id')) {
+            dfn.setAttribute('id', getId(topic));
+        }
     }
 
     // Replace  with the inner  or a new .
@@ -130,7 +148,7 @@ function convert(infile, outfile) {
             continue;
         }
 
-        const topic = getTopicIdentifier(span);
+        const topic = getTopic(span);
         const dfn = crossRefs.get(topic);
         if (!dfn) {
             // TODO: vet these cases for any that should actually be linked
@@ -169,7 +187,7 @@ function convert(infile, outfile) {
 
         // Remove data-x attributes. This might change the topic.
         removeDataX(span);
-        const needLt = getTopicIdentifier(span) !== topic;
+        const needLt = getTopic(span) !== topic;
 
         // Output a  instead of .
         const a = document.createElement('a');
@@ -239,7 +257,7 @@ function convert(infile, outfile) {
             continue;
         }
 
-        const topic = getTopicIdentifier(code);
+        const topic = getTopic(code);
         if (topic === '') {
             continue;
         }
@@ -258,7 +276,7 @@ function convert(infile, outfile) {
 
         // Remove data-x attributes. This might change the topic.
         removeDataX(code);
-        const needLt = getTopicIdentifier(code) !== topic;
+        const needLt = getTopic(code) !== topic;
 
         const a = document.createElement('a');
         if (needLt) {

From 78a15b203834099d6a2e5ab87fbf7544be2b8ad5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Thu, 13 Mar 2025 11:43:13 +0100
Subject: [PATCH 08/21] Don't use data-x to populate lt, just remove them

---
 wattsi2bikeshed.js | 53 ++--------------------------------------------
 1 file changed, 2 insertions(+), 51 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index ae37952..be30018 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -17,14 +17,6 @@ Include MDN Panels: false
 
 const kCrossRefAttribute = 'data-x';
 
-// Remove data-x attributes recursively.
-function removeDataX(elem) {
-    elem.removeAttribute(kCrossRefAttribute);
-    for (const descendent of elem.querySelectorAll('[data-x]')) {
-        descendent.removeAttribute(kCrossRefAttribute);
-    }
-}
-
 function replaceWithChildren(elem) {
     while (elem.firstChild) {
         elem.parentNode.insertBefore(elem.firstChild, elem);
@@ -108,13 +100,6 @@ function convert(infile, outfile) {
         }
         crossRefs.set(topic, dfn);
 
-        // Remove data-x attributes. If this changes the topic, then
-        // it came from data-x and is copied over to lt for Bikeshed.
-        removeDataX(dfn);
-        if (getTopic(dfn) !== topic) {
-            dfn.setAttribute('lt', topic);
-        }
-
         if (!dfn.hasAttribute('id')) {
             dfn.setAttribute('id', getId(topic));
         }
@@ -185,16 +170,14 @@ function convert(infile, outfile) {
             continue;
         }
 
-        // Remove data-x attributes. This might change the topic.
-        removeDataX(span);
-        const needLt = getTopic(span) !== topic;
-
         // Output a  instead of .
         const a = document.createElement('a');
 
         for (const name of span.getAttributeNames()) {
             const value = span.getAttribute(name);
             switch (name) {
+                case 'data-x':
+                    break;
                 case 'id':
                     // Copy over.
                     a.setAttribute(name, value);
@@ -208,10 +191,6 @@ function convert(infile, outfile) {
             a.appendChild(span.firstChild);
         }
         span.replaceWith(a);
-
-        if (needLt) {
-            a.setAttribute('lt', topic);
-        }
     }
 
     for (const code of document.querySelectorAll('pre > code')) {
@@ -274,14 +253,7 @@ function convert(infile, outfile) {
             code.removeAttribute('subdfn');
         }
 
-        // Remove data-x attributes. This might change the topic.
-        removeDataX(code);
-        const needLt = getTopic(code) !== topic;
-
         const a = document.createElement('a');
-        if (needLt) {
-            a.setAttribute('lt', topic);
-        }
         for (const name of code.getAttributeNames()) {
             a.setAttribute(name, code.getAttribute(name));
             code.removeAttribute(name);
@@ -291,27 +263,6 @@ function convert(infile, outfile) {
     }
 
     for (const elem of document.querySelectorAll('[data-x]')) {
-        const dataX = elem.getAttribute(kCrossRefAttribute);
-        if (dataX) {
-            if (elem.parentNode.localName == 'dfn') {
-                // console.warn(elem.parentNode.outerHTML);
-            }
-            elem.setAttribute('lt', dataX);
-        } else {
-            // An empty data-x attribute is for when  or  shouldn't
-            // link to anything, or a bare  in the output.
-            switch (elem.localName) {
-                case 'code':
-                case 'span':
-                    // Bikeshed will not change bare  or .
-                    break;
-                case 'dfn':
-                    // TODO: to make Bikeshed output a bare 
-                    break;
-                default:
-                    console.warn('Empty data-x attribute:', elem.outerHTML);
-            }
-        }
         elem.removeAttribute(kCrossRefAttribute);
     }
 

From 19fc2838ed35848ef5fdbf8bbe123f9eb81a99d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Thu, 13 Mar 2025 11:44:49 +0100
Subject: [PATCH 09/21] Remove "new" from the linking text of constructors

---
 wattsi2bikeshed.js | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index be30018..d3571e1 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -71,6 +71,29 @@ function getId(topic) {
         .replaceAll(/[\s<>\[\\\]^{|}%]+/g, '-');
 }
 
+// Get the linking text like Bikeshed:
+// https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L174-L201
+function getBikeshedLinkText(elem) {
+    // Note: ignoring data-lt="" and just looking at text content.
+    let text;
+    switch (elem.localName) {
+        case 'dfn':
+        case 'a':
+            text = elem.textContent;
+            break;
+        case 'h2':
+        case 'h3':
+        case 'h4':
+        case 'h5':
+        case 'h6':
+            text = (elem.querySelector('.content') ?? elem).textContent;
+            break;
+        default:
+            return null;
+    }
+    return text.trim().replaceAll(/\s+/g, ' ');
+}
+
 function convert(infile, outfile) {
     const source = readFileSync(infile, 'utf-8');
     const dom = new JSDOM(source);
@@ -103,6 +126,14 @@ function convert(infile, outfile) {
         if (!dfn.hasAttribute('id')) {
             dfn.setAttribute('id', getId(topic));
         }
+
+        // Remove "new" from the linking text of constructors.
+        if (dfn.hasAttribute('constructor')) {
+            const lt = getBikeshedLinkText(dfn);
+            if (lt.startsWith('new ')) {
+                dfn.setAttribute('lt', lt.substring(4));
+            }
+        }
     }
 
     // Replace  with the inner  or a new .

From b1400199db99796cb3b07f2518d75fb645114f19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Thu, 13 Mar 2025 14:39:05 +0100
Subject: [PATCH 10/21] Rewrite data-lt to lt

---
 wattsi2bikeshed.js | 24 ++++++------------------
 1 file changed, 6 insertions(+), 18 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index d3571e1..593821e 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -293,6 +293,12 @@ function convert(infile, outfile) {
         a.appendChild(code);
     }
 
+    // Rewrite data-lt to lt.
+    for (const elem of document.querySelectorAll('[data-lt]')) {
+        elem.setAttribute('lt', elem.getAttribute('data-lt'));
+        elem.removeAttribute('data-lt');
+    }
+
     for (const elem of document.querySelectorAll('[data-x]')) {
         elem.removeAttribute(kCrossRefAttribute);
     }
@@ -315,24 +321,6 @@ function convert(infile, outfile) {
         // TODO: handle .
     }
 
-    for (const elem of document.querySelectorAll('*')) {
-        for (const attrName of elem.getAttributeNames()) {
-            if (!attrName.startsWith('data-')) {
-                continue;
-            }
-            switch (attrName) {
-                case 'data-lt':
-                    // TODO, handle these somehow
-                    break;
-                case 'data-noexport':
-                    // Leave alone, see comment in source.
-                    break;
-                default:
-                    console.warn('Unhandled data attribute:', elem.outerHTML);
-            }
-        }
-    }
-
     const output = document.body.innerHTML
         .replaceAll('[[', '\\[[');
 

From 017385d6de7c56f0e39a53ad522ebbaef023d028 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Thu, 13 Mar 2025 15:06:46 +0100
Subject: [PATCH 11/21] Handle data-lt in more places

---
 wattsi2bikeshed.js | 53 +++++++++++++++++++++++++++-------------------
 1 file changed, 31 insertions(+), 22 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 593821e..c692530 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -73,25 +73,33 @@ function getId(topic) {
 
 // Get the linking text like Bikeshed:
 // https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L174-L201
-function getBikeshedLinkText(elem) {
-    // Note: ignoring data-lt="" and just looking at text content.
-    let text;
-    switch (elem.localName) {
-        case 'dfn':
-        case 'a':
-            text = elem.textContent;
-            break;
-        case 'h2':
-        case 'h3':
-        case 'h4':
-        case 'h5':
-        case 'h6':
-            text = (elem.querySelector('.content') ?? elem).textContent;
-            break;
-        default:
-            return null;
+function getBikeshedLinkTexts(elem) {
+    const dataLt = elem.getAttribute('data-lt');
+    if (dataLt === '') {
+        return [];
+    }
+
+    let texts = [];
+    if (dataLt) {
+        // TODO: what's the `rawText in ["|", "||", "|||"]` condition for?
+        texts = dataLt.split('|');
+    } else {
+        switch (elem.localName) {
+            case 'dfn':
+            case 'a':
+                texts = [elem.textContent];
+                break;
+            case 'h2':
+            case 'h3':
+            case 'h4':
+            case 'h5':
+            case 'h6':
+                texts = [(elem.querySelector('.content') ?? elem).textContent];
+                break;
+        }
     }
-    return text.trim().replaceAll(/\s+/g, ' ');
+
+    return texts.map((x) => x.trim().replaceAll(/\s+/g, ' '));
 }
 
 function convert(infile, outfile) {
@@ -128,10 +136,10 @@ function convert(infile, outfile) {
         }
 
         // Remove "new" from the linking text of constructors.
-        if (dfn.hasAttribute('constructor')) {
-            const lt = getBikeshedLinkText(dfn);
-            if (lt.startsWith('new ')) {
-                dfn.setAttribute('lt', lt.substring(4));
+        if (dfn.hasAttribute('constructor') && !dfn.hasAttribute('data-lt')) {
+            const lt = getBikeshedLinkTexts(dfn)[0];
+            if (lt?.startsWith('new ')) {
+                dfn.setAttribute('data-lt', lt.substring(4));
             }
         }
     }
@@ -208,6 +216,7 @@ function convert(infile, outfile) {
             const value = span.getAttribute(name);
             switch (name) {
                 case 'data-x':
+                case 'data-lt':
                     break;
                 case 'id':
                     // Copy over.

From 0e8e03e93933dc0e223de63210a98676eea87104 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Thu, 13 Mar 2025 16:54:03 +0100
Subject: [PATCH 12/21] Handle  links

---
 wattsi2bikeshed.js | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index c692530..77cd000 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -145,6 +145,8 @@ function convert(infile, outfile) {
     }
 
     // Replace  with the inner  or a new .
+    // TODO: align more closely with Wattsi:
+    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L1454-L1487
     const spans = document.querySelectorAll('span');
     for (const [i, span] of Object.entries(spans)) {
         // Don't touch any span with a descendent span.
@@ -231,6 +233,30 @@ function convert(infile, outfile) {
             a.appendChild(span.firstChild);
         }
         span.replaceWith(a);
+
+        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
+    }
+
+    // Wrap  with . Wattsi handling is here:
+    // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L1454-L1487
+    for (const i of document.querySelectorAll('i[data-x]')) {
+        if (i.closest('dfn')) {
+            continue;
+        }
+
+        const topic = getTopic(i);
+        const dfn = crossRefs.get(topic);
+        if (!dfn) {
+            continue;
+            // TODO: vet these cases for any that should actually be linked
+            // console.log(i.outerHTML);
+        }
+
+        const a = document.createElement('a');
+        i.parentNode.insertBefore(a, i);
+        a.appendChild(i);
+
+        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
     }
 
     for (const code of document.querySelectorAll('pre > code')) {
@@ -300,6 +326,8 @@ function convert(infile, outfile) {
         }
         code.replaceWith(a);
         a.appendChild(code);
+
+        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
     }
 
     // Rewrite data-lt to lt.

From 03bfae7d7f3183c476b2892133a0be4625fea190 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Fri, 14 Mar 2025 12:33:34 +0100
Subject: [PATCH 13/21] Make Bikeshed find the right  more often

---
 wattsi2bikeshed.js | 35 +++++++++++++++++++++++++++++++----
 1 file changed, 31 insertions(+), 4 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 77cd000..753bbf7 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -102,6 +102,33 @@ function getBikeshedLinkTexts(elem) {
     return texts.map((x) => x.trim().replaceAll(/\s+/g, ' '));
 }
 
+// Add for and lt to ensure that Bikeshed will link the  to the right .
+function ensureLink(a, dfn) {
+    if (dfn.hasAttribute('for')) {
+        a.setAttribute('for', dfn.getAttribute('for'));
+        // TODO: don't add when it's already unambiguous.
+    }
+
+    const dfnLts = getBikeshedLinkTexts(dfn);
+    if (dfnLts.length === 0) {
+        console.warn('No linking text for', dfn.outerHTML);
+        return;
+    }
+    const aLts = getBikeshedLinkTexts(a);
+    if (aLts.length !== 1) {
+        console.warn('Zero or too many linking texts for', a.outerHTML);
+    }
+    if (!dfnLts.some((lt) => lt === aLts[0])) {
+        // console.log('Fixing link from', a.outerHTML, 'to', dfn.outerHTML, 'with lt');
+        // Note: data-lt is rewritten to lt later. It would also work to remove
+        // any data-lt attribute here and just add lt.
+        a.setAttribute('data-lt', dfnLts[0]);
+    }
+
+    // TODO: check if Bikeshed would now find the right  and if not
+    // add additional attributes to make it so.
+}
+
 function convert(infile, outfile) {
     const source = readFileSync(infile, 'utf-8');
     const dom = new JSDOM(source);
@@ -218,8 +245,8 @@ function convert(infile, outfile) {
             const value = span.getAttribute(name);
             switch (name) {
                 case 'data-x':
-                case 'data-lt':
                     break;
+                case 'data-lt':
                 case 'id':
                     // Copy over.
                     a.setAttribute(name, value);
@@ -234,7 +261,7 @@ function convert(infile, outfile) {
         }
         span.replaceWith(a);
 
-        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
+        ensureLink(a, dfn);
     }
 
     // Wrap  with . Wattsi handling is here:
@@ -256,7 +283,7 @@ function convert(infile, outfile) {
         i.parentNode.insertBefore(a, i);
         a.appendChild(i);
 
-        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
+        ensureLink(a, dfn);
     }
 
     for (const code of document.querySelectorAll('pre > code')) {
@@ -327,7 +354,7 @@ function convert(infile, outfile) {
         code.replaceWith(a);
         a.appendChild(code);
 
-        // TODO: ensure that Bikeshed will identify the same  as Wattsi.
+        ensureLink(a, dfn);
     }
 
     // Rewrite data-lt to lt.

From 330630b072ad26ee21342492007ae456346eac41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Fri, 14 Mar 2025 13:20:19 +0100
Subject: [PATCH 14/21] Use noexport="" to silence some Bikeshed warnings

---
 wattsi2bikeshed.js | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 753bbf7..88356e0 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -171,6 +171,9 @@ function convert(infile, outfile) {
         }
     }
 
+    // Track used s in order to identify the unused ones.
+    const usedDfns = new Set();
+
     // Replace  with the inner  or a new .
     // TODO: align more closely with Wattsi:
     // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L1454-L1487
@@ -262,6 +265,7 @@ function convert(infile, outfile) {
         span.replaceWith(a);
 
         ensureLink(a, dfn);
+        usedDfns.add(dfn);
     }
 
     // Wrap  with . Wattsi handling is here:
@@ -284,6 +288,7 @@ function convert(infile, outfile) {
         a.appendChild(i);
 
         ensureLink(a, dfn);
+        usedDfns.add(dfn);
     }
 
     for (const code of document.querySelectorAll('pre > code')) {
@@ -355,6 +360,7 @@ function convert(infile, outfile) {
         a.appendChild(code);
 
         ensureLink(a, dfn);
+        usedDfns.add(dfn);
     }
 
     // Rewrite data-lt to lt.
@@ -372,6 +378,19 @@ function convert(infile, outfile) {
         elem.removeAttribute('data-x-href');
     }
 
+    // Add noexport to unused s to silence Bikeshed warnings about them.
+    // TODO: vet for cases that are accidentally unused.
+    for (const dfn of crossRefs.values()) {
+        if (usedDfns.has(dfn)) {
+            continue;
+        }
+        // This  is unused by Wattsi rules.
+        if (dfn.hasAttribute('data-export') || dfn.hasAttribute('export')) {
+            continue;
+        }
+        dfn.setAttribute('noexport', '');
+    }
+
     // Simplify  to Bikeshed autolinks.
     for (const a of document.querySelectorAll('a')) {
         break;

From 94ade7ed00754d2491e51a0c1fe3c412722eb8db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Fri, 14 Mar 2025 15:36:23 +0100
Subject: [PATCH 15/21] Use data-x attribute as local-lt to make more links
 work

This ends up quite noisy in the output and can probably be minified
somehow. It does link more things together however.
---
 wattsi2bikeshed.js | 140 +++++++++++++++++++++++++++++++++------------
 1 file changed, 105 insertions(+), 35 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 88356e0..e493128 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -39,27 +39,40 @@ function hasMarkup(text) {
 
 // Get the "topic" for cross-references like Wattsi:
 // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L882-L894
-function getTopic(elem) {
+//
+// Also return whether it came from a data-x attribute or text content, so that
+// data-x attribute values can be detected and processed further.
+function getTopicAndSource(elem) {
     let result;
+    let source;
     while (true) {
         if (elem.hasAttribute(kCrossRefAttribute)) {
             result = elem.getAttribute(kCrossRefAttribute);
+            source = kCrossRefAttribute;
             break;
         } else if (isElement(elem.firstChild) && elem.firstChild === elem.lastChild) {
             elem = elem.firstChild;
             continue;
         } else {
             result = elem.textContent;
+            source = 'textContent';
             break;
         }
     }
     // This matches Wattsi's MungeStringToTopic in spirit,
     // but perhaps not in every detail:
-    return result
-        .replaceAll('#', '')
-        .replaceAll(/\s+/g, ' ')
-        .toLowerCase()
-        .trim();
+    return [
+        result
+            .replaceAll('#', '')
+            .replaceAll(/\s+/g, ' ')
+            .toLowerCase()
+            .trim(),
+        source
+    ];
+}
+
+function getTopic(elem) {
+    return getTopicAndSource(elem)[0];
 }
 
 // Convert a topic to an ID like Wattsi:
@@ -73,60 +86,90 @@ function getId(topic) {
 
 // Get the linking text like Bikeshed:
 // https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L174-L201
-function getBikeshedLinkTexts(elem) {
+function getBikeshedLinkTextSet(elem) {
+    const texts = new Set();
+
     const dataLt = elem.getAttribute('data-lt');
     if (dataLt === '') {
-        return [];
+        return texts;
     }
 
-    let texts = [];
+    const add = (x) => texts.add(x.trim().replaceAll(/\s+/g, ' '));
+
     if (dataLt) {
         // TODO: what's the `rawText in ["|", "||", "|||"]` condition for?
-        texts = dataLt.split('|');
+        dataLt.split('|').map(add);
     } else {
         switch (elem.localName) {
             case 'dfn':
             case 'a':
-                texts = [elem.textContent];
+                add(elem.textContent);
                 break;
             case 'h2':
             case 'h3':
             case 'h4':
             case 'h5':
             case 'h6':
-                texts = [(elem.querySelector('.content') ?? elem).textContent];
+                add((elem.querySelector('.content') ?? elem).textContent);
                 break;
         }
     }
 
-    return texts.map((x) => x.trim().replaceAll(/\s+/g, ' '));
+    const dataLocalLt = elem.getAttribute('data-local-lt');
+    if (dataLocalLt) {
+        if (dataLocalLt.includes('|')) {
+            console.warn('Ignoring data-local-lt value containing |:', dataLocalLt);
+        } else {
+            add(dataLocalLt);
+        }
+    }
+
+    return texts;
+}
+
+// Get the *first* linking text like Bikeshed:
+// https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L215-L220
+function getBikeshedLinkText(elem) {
+    for (const text of getBikeshedLinkTextSet(elem)) {
+        return text;
+    }
+    return null;
 }
 
 // Add for and lt to ensure that Bikeshed will link the  to the right .
-function ensureLink(a, dfn) {
+function ensureLink(a, dfn, dfnLtCounts) {
     if (dfn.hasAttribute('for')) {
         a.setAttribute('for', dfn.getAttribute('for'));
         // TODO: don't add when it's already unambiguous.
     }
 
-    const dfnLts = getBikeshedLinkTexts(dfn);
-    if (dfnLts.length === 0) {
+    const dfnLts = getBikeshedLinkTextSet(dfn);
+    if (dfnLts.size === 0) {
         console.warn('No linking text for', dfn.outerHTML);
         return;
     }
-    const aLts = getBikeshedLinkTexts(a);
-    if (aLts.length !== 1) {
-        console.warn('Zero or too many linking texts for', a.outerHTML);
+    const aLt = getBikeshedLinkText(a);
+    if (!aLt) {
+        console.warn('No linking text for', a.outerHTML);
+        return;
+    }
+
+    if (a.hasAttribute('for')) {
+        // TODO: look in dfnLts when that tracks 
+        return;
     }
-    if (!dfnLts.some((lt) => lt === aLts[0])) {
-        // console.log('Fixing link from', a.outerHTML, 'to', dfn.outerHTML, 'with lt');
-        // Note: data-lt is rewritten to lt later. It would also work to remove
-        // any data-lt attribute here and just add lt.
-        a.setAttribute('data-lt', dfnLts[0]);
+
+    for (const lt of dfnLts) {
+        if (dfnLtCounts.get(lt) === 1) {
+            // This is a unique linking text.
+            // Note: data-lt is rewritten to lt later. It would also work to remove
+            // any data-lt attribute here and just add lt.
+            a.setAttribute('data-lt', lt);
+            return;
+        }
     }
 
-    // TODO: check if Bikeshed would now find the right  and if not
-    // add additional attributes to make it so.
+    // TODO: handle cases that end up here.
 }
 
 function convert(infile, outfile) {
@@ -147,28 +190,51 @@ function convert(infile, outfile) {
     // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L735-L759
 
     // Scan all definitions
-    const crossRefs = new Map();
+    const crossRefs = new Map(); // map from Wattsi topic to 
+    const dfnLtCounts = new Map(); // map from Bikeshed link text to number of uses in 
     for (const dfn of document.querySelectorAll('dfn')) {
         if (dfn.getAttribute(kCrossRefAttribute) === '') {
             continue;
         }
-        const topic = getTopic(dfn);
+        const [topic, source] = getTopicAndSource(dfn);
         if (crossRefs.has(topic)) {
             console.warn('Duplicate  topic:', topic);
         }
         crossRefs.set(topic, dfn);
 
         if (!dfn.hasAttribute('id')) {
+            // TODO: avoid if Bikeshed would generate the same ID
             dfn.setAttribute('id', getId(topic));
         }
 
+        const lts = getBikeshedLinkTextSet(dfn);
+
         // Remove "new" from the linking text of constructors.
         if (dfn.hasAttribute('constructor') && !dfn.hasAttribute('data-lt')) {
-            const lt = getBikeshedLinkTexts(dfn)[0];
-            if (lt?.startsWith('new ')) {
-                dfn.setAttribute('data-lt', lt.substring(4));
+            for (const lt of lts) {
+                if (lt.startsWith('new ')) {
+                    dfn.setAttribute('data-lt', lt.substring(4));
+                    break;
+                }
             }
         }
+
+        // Put data-x values into local-lt to enable disambiguating s
+        // with the same linking text, but only if it's not already in lts.
+        if (source === kCrossRefAttribute && !lts.has(topic)) {
+            dfn.setAttribute('data-local-lt', topic);
+            lts.add(topic); // equivalent to calling getBikeshedLinkTextSet(dfn) again
+        }
+
+        // Count uses of each Bikeshed linking text
+        if (dfn.hasAttribute('for')) {
+            // TODO: track  as well
+            continue;
+        }
+        for (const lt of lts) {
+            const count = (dfnLtCounts.get(lt) ?? 0) + 1
+            dfnLtCounts.set(lt, count);
+        }
     }
 
     // Track used s in order to identify the unused ones.
@@ -264,7 +330,7 @@ function convert(infile, outfile) {
         }
         span.replaceWith(a);
 
-        ensureLink(a, dfn);
+        ensureLink(a, dfn, dfnLtCounts);
         usedDfns.add(dfn);
     }
 
@@ -287,7 +353,7 @@ function convert(infile, outfile) {
         i.parentNode.insertBefore(a, i);
         a.appendChild(i);
 
-        ensureLink(a, dfn);
+        ensureLink(a, dfn, dfnLtCounts);
         usedDfns.add(dfn);
     }
 
@@ -359,15 +425,19 @@ function convert(infile, outfile) {
         code.replaceWith(a);
         a.appendChild(code);
 
-        ensureLink(a, dfn);
+        ensureLink(a, dfn, dfnLtCounts);
         usedDfns.add(dfn);
     }
 
-    // Rewrite data-lt to lt.
+    // Rewrite data-lt to lt and data-local-lt to local-lt.
     for (const elem of document.querySelectorAll('[data-lt]')) {
         elem.setAttribute('lt', elem.getAttribute('data-lt'));
         elem.removeAttribute('data-lt');
     }
+    for (const elem of document.querySelectorAll('[data-local-lt]')) {
+        elem.setAttribute('local-lt', elem.getAttribute('data-local-lt'));
+        elem.removeAttribute('data-local-lt');
+    }
 
     for (const elem of document.querySelectorAll('[data-x]')) {
         elem.removeAttribute(kCrossRefAttribute);

From 004e01460465b2a9bdeb084df122b0d746681745 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Mon, 17 Mar 2025 13:40:05 +0100
Subject: [PATCH 16/21] Fix up document.write/writeln cases

---
 wattsi2bikeshed.js | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index e493128..a61f068 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -219,6 +219,17 @@ function convert(infile, outfile) {
             }
         }
 
+        // Remove leading "document." from linking text of document.write/writeln.
+        if (dfn.hasAttribute('method') && dfn.getAttribute('for') === 'Document' &&
+            !dfn.hasAttribute('data-lt')) {
+            for (const lt of lts) {
+                if (lt.startsWith('document.')) {
+                    dfn.setAttribute('data-lt', lt.substring(9));
+                    break;
+                }
+            }
+        }
+
         // Put data-x values into local-lt to enable disambiguating s
         // with the same linking text, but only if it's not already in lts.
         if (source === kCrossRefAttribute && !lts.has(topic)) {

From 15d13274849f5203578f6ed92b0580ef388cacd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Mon, 17 Mar 2025 15:14:12 +0100
Subject: [PATCH 17/21] Handle  better

---
 wattsi2bikeshed.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index a61f068..17e0468 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -193,10 +193,15 @@ function convert(infile, outfile) {
     const crossRefs = new Map(); // map from Wattsi topic to 
     const dfnLtCounts = new Map(); // map from Bikeshed link text to number of uses in 
     for (const dfn of document.querySelectorAll('dfn')) {
-        if (dfn.getAttribute(kCrossRefAttribute) === '') {
+        const [topic, source] = getTopicAndSource(dfn);
+        if (topic === '' && source === kCrossRefAttribute) {
+            // This isn't a linkable definition and Wattsi outputs a plain 
+            // with no attributes. The closest thing in Bikeshed is a definition
+            // with no linking text that is not exported.
+            dfn.setAttribute('data-lt', '');
+            dfn.setAttribute('noexport', '');
             continue;
         }
-        const [topic, source] = getTopicAndSource(dfn);
         if (crossRefs.has(topic)) {
             console.warn('Duplicate  topic:', topic);
         }

From b4254fccde4e5640029b91c4e2e6b475e5c84a82 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Tue, 18 Mar 2025 16:34:58 +0100
Subject: [PATCH 18/21] Lowercase linking texts to detect more clashes

---
 wattsi2bikeshed.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 17e0468..9c0ab46 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -86,6 +86,9 @@ function getId(topic) {
 
 // Get the linking text like Bikeshed:
 // https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L174-L201
+//
+// Also approximate the additional munging Bikeshed does here:
+// https://github.com/speced/bikeshed/blob/9f194ae38e5495487d58b1f1180c29a9fa09ea5d/bikeshed/refs/manager.py#L291-L298
 function getBikeshedLinkTextSet(elem) {
     const texts = new Set();
 
@@ -94,7 +97,16 @@ function getBikeshedLinkTextSet(elem) {
         return texts;
     }
 
-    const add = (x) => texts.add(x.trim().replaceAll(/\s+/g, ' '));
+    function add(lt) {
+        lt = lt.trim().replaceAll(/\s+/g, ' ');
+        // These are the extra bits from addLocalDfns in Bikeshed:
+        lt = lt.replaceAll("’", "'");
+        // TODO: line-ending em dashes or -- (if they exist in HTML)
+        // TODO: only lowercase dfn types that Bikeshed would if lowercasing
+        // everything results in collisions that Bikeshed doesn't have.
+        lt = lt.toLowerCase();
+        texts.add(lt);
+    }
 
     if (dataLt) {
         // TODO: what's the `rawText in ["|", "||", "|||"]` condition for?

From 2dafc459f740b30d357998cec7a1bab00b2e03f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Wed, 19 Mar 2025 09:01:56 +0100
Subject: [PATCH 19/21] Use local-lt="xxx-...." to ensure links

---
 wattsi2bikeshed.js | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 9c0ab46..d0fb537 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -181,7 +181,18 @@ function ensureLink(a, dfn, dfnLtCounts) {
         }
     }
 
-    // TODO: handle cases that end up here.
+    if (!dfn.hasAttribute('data-local-lt')) {
+        if (!dfn.id) {
+            console.warn('No id for dfn', dfn.outerHTML);
+            return;
+        }
+        // Use a prefix to make the linking text unique. The prefix is "xxx-""
+        // because class="XXX" is used as a FIXME/TODO in HTML, and these
+        // local-lt attributes should be removed over time.
+        dfn.setAttribute('data-local-lt', `xxx-${dfn.id}`);
+    }
+
+    a.setAttribute('data-lt', dfn.getAttribute('data-local-lt'));
 }
 
 function convert(infile, outfile) {
@@ -247,13 +258,6 @@ function convert(infile, outfile) {
             }
         }
 
-        // Put data-x values into local-lt to enable disambiguating s
-        // with the same linking text, but only if it's not already in lts.
-        if (source === kCrossRefAttribute && !lts.has(topic)) {
-            dfn.setAttribute('data-local-lt', topic);
-            lts.add(topic); // equivalent to calling getBikeshedLinkTextSet(dfn) again
-        }
-
         // Count uses of each Bikeshed linking text
         if (dfn.hasAttribute('for')) {
             // TODO: track  as well

From d60175e109e1f6a1322daed0e7e4afd5b797fd0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Wed, 19 Mar 2025 09:08:22 +0100
Subject: [PATCH 20/21] Remove getTopicAndSource()

---
 wattsi2bikeshed.js | 29 ++++++++---------------------
 1 file changed, 8 insertions(+), 21 deletions(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index d0fb537..8142e24 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -39,40 +39,27 @@ function hasMarkup(text) {
 
 // Get the "topic" for cross-references like Wattsi:
 // https://github.com/whatwg/wattsi/blob/b9c28036a2a174f7f87315164f001120596a95f1/src/wattsi.pas#L882-L894
-//
-// Also return whether it came from a data-x attribute or text content, so that
-// data-x attribute values can be detected and processed further.
-function getTopicAndSource(elem) {
+function getTopic(elem) {
     let result;
-    let source;
     while (true) {
         if (elem.hasAttribute(kCrossRefAttribute)) {
             result = elem.getAttribute(kCrossRefAttribute);
-            source = kCrossRefAttribute;
             break;
         } else if (isElement(elem.firstChild) && elem.firstChild === elem.lastChild) {
             elem = elem.firstChild;
             continue;
         } else {
             result = elem.textContent;
-            source = 'textContent';
             break;
         }
     }
     // This matches Wattsi's MungeStringToTopic in spirit,
     // but perhaps not in every detail:
-    return [
-        result
-            .replaceAll('#', '')
-            .replaceAll(/\s+/g, ' ')
-            .toLowerCase()
-            .trim(),
-        source
-    ];
-}
-
-function getTopic(elem) {
-    return getTopicAndSource(elem)[0];
+    return result
+        .replaceAll('#', '')
+        .replaceAll(/\s+/g, ' ')
+        .toLowerCase()
+        .trim();
 }
 
 // Convert a topic to an ID like Wattsi:
@@ -216,8 +203,8 @@ function convert(infile, outfile) {
     const crossRefs = new Map(); // map from Wattsi topic to 
     const dfnLtCounts = new Map(); // map from Bikeshed link text to number of uses in 
     for (const dfn of document.querySelectorAll('dfn')) {
-        const [topic, source] = getTopicAndSource(dfn);
-        if (topic === '' && source === kCrossRefAttribute) {
+        const topic = getTopic(dfn);
+        if (topic === '') {
             // This isn't a linkable definition and Wattsi outputs a plain 
             // with no attributes. The closest thing in Bikeshed is a definition
             // with no linking text that is not exported.

From a266ce4294f33b72ca6b9c692cd84fb1d4c9e92e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= 
Date: Wed, 19 Mar 2025 22:37:48 +0100
Subject: [PATCH 21/21] Update permalink

---
 wattsi2bikeshed.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/wattsi2bikeshed.js b/wattsi2bikeshed.js
index 8142e24..828b05d 100644
--- a/wattsi2bikeshed.js
+++ b/wattsi2bikeshed.js
@@ -75,7 +75,7 @@ function getId(topic) {
 // https://github.com/speced/bikeshed/blob/50d0ec772915adcd5cec0c2989a27fa761d70e71/bikeshed/h/dom.py#L174-L201
 //
 // Also approximate the additional munging Bikeshed does here:
-// https://github.com/speced/bikeshed/blob/9f194ae38e5495487d58b1f1180c29a9fa09ea5d/bikeshed/refs/manager.py#L291-L298
+// https://github.com/speced/bikeshed/blob/f3fd50cc3a67ecbffb562b16252237aeaa2b4eae/bikeshed/refs/manager.py#L291-L297
 function getBikeshedLinkTextSet(elem) {
     const texts = new Set();