1
1
mirror of https://github.com/theoludwig/html-w3c-validator.git synced 2025-05-21 23:21:29 +02:00

17 Commits

Author SHA1 Message Date
fbe3830db7 feat: json $schema locally => ./node_modules/html-w3c-validator/schema/schema.json 2024-11-09 16:42:44 +01:00
58d392d320 chore: allow to publish pre-release beta version 2024-11-09 16:22:14 +01:00
e9a62072cf chore: remove usage of git hooks (husky, lint-staged, commitlint) + usage of node --run 2024-11-09 16:10:02 +01:00
cc6a1278a1 fix: print correctly validation results
There was an error thrown by `table` library to print validation results
when there were 2 (or more) errors, with one error with line/column
and the other error without specified line/column.

Fixes #6
2024-11-09 15:45:31 +01:00
e53c80d578 fix: update dependencies to latest 2024-05-23 21:21:34 +02:00
5f63d749e8 docs: fix wrong link for .html-w3c-validatorrc.json JSON schema 2024-02-02 17:28:44 +01:00
67b70feb15 fix: wrong husky config 2024-02-02 17:10:44 +01:00
6b28c6d224 perf: remove usage of html-validator dependency
Allows to have better control of the implementation.
Also reduce package size by removing needed dependencies to install.
2024-02-02 17:02:19 +01:00
a1340b2729 fix: improve config schema 2024-02-02 15:56:32 +01:00
92320385b5 docs: update description 2024-02-02 15:44:03 +01:00
8934ac1b7a feat: add config JSON schema 2024-02-02 15:35:18 +01:00
09731d4749 refactor: simplify logic 2024-02-02 14:47:31 +01:00
2e0e136355 docs: add CLI option --current-working-directory 2024-02-02 08:22:57 +01:00
04a2742e46 chore: cleaner configs 2024-01-30 01:16:56 +01:00
75f862d0fd docs(license): add email address 2024-01-29 21:57:54 +01:00
7dafa2401b fix: update dependencies to latest 2023-12-26 22:43:05 +01:00
02a5010340 chore: better Prettier config for easier reviews 2023-10-23 23:21:05 +02:00
46 changed files with 5418 additions and 7783 deletions

View File

@ -1 +0,0 @@
{ "extends": ["@commitlint/config-conventional"] }

View File

@ -1,16 +1,18 @@
{ {
"extends": ["conventions", "prettier"], "root": true,
"plugins": ["prettier", "import", "unicorn"], "extends": ["conventions"],
"parserOptions": { "plugins": ["promise", "unicorn"],
"project": "./tsconfig.json" "overrides": [
}, {
"env": { "files": ["*.ts", "*.tsx"],
"node": true "parser": "@typescript-eslint/parser",
}, "plugins": ["@typescript-eslint"],
"rules": { "parserOptions": {
"prettier/prettier": "error", "projectService": true
"import/extensions": ["error", "always"], },
"unicorn/prevent-abbreviations": "error", "rules": {
"unicorn/prefer-node-protocol": "error" "@typescript-eslint/no-unnecessary-condition": "off"
} }
}
]
} }

View File

@ -1,13 +1,13 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--
Please provide a clear and concise description of what the bug is. Include Please provide a clear and concise description of what the bug is.
screenshots if needed. Please make sure your issue has not already been fixed. Include screenshots if needed. Please make sure your issue has not already been fixed.
--> -->
## Steps To Reproduce ## Steps To Reproduce

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,27 +1,27 @@
name: 'Build' name: "Build"
on: on:
push: push:
branches: [develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [main, develop]
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.2.2"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v4.1.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "node --run build"
- run: 'npm run build:typescript' - run: "node --run build:typescript"

View File

@ -1,28 +1,26 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
branches: [develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [main, develop]
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.2.2"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v4.1.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- run: 'npm run lint:commit -- --to "${{ github.sha }}"' - run: "node --run lint:editorconfig"
- run: 'npm run lint:editorconfig' - run: "node --run lint:eslint"
- run: 'npm run lint:markdown' - run: "node --run lint:prettier"
- run: 'npm run lint:eslint'
- run: 'npm run lint:prettier'

View File

@ -1,42 +1,42 @@
name: 'Release' name: "Release"
on: on:
push: push:
branches: [master] branches: [main, beta]
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
permissions: permissions:
contents: 'write' contents: "write"
issues: 'write' issues: "write"
pull-requests: 'write' pull-requests: "write"
id-token: 'write' id-token: "write"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.2.2"
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v4.1.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "node --run build"
- run: 'npm run build:typescript' - run: "node --run build:typescript"
- name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies' - name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies"
run: 'npm audit signatures' run: "npm audit signatures"
- name: 'Release' - name: "Release"
run: 'npm run release' run: "node --run release"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,28 +1,28 @@
name: 'Test' name: "Test"
on: on:
push: push:
branches: [develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [main, develop]
jobs: jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.2.2"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v4.1.0"
with: with:
node-version: 'lts/*' node-version: "lts/*"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "node --run build"
- name: 'Test' - name: "Test"
run: 'npm run test' run: "node --run test"

View File

@ -1,6 +0,0 @@
{
"*": ["editorconfig-checker"],
"*.{js,ts,jsx,tsx}": ["prettier --write", "eslint --fix"],
"*.{yml,json}": ["prettier --write"],
"*.md": ["prettier --write", "markdownlint --dot --fix"]
}

View File

@ -1,11 +0,0 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true,
"MD033": false
},
"globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
}

4
.npmrc
View File

@ -1,2 +1,2 @@
save-exact=true save-exact = true
provenance=true provenance = true

View File

@ -1,6 +1,3 @@
{ {
"singleQuote": true, "semi": false
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

View File

@ -1,18 +1,8 @@
{ {
"branches": ["master"], "branches": ["main", { "name": "beta", "prerelease": true }],
"plugins": [ "plugins": [
[ "@semantic-release/commit-analyzer",
"@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
"@semantic-release/npm", "@semantic-release/npm",
"@semantic-release/github" "@semantic-release/github"
] ]

2
.swcrc
View File

@ -5,7 +5,7 @@
"syntax": "typescript", "syntax": "typescript",
"dynamicImport": true "dynamicImport": true
}, },
"target": "es2022" "target": "esnext"
}, },
"module": { "module": {
"type": "es6" "type": "es6"

View File

@ -1,8 +0,0 @@
{
"recommendations": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint"
]
}

14
.vscode/settings.json vendored
View File

@ -1,14 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifierEnding": "js",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.configPath": ".prettierrc.json",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
}

View File

@ -1,6 +1,6 @@
MIT License # MIT License
Copyright (c) Théo LUDWIG Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,19 +1,17 @@
<h1 align="center">html-w3c-validator</h1> <h1 align="center">html-w3c-validator</h1>
<p align="center"> <p align="center">
<strong>CLI for validating multiple html pages using <a href="https://validator.w3.org/">validator.w3.org</a>.</strong> <strong>CLI for validating HTML pages using <a href="https://validator.w3.org/">validator.w3.org</a>.</strong>
</p>
</p> </p>
<p align="center"> <p align="center">
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="CONTRIBUTING" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a> <a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
<a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a> <a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
<br /> <br />
<a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/build.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/build.yml/badge.svg?branch=develop" /></a> <a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/build.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/build.yml/badge.svg?branch=develop" alt="Build" /></a>
<a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/lint.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/lint.yml/badge.svg?branch=develop" /></a> <a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/lint.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/lint.yml/badge.svg?branch=develop" alt="Lint" /></a>
<a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/test.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/test.yml/badge.svg?branch=develop" /></a> <a href="https://github.com/theoludwig/html-w3c-validator/actions/workflows/test.yml"><img src="https://github.com/theoludwig/html-w3c-validator/actions/workflows/test.yml/badge.svg?branch=develop" alt="Test" /></a>
<br /> <br />
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a> <a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
<a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a> <a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a>
@ -22,11 +20,11 @@
## 📜 About ## 📜 About
**html-w3c-validator** is a CLI tool to validate multiple html pages using [validator.w3.org](https://validator.w3.org/). **html-w3c-validator** is a CLI tool to validate HTML pages using [validator.w3.org](https://validator.w3.org/).
You might use a JavaScript framework or simply use HTML but **you should always validate your production HTML** and this validation should be part of your CI/CD pipeline (tests, linting, etc.). You might use a JavaScript framework or simply use HTML but **you should always validate your production HTML** and this validation should be part of your CI/CD pipeline (tests, linting, etc.).
### Why should I validate my HTML pages? ### Why is HTML page validation important?
Quote from [https://validator.w3.org/docs/help.html#why-validate](https://validator.w3.org/docs/help.html#why-validate): Quote from [https://validator.w3.org/docs/help.html#why-validate](https://validator.w3.org/docs/help.html#why-validate):
@ -40,7 +38,7 @@ You can combine **html-w3c-validator** with [start-server-and-test](https://gith
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= 16.0.0 [Node.js](https://nodejs.org/) >= 16.0.0
### Installation (with [start-server-and-test](https://github.com/bahmutov/start-server-and-test)) ### Installation (with [start-server-and-test](https://github.com/bahmutov/start-server-and-test))
@ -59,8 +57,8 @@ npm install --save-dev html-w3c-validator start-server-and-test
"start": "serve \"./build\"", "start": "serve \"./build\"",
// Command to validate your HTML pages // Command to validate your HTML pages
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"" "test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"",
} },
} }
``` ```
@ -68,20 +66,23 @@ npm install --save-dev html-w3c-validator start-server-and-test
```jsonc ```jsonc
{ {
"$schema": "./node_modules/html-w3c-validator/schema/schema.json",
// URLs to validate.
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"], "urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"],
// You can also specify HTML files instead of URLs // Files to validate.
"files": ["./index.html", "./about.html"], "files": ["./index.html", "./about.html"],
// Specify the severities of the validator (default: ["warning", "error"]) // Specify the severities of the validator messages to report. (default: ["warning", "error"])
"severities": ["info", "warning", "error"] "severities": ["info", "warning", "error"],
} }
``` ```
## Usage ## Usage
```sh ```sh
npm run test:html-w3c-validator node --run test:html-w3c-validator
``` ```
Example of output (in case of success): Example of output (in case of success):
@ -98,8 +99,9 @@ See the [./example](./example) folder for practical usage.
### Options ### Options
```text ```text
-V, --version Output the version number. --current-working-directory <path> The current working directory (default: `process.cwd()`).
-h, --help Display help for command. -V, --version Output the version number.
-h, --help Display help for command.
``` ```
## 💡 Contributing ## 💡 Contributing

View File

@ -1,4 +1,5 @@
{ {
"$schema": "./node_modules/html-w3c-validator/schema/schema.json",
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"], "urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"],
"files": ["./build/index.html", "./build/about.html"] "files": ["./build/index.html", "./build/about.html"]
} }

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@
"test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\"" "test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\""
}, },
"devDependencies": { "devDependencies": {
"serve": "14.2.0", "serve": "14.2.4",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.8",
"html-w3c-validator": "file:.." "html-w3c-validator": "file:.."
} }
} }

11750
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
{ {
"name": "html-w3c-validator", "name": "html-w3c-validator",
"version": "0.0.0-development", "version": "0.0.0-development",
"description": "CLI for validating HTML pages using validator.w3.org.",
"public": true, "public": true,
"description": "CLI for validating multiple html pages using validator.w3.org.", "types": "module",
"type": "module", "type": "module",
"author": "Théo LUDWIG <contact@theoludwig.fr>", "author": "Théo LUDWIG <contact@theoludwig.fr>",
"license": "MIT", "license": "MIT",
@ -24,6 +25,7 @@
"bin": "build/index.js", "bin": "build/index.js",
"files": [ "files": [
"build", "build",
"./schema/schema.json",
"!**/*.test.js", "!**/*.test.js",
"!**/*.map" "!**/*.map"
], ],
@ -32,61 +34,44 @@
"provenance": true "provenance": true
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0"
"npm": ">=9.0.0"
}, },
"scripts": { "scripts": {
"build": "rimraf ./build && swc ./src --out-dir ./build", "build": "swc ./src --out-dir ./build --strip-leading-paths --delete-dir-on-start",
"build:typescript": "tsc", "build:typescript": "tsc",
"start": "node --enable-source-maps build/index.js", "start": "node --enable-source-maps build/index.js",
"lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:prettier": "prettier . --check", "lint:prettier": "prettier . --check",
"lint:staged": "lint-staged", "test": "node --enable-source-maps --test",
"test": "node --enable-source-maps --test build/",
"release": "semantic-release" "release": "semantic-release"
}, },
"dependencies": { "dependencies": {
"chalk": "5.3.0", "chalk": "5.3.0",
"clipanion": "3.2.1", "clipanion": "3.2.1",
"html-validator": "6.0.1", "log-symbols": "6.0.0",
"log-symbols": "5.1.0", "ora": "8.1.1",
"ora": "7.0.1", "read-pkg": "9.0.1",
"read-pkg": "8.0.0", "table": "6.8.2"
"table": "6.8.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.7.0", "@swc/cli": "0.5.0",
"@commitlint/config-conventional": "17.7.0", "@swc/core": "1.9.1",
"@swc/cli": "0.1.62", "@tsconfig/strictest": "2.0.5",
"@swc/core": "1.3.75", "@types/node": "22.9.0",
"@tsconfig/strictest": "2.0.1", "@types/sinon": "17.0.3",
"@types/html-validator": "5.0.3", "@typescript-eslint/eslint-plugin": "8.13.0",
"@types/mock-fs": "4.13.1", "@typescript-eslint/parser": "8.13.0",
"@types/node": "20.4.9", "editorconfig-checker": "6.0.0",
"@types/sinon": "10.0.16", "eslint": "8.57.1",
"@typescript-eslint/eslint-plugin": "6.3.0", "eslint-config-conventions": "16.0.1",
"@typescript-eslint/parser": "6.3.0", "eslint-plugin-promise": "7.1.0",
"editorconfig-checker": "5.1.1", "eslint-plugin-unicorn": "55.0.0",
"eslint": "8.46.0", "execa": "9.5.1",
"eslint-config-conventions": "11.0.1", "prettier": "3.3.3",
"eslint-config-prettier": "9.0.0", "semantic-release": "23.1.1",
"eslint-plugin-import": "2.28.0", "serve": "14.2.4",
"eslint-plugin-prettier": "5.0.0", "sinon": "19.0.2",
"eslint-plugin-promise": "6.1.1", "typescript": "5.6.3"
"eslint-plugin-unicorn": "48.0.1",
"execa": "7.2.0",
"lint-staged": "13.2.3",
"markdownlint-cli2": "0.8.1",
"markdownlint-rule-relative-links": "2.1.0",
"mock-fs": "5.2.0",
"prettier": "3.0.1",
"rimraf": "5.0.1",
"semantic-release": "21.0.7",
"serve": "14.2.0",
"sinon": "15.2.0",
"typescript": "5.1.6"
} }
} }

48
schema/schema.json Normal file
View File

@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "html-w3c-validator://config/schema.json",
"title": "html-w3c-validator configuration schema",
"type": "object",
"properties": {
"$schema": {
"description": "JSON Schema",
"type": "string"
},
"urls": {
"description": "URLs to validate.",
"type": "array",
"default": [],
"minItems": 1,
"items": {
"description": "URL to validate.",
"type": "string",
"format": "uri",
"minLength": 1
}
},
"files": {
"description": "Files to validate.",
"type": "array",
"minItems": 1,
"default": [],
"items": {
"description": "File to validate.",
"type": "string",
"format": "uri-reference",
"minLength": 1
}
},
"severities": {
"description": "Specify the severities to report.",
"type": "array",
"minItems": 1,
"default": ["warning", "error"],
"items": {
"description": "Severity to report.",
"type": "string",
"enum": ["error", "warning", "info"]
}
}
},
"additionalProperties": false
}

View File

@ -1,23 +1,19 @@
import path from 'node:path' import fs from "node:fs"
import fs from 'node:fs' import path from "node:path"
import { Command, Option } from 'clipanion' import chalk from "chalk"
import * as typanion from 'typanion' import { Command, Option } from "clipanion"
import chalk from 'chalk' import logSymbols from "log-symbols"
import ora from 'ora' import ora from "ora"
import logSymbols from 'log-symbols' import { table } from "table"
import type { import * as typanion from "typanion"
ValidationMessageLocationObject,
ParsedJsonAsValidationResults
} from 'html-validator'
import validateHTML from 'html-validator'
import { table } from 'table'
import { isExistingPath } from './utils/isExistingPath.js' import type { ValidateHTMLResult } from "./validateHTML.js"
import { getHTMLFromURL, validateHTML } from "./validateHTML.js"
export const CONFIG_FILE_NAME = '.html-w3c-validatorrc.json' export const CONFIG_FILE_NAME = ".html-w3c-validatorrc.json"
export const SEVERITIES = ['error', 'warning', 'info'] as const export const SEVERITIES = ["error", "warning", "info"] as const
export type Severity = (typeof SEVERITIES)[number] export type Severity = (typeof SEVERITIES)[number]
@ -50,84 +46,82 @@ const printResults = (results: Result[]): void => {
export class HTMLValidatorCommand extends Command { export class HTMLValidatorCommand extends Command {
static override usage = { static override usage = {
description: description:
'CLI for validating multiple html pages using <https://validator.w3.org/>.' "CLI for validating HTML pages using <https://validator.w3.org/>.",
} }
public currentWorkingDirectory = Option.String( public currentWorkingDirectory = Option.String(
'--current-working-directory', "--current-working-directory",
process.cwd(), process.cwd(),
{ {
description: 'The current working directory.', description: "The current working directory.",
validator: typanion.isString() validator: typanion.isString(),
} },
) )
public async execute(): Promise<number> { public async execute(): Promise<number> {
const configPath = path.join(this.currentWorkingDirectory, CONFIG_FILE_NAME) const configPath = path.join(this.currentWorkingDirectory, CONFIG_FILE_NAME)
try { try {
if (!(await isExistingPath(configPath))) { let configData: string
try {
configData = await fs.promises.readFile(configPath, {
encoding: "utf-8",
})
} catch {
throw new Error( throw new Error(
`No config file found at ${configPath}. Please create "${CONFIG_FILE_NAME}".` `No config file found at ${configPath}. Please create "${CONFIG_FILE_NAME}".`,
) )
} }
const configData = await fs.promises.readFile(configPath, {
encoding: 'utf-8'
})
let config: Config = { urls: [], files: [] } let config: Config = { urls: [], files: [] }
let isValidConfig = true
try { try {
config = JSON.parse(configData) config = JSON.parse(configData)
} catch { } catch {
isValidConfig = false
}
if (!isValidConfig) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please check the JSON syntax.` `Invalid config file at "${configPath}". Please check the JSON syntax.`,
) )
} }
if (config.urls != null && !Array.isArray(config.urls)) { if (config.urls != null && !Array.isArray(config.urls)) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please include an array of URLs.` `Invalid config file at "${configPath}". Please include an array of URLs.`,
) )
} }
if (config.files != null && !Array.isArray(config.files)) { if (config.files != null && !Array.isArray(config.files)) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please include an array of files.` `Invalid config file at "${configPath}". Please include an array of files.`,
) )
} }
const urls = const urls =
config.urls == null config.urls == null
? [] ? []
: config.urls.map((url) => { : config.urls.map((url) => {
return { type: 'url', data: url } return { type: "url", data: url }
}) })
const files = const files =
config.files == null config.files == null
? [] ? []
: config.files.map((file) => { : config.files.map((file) => {
return { type: 'file', data: file } return { type: "file", data: file }
}) })
const dataToValidate = [...urls, ...files] const dataToValidate = [...urls, ...files]
if (dataToValidate.length === 0) { if (dataToValidate.length === 0) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please add URLs or files.` `Invalid config file at "${configPath}". Please add URLs or files.`,
) )
} }
const severities: Severity[] = config.severities ?? ['warning', 'error'] const severities: Severity[] = config.severities ?? ["warning", "error"]
for (const severity of severities) { for (const severity of severities) {
if (!SEVERITIES.includes(severity)) { if (!SEVERITIES.includes(severity)) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join( `Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
', ' ", ",
)}).` )}).`,
) )
} }
} }
if (severities.length === 0) { if (severities.length === 0) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join( `Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
', ' ", ",
)}).` )}).`,
) )
} }
const errors: Error[] = [] const errors: Error[] = []
@ -137,32 +131,28 @@ export class HTMLValidatorCommand extends Command {
await Promise.all( await Promise.all(
dataToValidate.map(async ({ data, type }) => { dataToValidate.map(async ({ data, type }) => {
try { try {
const options = { let result: ValidateHTMLResult | undefined
format: 'json' as 'json' | undefined if (type === "url") {
}
let result: ParsedJsonAsValidationResults | undefined
if (type === 'url') {
result = await validateHTML({ result = await validateHTML({
url: data, htmlData: await getHTMLFromURL(data),
isLocal: true,
...options
}) })
} else if (type === 'file') { } else if (type === "file") {
const htmlPath = path.resolve(this.currentWorkingDirectory, data) const htmlPath = path.resolve(this.currentWorkingDirectory, data)
if (!(await isExistingPath(htmlPath))) { let html: string
try {
html = await fs.promises.readFile(htmlPath, {
encoding: "utf-8",
})
} catch {
throw new Error( throw new Error(
`No file found at "${htmlPath}". Please check the path.` `No file found at "${htmlPath}". Please check the path.`,
) )
} }
const html = await fs.promises.readFile(htmlPath, {
encoding: 'utf-8'
})
result = await validateHTML({ result = await validateHTML({
data: html, htmlData: html,
...options
}) })
} else { } else {
throw new Error('Invalid type') throw new Error("Invalid type")
} }
const hasErrors = result.messages.some((message) => { const hasErrors = result.messages.some((message) => {
return ( return (
@ -184,8 +174,8 @@ export class HTMLValidatorCommand extends Command {
} }
const row: string[] = [] const row: string[] = []
if (message.type === 'info') { if (message.type === "info") {
if (message.subType === 'warning') { if (message.subType === "warning") {
row.push(chalk.yellow(message.subType)) row.push(chalk.yellow(message.subType))
} else { } else {
row.push(chalk.blue(message.type)) row.push(chalk.blue(message.type))
@ -194,11 +184,17 @@ export class HTMLValidatorCommand extends Command {
row.push(chalk.red(message.type)) row.push(chalk.red(message.type))
} }
row.push(message.message) row.push(message.message)
const violation = message as ValidationMessageLocationObject if (
if (violation.extract != null) { message.extract != null &&
message.lastLine != null &&
message.firstColumn != null &&
message.lastColumn != null
) {
row.push( row.push(
`line: ${violation.lastLine}, column: ${violation.firstColumn}-${violation.lastColumn}` `line: ${message.lastLine}, column: ${message.firstColumn}-${message.lastColumn}`,
) )
} else {
row.push("")
} }
messagesTable.push(row) messagesTable.push(row)
} }
@ -212,7 +208,7 @@ export class HTMLValidatorCommand extends Command {
errors.push({ data, messagesTable }) errors.push({ data, messagesTable })
} }
} }
}) }),
) )
if (!isValid) { if (!isValid) {
loader.fail() loader.fail()
@ -220,22 +216,22 @@ export class HTMLValidatorCommand extends Command {
for (const error of errors) { for (const error of errors) {
console.error(`\n${error.data}`) console.error(`\n${error.data}`)
console.error(table(error.messagesTable)) console.error(table(error.messagesTable))
console.error('------------------------------') console.error("------------------------------")
} }
console.error() console.error()
throw new Error('HTML validation (W3C) failed!') throw new Error("HTML validation (W3C) failed!")
} }
loader.succeed( loader.succeed(
`${chalk.bold.green('Success:')} HTML validation (W3C) passed! 🎉` `${chalk.bold.green("Success:")} HTML validation (W3C) passed! 🎉`,
) )
printResults(results) printResults(results)
return 0 return 0
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
console.error(`${chalk.bold.red('Error:')} ${error.message}`) console.error(`${chalk.bold.red("Error:")} ${error.message}`)
} else { } else {
console.error( console.error(
`${chalk.bold.red('Error:')} HTML validation (W3C) failed!` `${chalk.bold.red("Error:")} HTML validation (W3C) failed!`,
) )
} }
return 1 return 1

View File

@ -1,416 +1,468 @@
import test from 'node:test' import test from "node:test"
import assert from 'node:assert/strict' import assert from "node:assert/strict"
import path from 'node:path' import path from "node:path"
import { PassThrough } from 'node:stream' import fs from "node:fs"
import { PassThrough } from "node:stream"
import { fileURLToPath } from "node:url"
import sinon from 'sinon' import sinon from "sinon"
import { execa } from 'execa' import { execa } from "execa"
import { table } from 'table' import { table } from "table"
import chalk from 'chalk' import chalk from "chalk"
import logSymbols from 'log-symbols' import logSymbols from "log-symbols"
import { cli } from '../cli.js' import { cli } from "../cli.js"
import { import {
HTMLValidatorCommand, HTMLValidatorCommand,
CONFIG_FILE_NAME, CONFIG_FILE_NAME,
SEVERITIES SEVERITIES,
} from '../HTMLValidatorCommand.js' } from "../HTMLValidatorCommand.js"
const FIXTURES_PATH = path.join(process.cwd(), 'src', '__test__', 'fixtures') const FIXTURES_PATH = path.join(process.cwd(), "src", "__test__", "fixtures")
await test('html-w3c-validator', async (t) => { await test("html-w3c-validator", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('should be instance of the command', async () => { await t.test("should be instance of the command", async () => {
const command = cli.process([]) const command = cli.process([])
assert(command instanceof HTMLValidatorCommand) assert(command instanceof HTMLValidatorCommand)
}) })
await t.test( await t.test(
'succeeds and validate the html correctly (example)', "succeeds and validate the html correctly (example)",
async () => { async () => {
const exampleURL = new URL('../../example', import.meta.url) const exampleURL = new URL("../../example", import.meta.url)
process.chdir(exampleURL.pathname) process.chdir(exampleURL.pathname)
await execa('rimraf', ['node_modules']) await fs.promises.rm(
await execa('npm', ['install']) path.join(fileURLToPath(exampleURL), "node_modules"),
const { exitCode } = await execa('npm', [ { recursive: true, force: true },
'run', )
'test:html-w3c-validator' await execa("npm", ["install"])
const { exitCode } = await execa("node", [
"--run",
"test:html-w3c-validator",
]) ])
assert.strictEqual(exitCode, 0) assert.strictEqual(exitCode, 0)
} },
) )
await t.test( await t.test(
'succeeds and validate the html correctly (example without working directory)', "succeeds and validate the html correctly (example without working directory)",
async () => { async () => {
const logs: string[] = [] const logs: string[] = []
sinon.stub(console, 'log').value((log: string) => { sinon.stub(console, "log").value((...log: string[]) => {
logs.push(log) logs.push(...log)
}) })
const consoleLogSpy = sinon.spy(console, 'log') const consoleLogSpy = sinon.spy(console, "log")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run([], { const exitCode = await cli.run([], {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
}) })
stream.end() stream.end()
assert.strictEqual(exitCode, 0) assert.strictEqual(exitCode, 0, logs.join("\n"))
assert.strictEqual( assert.strictEqual(
consoleLogSpy.calledWith( consoleLogSpy.calledWith(
logSymbols.success, logSymbols.success,
'./example/build/index.html' "./example/build/index.html",
), ),
true, true,
logs.join('\n') logs.join("\n"),
) )
assert.strictEqual( assert.strictEqual(
consoleLogSpy.calledWith( consoleLogSpy.calledWith(
logSymbols.success, logSymbols.success,
'./example/build/about.html' "./example/build/about.html",
), ),
true, true,
logs.join('\n') logs.join("\n"),
) )
} },
) )
await t.test( await t.test(
'succeeds and validate the html correctly (fixture)', "succeeds and validate the html correctly (fixture)",
async () => { async () => {
const workingDirectory = path.join(FIXTURES_PATH, 'success') const workingDirectory = path.join(FIXTURES_PATH, "success")
const logs: string[] = [] const logs: string[] = []
sinon.stub(console, 'log').value((log: string) => { sinon.stub(console, "log").value((...log: string[]) => {
logs.push(log) logs.push(...log)
}) })
const consoleLogSpy = sinon.spy(console, 'log') const consoleLogSpy = sinon.spy(console, "log")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 0) assert.strictEqual(exitCode, 0, logs.join("\n"))
assert.strictEqual( assert.strictEqual(
consoleLogSpy.calledWith(logSymbols.success, './build/index.html'), consoleLogSpy.calledWith(logSymbols.success, "./build/index.html"),
true, true,
logs.join('\n') logs.join("\n"),
) )
assert.strictEqual( assert.strictEqual(
consoleLogSpy.calledWith(logSymbols.success, './build/about.html'), consoleLogSpy.calledWith(logSymbols.success, "./build/about.html"),
true, true,
logs.join('\n') logs.join("\n"),
) )
} },
) )
await t.test('fails with not found config', async () => { await t.test(
const workingDirectory = path.join(FIXTURES_PATH, 'error-config-not-found') "fails by validating the html correctly with 2 errors: one with line/column, the other without (GitHub issue #6)",
async () => {
const workingDirectory = path.join(FIXTURES_PATH, "issue-6")
const errors: string[] = []
sinon.stub(console, "error").value((error: string) => {
errors.push(error)
})
const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough()
const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`],
{
stdin: process.stdin,
stdout: stream,
stderr: stream,
},
)
stream.end()
assert.strictEqual(exitCode, 1)
const messagesTable = [
[
chalk.red("error"),
"The character encoding was not declared. Proceeding using “windows-1252”.",
"",
],
[
chalk.yellow("warning"),
"Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.",
"line: 2, column: 16-7",
],
]
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") + " HTML validation (W3C) failed!",
),
true,
errors.join("\n"),
)
assert.strictEqual(
consoleErrorSpy.calledWith(table(messagesTable)),
true,
errors.join("\n"),
)
},
)
await t.test("fails with not found config", async () => {
const workingDirectory = path.join(FIXTURES_PATH, "error-config-not-found")
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` No config file found at ${configPath}. Please create "${CONFIG_FILE_NAME}".` ` No config file found at ${configPath}. Please create "${CONFIG_FILE_NAME}".`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid JSON config', async () => { await t.test("fails with invalid JSON config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-json' "error-config-invalid-json",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please check the JSON syntax.` ` Invalid config file at "${configPath}". Please check the JSON syntax.`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid URLs config', async () => { await t.test("fails with invalid URLs config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-urls' "error-config-invalid-urls",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please include an array of URLs.` ` Invalid config file at "${configPath}". Please include an array of URLs.`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid files config', async () => { await t.test("fails with invalid files config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-files' "error-config-invalid-files",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please include an array of files.` ` Invalid config file at "${configPath}". Please include an array of files.`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid files and urls config', async () => { await t.test("fails with invalid files and urls config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-files-and-urls' "error-config-invalid-files-and-urls",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add URLs or files.` ` Invalid config file at "${configPath}". Please add URLs or files.`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid severities config', async () => { await t.test("fails with invalid severities config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-severities' "error-config-invalid-severities",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join( ` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
', ' ", ",
)}).` )}).`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid empty severities config', async () => { await t.test("fails with invalid empty severities config", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-config-invalid-severities-empty' "error-config-invalid-severities-empty",
) )
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME) const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join( ` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
', ' ", ",
)}).` )}).`,
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid files paths to check', async () => { await t.test("fails with invalid files paths to check", async () => {
const workingDirectory = path.join( const workingDirectory = path.join(
FIXTURES_PATH, FIXTURES_PATH,
'error-invalid-files-paths-to-check' "error-invalid-files-paths-to-check",
) )
const htmlPath = path.resolve(workingDirectory, 'index.html') const htmlPath = path.resolve(workingDirectory, "index.html")
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
const messagesTable = [ const messagesTable = [
[`No file found at "${htmlPath}". Please check the path.`] [`No file found at "${htmlPath}". Please check the path.`],
] ]
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + ' HTML validation (W3C) failed!' chalk.bold.red("Error:") + " HTML validation (W3C) failed!",
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith(table(messagesTable)), consoleErrorSpy.calledWith(table(messagesTable)),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
await t.test('fails with invalid W3C HTML', async () => { await t.test("fails with invalid W3C HTML", async () => {
const workingDirectory = path.join(FIXTURES_PATH, 'error-invalid-w3c-html') const workingDirectory = path.join(FIXTURES_PATH, "error-invalid-w3c-html")
const errors: string[] = [] const errors: string[] = []
sinon.stub(console, 'error').value((error: string) => { sinon.stub(console, "error").value((error: string) => {
errors.push(error) errors.push(error)
}) })
const consoleErrorSpy = sinon.spy(console, 'error') const consoleErrorSpy = sinon.spy(console, "error")
const stream = new PassThrough() const stream = new PassThrough()
const exitCode = await cli.run( const exitCode = await cli.run(
[`--current-working-directory=${workingDirectory}`], [`--current-working-directory=${workingDirectory}`],
{ {
stdin: process.stdin, stdin: process.stdin,
stdout: stream, stdout: stream,
stderr: stream stderr: stream,
} },
) )
stream.end() stream.end()
assert.strictEqual(exitCode, 1) assert.strictEqual(exitCode, 1)
const messagesTable = [ const messagesTable = [
[ [
chalk.yellow('warning'), chalk.yellow("warning"),
'Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.', "Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.",
'line: 2, column: 16-6' "line: 2, column: 16-6",
] ],
] ]
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith( consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + ' HTML validation (W3C) failed!' chalk.bold.red("Error:") + " HTML validation (W3C) failed!",
), ),
true, true,
errors.join('\n') errors.join("\n"),
) )
assert.strictEqual( assert.strictEqual(
consoleErrorSpy.calledWith(table(messagesTable)), consoleErrorSpy.calledWith(table(messagesTable)),
true, true,
errors.join('\n') errors.join("\n"),
) )
}) })
}) })

View File

@ -1,4 +1,5 @@
{ {
"$schema": "../../../../schema/schema.json",
"urls": [], "urls": [],
"files": [] "files": []
} }

View File

@ -1,3 +1,4 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": "Invalid" "files": "Invalid"
} }

View File

@ -1,4 +1,5 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": ["./index.html"], "files": ["./index.html"],
"severities": [] "severities": []
} }

View File

@ -1,4 +1,5 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": ["./index.html"], "files": ["./index.html"],
"severities": ["errors-invalid"] "severities": ["errors-invalid"]
} }

View File

@ -1,3 +1,4 @@
{ {
"$schema": "../../../../schema/schema.json",
"urls": "Invalid" "urls": "Invalid"
} }

View File

@ -1,3 +1,4 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": ["./index.html"] "files": ["./index.html"]
} }

View File

@ -1,3 +1,4 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": ["./build/index.html", "./build/about.html"] "files": ["./build/index.html", "./build/about.html"]
} }

View File

@ -0,0 +1,5 @@
{
"$schema": "../../../../schema/schema.json",
"files": ["./build/index.html"],
"severities": ["info", "warning", "error"]
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<title>Have We Stopped Killing Yet?</title>
<link rel=icon href=favicon.ico>
<link rel=stylesheet href=setup/default.css>
<meta name=viewport content="initial-scale=1,width=device-width">
<meta property="og:image" content="https://mirrors.meiert.org/havewestoppedkillingyet.org/media/redrum.png">
<h1>Have We Stopped Killing Yet?</h1>
<p><strong>No.</strong>
<p>This is <a href=https://meiert.com/en/blog/on-taking-life/>unacceptable</a>.
<p>We dont need arms and armies and industrial slaughterhouses.
<p>We need respect for life.
<p>Everyone can <em>do something</em>. Speaking up is doing something. <a href=https://meiert.com/en/><img src=https://hell.meiert.org/core/png/meiert-logo-80x80-alt.png alt="Jens Oliver Meiert"></a>

View File

@ -1,3 +1,4 @@
{ {
"$schema": "../../../../schema/schema.json",
"files": ["./build/index.html", "./build/about.html"] "files": ["./build/index.html", "./build/about.html"]
} }

View File

@ -1,12 +1,12 @@
import { Builtins, Cli } from 'clipanion' import { Builtins, Cli } from "clipanion"
import { HTMLValidatorCommand } from './HTMLValidatorCommand.js' import { HTMLValidatorCommand } from "./HTMLValidatorCommand.js"
import { packageJSON } from './packageJSON.js' import { packageJSON } from "./packageJSON.js"
export const cli = new Cli({ export const cli = new Cli({
binaryLabel: packageJSON.name, binaryLabel: packageJSON.name,
binaryName: packageJSON.name, binaryName: packageJSON.name,
binaryVersion: packageJSON.version binaryVersion: packageJSON.version,
}) })
cli.register(Builtins.HelpCommand) cli.register(Builtins.HelpCommand)
cli.register(Builtins.VersionCommand) cli.register(Builtins.VersionCommand)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Cli } from 'clipanion' import { Cli } from "clipanion"
import { cli } from './cli.js' import { cli } from "./cli.js"
const [, , ...arguments_] = process.argv const [, , ...arguments_] = process.argv

View File

@ -1,5 +1,5 @@
import { readPackage } from 'read-pkg' import { readPackage } from "read-pkg"
export const packageJSON = await readPackage({ export const packageJSON = await readPackage({
cwd: new URL('..', import.meta.url) cwd: new URL("..", import.meta.url),
}) })

View File

@ -1,26 +0,0 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import fsMock from 'mock-fs'
import { isExistingPath } from '../isExistingPath.js'
await test('utils/isExistingPath', async (t) => {
t.afterEach(() => {
fsMock.restore()
})
await t.test('should return true if the file exists', async () => {
fsMock({
'/file.txt': ''
})
assert.strictEqual(await isExistingPath('/file.txt'), true)
})
await t.test("should return false if the file doesn't exists", async () => {
fsMock({
'/file.txt': ''
})
assert.strictEqual(await isExistingPath('/randomfile.txt'), false)
})
})

View File

@ -1,10 +0,0 @@
import fs from 'node:fs'
export const isExistingPath = async (path: string): Promise<boolean> => {
try {
await fs.promises.access(path, fs.constants.F_OK)
return true
} catch {
return false
}
}

46
src/validateHTML.ts Normal file
View File

@ -0,0 +1,46 @@
export interface ValidateHTMLOptions {
htmlData: string
}
export interface ValidationMessage {
type: "error" | "info" | "non-document-error"
subType?: "warning" | "fatal" | "internal" | "io" | "schema"
message: string
extract?: string
lastLine?: number
firstColumn?: number
lastColumn?: number
}
export interface ValidateHTMLResult {
messages: ValidationMessage[]
}
export const validateHTML = async (
options: ValidateHTMLOptions,
): Promise<ValidateHTMLResult> => {
const { htmlData } = options
const url = new URL("https://validator.w3.org/nu/")
url.searchParams.set("out", "json")
const response = await fetch(url, {
method: "POST",
body: htmlData,
headers: {
"Content-Type": "text/html",
},
})
if (!response.ok) {
throw new Error(response.statusText)
}
const result = (await response.json()) as ValidateHTMLResult
return result
}
export const getHTMLFromURL = async (url: string): Promise<string> => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch HTML from "${url}"`)
}
const html = await response.text()
return html
}

View File

@ -2,12 +2,13 @@
"extends": "@tsconfig/strictest/tsconfig.json", "extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"moduleResolution": "node", "module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build", "outDir": "./build",
"rootDir": "./src", "rootDir": "./src",
"noEmit": true, "noEmit": true,
"exactOptionalPropertyTypes": false "exactOptionalPropertyTypes": false,
"checkJs": false
} }
} }