]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
New validator (#41775)
authorMark Otto <markd.otto@gmail.com>
Sun, 28 Sep 2025 03:33:21 +0000 (20:33 -0700)
committerMark Otto <markdotto@gmail.com>
Fri, 10 Oct 2025 17:04:00 +0000 (10:04 -0700)
* New validator

* update

* remove

* update

.github/workflows/docs.yml
build/html-validate.mjs [new file with mode: 0644]
build/vnu-jar.mjs [deleted file]
package-lock.json
package.json

index 59abb9111ff5dbba36e801bd03810490b628df5e..93035f41dec939b323eb51833536c9d71e642bc5 100644 (file)
@@ -30,8 +30,6 @@ jobs:
           node-version: "${{ env.NODE }}"
           cache: npm
 
-      - run: java -version
-
       - name: Install npm dependencies
         run: npm ci
 
@@ -39,7 +37,7 @@ jobs:
         run: npm run docs-build
 
       - name: Validate HTML
-        run: npm run docs-vnu
+        run: npm run docs-html-validate
 
       - name: Run linkinator
         uses: JustinBeckwith/linkinator-action@3d5ba091319fa7b0ac14703761eebb7d100e6f6d # v1.11.0
diff --git a/build/html-validate.mjs b/build/html-validate.mjs
new file mode 100644 (file)
index 0000000..09b9ad6
--- /dev/null
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to run html-validate for HTML validation.
+ *
+ * This replaces the Java-based vnu-jar validator with a faster, Node.js-only solution.
+ * Benefits:
+ * - No Java dependency required
+ * - Faster execution (no JVM startup time)
+ * - Easy to configure with rule-based system
+ * - Better integration with Node.js build tools
+ *
+ * Copyright 2017-2025 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+import { HtmlValidate } from 'html-validate'
+import { globby } from 'globby'
+
+const htmlValidate = new HtmlValidate({
+  rules: {
+    // Allow autocomplete on buttons (Bootstrap specific)
+    'attribute-allowed-values': 'off',
+    // Allow aria-disabled on links (Bootstrap specific)
+    'aria-label-misuse': 'off',
+    // Allow modern CSS syntax
+    'valid-id': 'off',
+    // Allow void elements with trailing slashes (Astro)
+    'void-style': 'off',
+    // Allow custom attributes
+    'no-unknown-elements': 'off',
+    'attribute-boolean-style': 'off',
+    'no-inline-style': 'off',
+    // KEEP duplicate ID checking enabled (this is important for HTML validity)
+    'no-dup-id': 'error'
+  },
+  elements: [
+    'html5',
+    {
+      // Allow custom attributes for Astro/framework compatibility
+      '*': {
+        attributes: {
+          'is:raw': { boolean: true },
+          switch: { boolean: true },
+          autocomplete: { enum: ['on', 'off', 'new-password', 'current-password'] }
+        }
+      }
+    }
+  ]
+})
+
+async function validateHTML() {
+  try {
+    console.log('Running html-validate validation...')
+
+    // Find all HTML files
+    const files = await globby([
+      '_site/**/*.html',
+      'js/tests/**/*.html'
+    ], {
+      ignore: ['**/node_modules/**']
+    })
+
+    console.log(`Validating ${files.length} HTML files...`)
+
+    let hasErrors = false
+
+    // Validate all files in parallel to avoid await-in-loop
+    const validationPromises = files.map(file =>
+      htmlValidate.validateFile(file).then(report => ({ file, report }))
+    )
+
+    const validationResults = await Promise.all(validationPromises)
+
+    // Process results and check for errors
+    for (const { file, report } of validationResults) {
+      if (!report.valid) {
+        hasErrors = true
+        console.error(`\nErrors in ${file}:`)
+
+        // Extract error messages with reduced nesting
+        const errorMessages = report.results.flatMap(result =>
+          result.messages.filter(message => message.severity === 2)
+        )
+
+        for (const message of errorMessages) {
+          console.error(`  Line ${message.line}:${message.column} - ${message.message} (${message.ruleId})`)
+        }
+      }
+    }
+
+    if (hasErrors) {
+      console.error('\nHTML validation failed!')
+      process.exit(1)
+    } else {
+      console.log('✓ All HTML files are valid!')
+    }
+  } catch (error) {
+    console.error('HTML validation error:', error)
+    process.exit(1)
+  }
+}
+
+validateHTML()
diff --git a/build/vnu-jar.mjs b/build/vnu-jar.mjs
deleted file mode 100644 (file)
index 4eedb1b..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env node
-
-/*!
- * Script to run vnu-jar if Java is available.
- * Copyright 2017-2025 The Bootstrap Authors
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
- */
-
-import { execFile, spawn } from 'node:child_process'
-import vnu from 'vnu-jar'
-
-execFile('java', ['-version'], (error, stdout, stderr) => {
-  if (error) {
-    console.error('Skipping vnu-jar test; Java is probably missing.')
-    console.error(error)
-    return
-  }
-
-  console.log('Running vnu-jar validation...')
-
-  const is32bitJava = !/64-Bit/.test(stderr)
-
-  // vnu-jar accepts multiple ignores joined with a `|`.
-  // Also note that the ignores are string regular expressions.
-  const ignores = [
-    // "autocomplete" is included in <button> and checkboxes and radio <input>s due to
-    // Firefox's non-standard autocomplete behavior - see https://bugzilla.mozilla.org/show_bug.cgi?id=654072
-    'Attribute “autocomplete” is only allowed when the input type is.*',
-    'Attribute “autocomplete” not allowed on element “button” at this point.',
-    // Per https://www.w3.org/TR/html-aria/#docconformance having "aria-disabled" on a link is
-    // NOT RECOMMENDED, but it's still valid - we explain in the docs that it's not ideal,
-    // and offer more robust alternatives, but also need to show a less-than-ideal example
-    'An “aria-disabled” attribute whose value is “true” should not be specified on an “a” element that has an “href” attribute.',
-    // A `code` element with the `is:raw` attribute coming from remark-prismjs (Astro upstream possible bug)
-    'Attribute “is:raw” is not serializable as XML 1.0.',
-    'Attribute “is:raw” not allowed on element “code” at this point.',
-    // Astro's expecting trailing slashes on HTML tags such as <br />
-    'Trailing slash on void elements has no effect and interacts badly with unquoted attribute values.',
-    // Allow `switch` attribute.
-    'Attribute “switch” not allowed on element “input” at this point.'
-  ].join('|')
-
-  const args = [
-    '-jar',
-    `"${vnu}"`,
-    '--asciiquotes',
-    '--skip-non-html',
-    '--Werror',
-    `--filterpattern "${ignores}"`,
-    '_site/',
-    'js/tests/'
-  ]
-
-  // For the 32-bit Java we need to pass `-Xss512k`
-  if (is32bitJava) {
-    args.splice(0, 0, '-Xss512k')
-  }
-
-  console.log(`command used: java ${args.join(' ')}`)
-
-  return spawn('java', args, {
-    shell: true,
-    stdio: 'inherit'
-  })
-    .on('exit', process.exit)
-})
index ac593702644c7b5928efe9d40d37f7404dea370f..be19fec6ffb932e29d09abd9dd3f5aae505e29f1 100644 (file)
@@ -54,6 +54,7 @@
         "github-slugger": "^2.0.0",
         "globby": "^15.0.0",
         "hammer-simulator": "0.0.1",
+        "html-validate": "^8.24.1",
         "htmlparser2": "^10.0.0",
         "image-size": "^2.0.2",
         "ip": "^2.0.1",
@@ -90,7 +91,6 @@
         "stylelint-config-twbs-bootstrap": "^16.1.0",
         "terser": "^5.44.0",
         "unist-util-visit": "^5.0.0",
-        "vnu-jar": "24.10.17",
         "zod": "^4.1.12"
       },
       "peerDependencies": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@html-validate/stylish": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-4.3.0.tgz",
+      "integrity": "sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==",
+      "dev": true,
+      "dependencies": {
+        "kleur": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.13.0",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
       "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
       "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
       }
       "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
       "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
       "dev": true,
-      "license": "MIT",
       "engines": {
         "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
       }
     },
     "node_modules/@jest/schemas": {
-      "version": "30.0.5",
-      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
-      "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+      "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
       "dev": true,
-      "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
-        "@sinclair/typebox": "^0.34.0"
+        "@sinclair/typebox": "^0.27.8"
       },
       "engines": {
-        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
     "node_modules/@jridgewell/gen-mapping": {
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@sidvind/better-ajv-errors": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-3.0.1.tgz",
+      "integrity": "sha512-++1mEYIeozfnwWI9P1ECvOPoacy+CgDASrmGvXPMCcqgx0YUzB01vZ78uHdQ443V6sTY+e9MzHqmN9DOls02aw==",
+      "dev": true,
+      "dependencies": {
+        "kleur": "^4.1.0"
+      },
+      "engines": {
+        "node": ">= 16.14"
+      },
+      "peerDependencies": {
+        "ajv": "^6.12.3 || ^7.0.0 || ^8.0.0"
+      }
+    },
     "node_modules/@sinclair/typebox": {
-      "version": "0.34.41",
-      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
-      "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+      "version": "0.27.8",
+      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
+      "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
       "dev": true,
-      "license": "MIT"
+      "optional": true,
+      "peer": true
     },
     "node_modules/@sindresorhus/merge-streams": {
       "version": "4.0.0",
         "node": ">=0.3.1"
       }
     },
+    "node_modules/diff-sequences": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+      "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/html-validate": {
+      "version": "8.29.0",
+      "resolved": "https://registry.npmjs.org/html-validate/-/html-validate-8.29.0.tgz",
+      "integrity": "sha512-RFfFIWaUB9SN8iETL2GoPvjWEX1gcbz0+m+vao7xkPl0cnlgMDu9RcjdQz6T3n6LgT/LENPkvxHzVkqY/Qs3Tg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/html-validate"
+        }
+      ],
+      "dependencies": {
+        "@html-validate/stylish": "^4.1.0",
+        "@sidvind/better-ajv-errors": "3.0.1",
+        "ajv": "^8.0.0",
+        "glob": "^10.0.0",
+        "kleur": "^4.1.0",
+        "minimist": "^1.2.0",
+        "prompts": "^2.0.0",
+        "semver": "^7.0.0"
+      },
+      "bin": {
+        "html-validate": "bin/html-validate.js"
+      },
+      "engines": {
+        "node": ">= 16.14"
+      },
+      "peerDependencies": {
+        "jest": "^27.1 || ^28.1.3 || ^29.0.3",
+        "jest-diff": "^27.1 || ^28.1.3 || ^29.0.3",
+        "jest-snapshot": "^27.1 || ^28.1.3 || ^29.0.3",
+        "vitest": "^0.34.0 || ^1.0.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "jest": {
+          "optional": true
+        },
+        "jest-diff": {
+          "optional": true
+        },
+        "jest-snapshot": {
+          "optional": true
+        },
+        "vitest": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/html-validate/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/html-validate/node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/html-validate/node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/html-validate/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true
+    },
+    "node_modules/html-validate/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/html-validate/node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/html-void-elements": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
       }
     },
     "node_modules/jest-diff": {
-      "version": "30.2.0",
-      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
-      "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==",
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+      "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
       "dev": true,
-      "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
-        "@jest/diff-sequences": "30.0.1",
-        "@jest/get-type": "30.1.0",
-        "chalk": "^4.1.2",
-        "pretty-format": "30.2.0"
+        "chalk": "^4.0.0",
+        "diff-sequences": "^29.6.3",
+        "jest-get-type": "^29.6.3",
+        "pretty-format": "^29.7.0"
       },
       "engines": {
-        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
     "node_modules/jest-diff/node_modules/ansi-styles": {
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
       "dev": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "color-convert": "^2.0.1"
       },
       "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
       "dev": true,
       "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/jest-get-type": {
+      "version": "29.6.3",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+      "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+      }
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       }
     },
     "node_modules/pretty-format": {
-      "version": "30.2.0",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
-      "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+      "version": "29.7.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+      "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
       "dev": true,
-      "license": "MIT",
+      "optional": true,
+      "peer": true,
       "dependencies": {
-        "@jest/schemas": "30.0.5",
-        "ansi-styles": "^5.2.0",
-        "react-is": "^18.3.1"
+        "@jest/schemas": "^29.6.3",
+        "ansi-styles": "^5.0.0",
+        "react-is": "^18.0.0"
       },
       "engines": {
-        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
       }
     },
     "node_modules/pretty-format/node_modules/ansi-styles": {
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
       "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
       "dev": true,
-      "license": "MIT",
+      "optional": true,
+      "peer": true,
       "engines": {
         "node": ">=10"
       },
       "version": "18.3.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
       "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/read-cache": {
       "version": "1.0.0",
         }
       }
     },
+    "node_modules/sass-true/node_modules/@jest/schemas": {
+      "version": "30.0.5",
+      "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+      "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+      "dev": true,
+      "dependencies": {
+        "@sinclair/typebox": "^0.34.0"
+      },
+      "engines": {
+        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+      }
+    },
+    "node_modules/sass-true/node_modules/@sinclair/typebox": {
+      "version": "0.34.41",
+      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+      "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+      "dev": true
+    },
+    "node_modules/sass-true/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/sass-true/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/sass-true/node_modules/jest-diff": {
+      "version": "30.1.2",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz",
+      "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==",
+      "dev": true,
+      "dependencies": {
+        "@jest/diff-sequences": "30.0.1",
+        "@jest/get-type": "30.1.0",
+        "chalk": "^4.1.2",
+        "pretty-format": "30.0.5"
+      },
+      "engines": {
+        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+      }
+    },
+    "node_modules/sass-true/node_modules/pretty-format": {
+      "version": "30.0.5",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
+      "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
+      "dev": true,
+      "dependencies": {
+        "@jest/schemas": "30.0.5",
+        "ansi-styles": "^5.2.0",
+        "react-is": "^18.3.1"
+      },
+      "engines": {
+        "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+      }
+    },
+    "node_modules/sass-true/node_modules/pretty-format/node_modules/ansi-styles": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+      "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
     "node_modules/sax": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
         }
       }
     },
-    "node_modules/vnu-jar": {
-      "version": "24.10.17",
-      "resolved": "https://registry.npmjs.org/vnu-jar/-/vnu-jar-24.10.17.tgz",
-      "integrity": "sha512-YT7gNrRY5PiJrI1GavlWRHWIwqq2o52COc6J9QeXPfoldKRiZ9BeGP4shNLLaVfi0naA+/LMksdYWkKCr4pnVg==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
     "node_modules/void-elements": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
index 60e900dfa63d1a522506ff17e0d105f90c0fd1b1..5f62d30d2d062ff6abebad1f1f597341ed9b246c 100644 (file)
@@ -77,8 +77,8 @@
     "docs": "npm-run-all docs-build docs-lint",
     "docs-build": "npm run astro-build",
     "docs-compile": "npm run docs-build",
-    "docs-vnu": "node build/vnu-jar.mjs",
-    "docs-lint": "npm-run-all docs-prettier-check docs-vnu",
+    "docs-html-validate": "node build/html-validate.mjs",
+    "docs-lint": "npm-run-all docs-prettier-check docs-html-validate",
     "docs-prettier-check": "prettier --config site/.prettierrc.json -c --cache site",
     "docs-prettier-format": "prettier --config site/.prettierrc.json --write --cache site",
     "docs-serve": "npm run astro-dev",
     "github-slugger": "^2.0.0",
     "globby": "^15.0.0",
     "hammer-simulator": "0.0.1",
+    "html-validate": "^8.24.1",
     "htmlparser2": "^10.0.0",
     "image-size": "^2.0.2",
     "ip": "^2.0.1",
     "stylelint-config-twbs-bootstrap": "^16.1.0",
     "terser": "^5.44.0",
     "unist-util-visit": "^5.0.0",
-    "vnu-jar": "24.10.17",
     "zod": "^4.1.12"
   },
   "files": [