1
1
mirror of https://github.com/theoludwig/html-w3c-validator.git synced 2024-12-08 00:45:37 +01:00

Compare commits

..

No commits in common. "develop" and "v1.2.0" have entirely different histories.

64 changed files with 23567 additions and 8807 deletions

1
.commitlintrc.json Normal file
View File

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

View File

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

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. Please provide a clear and concise description of what the bug is. Include
Include screenshots if needed. Please make sure your issue has not already been fixed. 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,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
# What changes this PR introduce? ## What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

View File

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

View File

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

View File

@ -1,42 +1,32 @@
name: "Release" name: 'Release'
on: on:
push: push:
branches: [main, beta] branches: [master]
jobs: jobs:
release: release:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
permissions:
contents: "write"
issues: "write"
pull-requests: "write"
id-token: "write"
steps: steps:
- uses: "actions/checkout@v4.2.2" - uses: 'actions/checkout@v3.0.0'
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v4.1.0" uses: 'actions/setup-node@v3.1.0'
with: with:
node-version: "lts/*" node-version: 'lts/*'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "Build" - name: 'Build'
run: "node --run build" run: 'npm run build'
- run: "node --run build:typescript" - name: 'Release'
run: 'npm run release'
- name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies"
run: "npm audit signatures"
- name: "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: [main, develop] branches: [master, develop]
jobs: jobs:
test: test:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
steps: steps:
- uses: "actions/checkout@v4.2.2" - uses: 'actions/checkout@v3.0.0'
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v4.1.0" uses: 'actions/setup-node@v3.1.0'
with: with:
node-version: "lts/*" node-version: 'lts/*'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "Build" - name: 'Build'
run: "node --run build" run: 'npm run build'
- name: "Test" - name: 'Test'
run: "node --run test" run: 'npm run test'

View File

@ -1,3 +0,0 @@
{
"files": ["./example/build/index.html", "./example/build/about.html"]
}

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:commit -- --edit

5
.husky/pre-commit Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged
npm run build

6
.lintstagedrc.json Normal file
View File

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

6
.markdownlint.json Normal file
View File

@ -0,0 +1,6 @@
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}

3
.npmrc
View File

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

View File

@ -1 +0,0 @@
fixtures

View File

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

View File

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

15
.swcrc
View File

@ -1,13 +1,22 @@
{ {
"sourceMaps": true,
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"decorators": true,
"dynamicImport": true "dynamicImport": true
}, },
"target": "esnext" "transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2022",
"loose": true
}, },
"module": { "module": {
"type": "es6" "type": "es6",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
} }
} }

9
.taprc Normal file
View File

@ -0,0 +1,9 @@
ts: false
jsx: false
flow: false
check-coverage: false
coverage: false
timeout: 120000
files:
- 'build/**/*.test.js'

9
.vscode/extensions.json vendored Normal file
View File

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

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"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
}
}

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
<contact@theoludwig.fr>. contact@divlo.fr.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -2,14 +2,6 @@
Thanks a lot for your interest in contributing to **html-w3c-validator**! 🎉 Thanks a lot for your interest in contributing to **html-w3c-validator**! 🎉
## Code of Conduct
**html-w3c-validator** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
## Open Development
All work on **html-w3c-validator** happens directly on this repository. Both core team members and external contributors send pull requests which go through the same review process.
## Types of contributions ## Types of contributions
- Reporting a bug. - Reporting a bug.
@ -19,7 +11,7 @@ All work on **html-w3c-validator** happens directly on this repository. Both cor
## Pull Requests ## Pull Requests
- **Please first discuss** the change you wish to make via [issue](https://github.com/theoludwig/html-w3c-validator/issues) before making a change. It might avoid a waste of your time. - **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/html-w3c-validator/issues) before making a change. It might avoid a waste of your time.
- Ensure your code respect linting. - Ensure your code respect linting.
@ -29,4 +21,26 @@ If you're adding new features to **html-w3c-validator**, please include tests.
## Commits ## Commits
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases. The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.

View File

@ -1,6 +1,6 @@
# MIT License MIT License
Copyright (c) Théo LUDWIG <contact@theoludwig.fr> Copyright (c) Divlo
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,17 +1,19 @@
<h1 align="center">html-w3c-validator</h1> <h1 align="center">html-w3c-validator</h1>
<p align="center"> <p align="center">
<strong>CLI for validating HTML pages using <a href="https://validator.w3.org/">validator.w3.org</a>.</strong> <strong>CLI for validating multiple 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" alt="CONTRIBUTING" /></a> <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></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" alt="Build" /></a> <a href="https://github.com/Divlo/html-w3c-validator/actions/workflows/build.yml"><img src="https://github.com/Divlo/html-w3c-validator/actions/workflows/build.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/Divlo/html-w3c-validator/actions/workflows/lint.yml"><img src="https://github.com/Divlo/html-w3c-validator/actions/workflows/lint.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> <a href="https://github.com/Divlo/html-w3c-validator/actions/workflows/test.yml"><img src="https://github.com/Divlo/html-w3c-validator/actions/workflows/test.yml/badge.svg?branch=develop" /></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>
@ -20,11 +22,11 @@
## 📜 About ## 📜 About
**html-w3c-validator** is a CLI tool to validate HTML pages using [validator.w3.org](https://validator.w3.org/). **html-w3c-validator** is a CLI tool to validate multiple 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 is HTML page validation important? ### Why should I validate my HTML pages?
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):
@ -38,7 +40,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))
@ -57,8 +59,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://localhost:3000\" \"html-w3c-validator\""
}, }
} }
``` ```
@ -66,30 +68,24 @@ npm install --save-dev html-w3c-validator start-server-and-test
```jsonc ```jsonc
{ {
"$schema": "./node_modules/html-w3c-validator/schema/schema.json", "urls": ["http://localhost:3000/", "http://localhost:3000/about"],
// URLs to validate. // You can also specify HTML files instead of URLs
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"], "files": ["./index.html", "./about.html"]
// Files to validate.
"files": ["./index.html", "./about.html"],
// Specify the severities of the validator messages to report. (default: ["warning", "error"])
"severities": ["info", "warning", "error"],
} }
``` ```
## Usage ## Usage
```sh ```sh
node --run test:html-w3c-validator npm run test:html-w3c-validator
``` ```
Example of output (in case of success): Example of output (in case of success):
```txt ```txt
✔ Validating http://127.0.0.1:3000/ ✔ Validating http://localhost:3000/
✔ Validating http://127.0.0.1:3000/about ✔ Validating http://localhost:3000/about
Success: HTML validation (W3C) passed! 🎉 Success: HTML validation (W3C) passed! 🎉
``` ```
@ -99,7 +95,6 @@ See the [./example](./example) folder for practical usage.
### Options ### Options
```text ```text
--current-working-directory <path> The current working directory (default: `process.cwd()`).
-V, --version Output the version number. -V, --version Output the version number.
-h, --help Display help for command. -h, --help Display help for command.
``` ```

View File

@ -1,5 +1,4 @@
{ {
"$schema": "./node_modules/html-w3c-validator/schema/schema.json", "urls": ["http://localhost:3000/", "http://localhost: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"]
} }

View File

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View File

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

3289
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,12 @@
"name": "example", "name": "example",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "serve ./build", "start": "serve \"./build\"",
"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://localhost:3000\" \"html-w3c-validator\""
}, },
"devDependencies": { "devDependencies": {
"serve": "14.2.4", "serve": "13.0.2",
"start-server-and-test": "2.0.8", "start-server-and-test": "1.14.0",
"html-w3c-validator": "file:.." "html-w3c-validator": "file:.."
} }
} }

27558
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,19 @@
{ {
"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,
"types": "module", "description": "CLI for validating multiple html pages using validator.w3.org.",
"type": "module", "type": "module",
"author": "Théo LUDWIG <contact@theoludwig.fr>", "author": "Divlo <contact@divlo.fr>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/theoludwig/html-w3c-validator.git" "url": "https://github.com/Divlo/html-w3c-validator.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/theoludwig/html-w3c-validator/issues" "url": "https://github.com/Divlo/html-w3c-validator/issues"
}, },
"homepage": "https://github.com/theoludwig/html-w3c-validator#readme", "homepage": "https://github.com/Divlo/html-w3c-validator#readme",
"keywords": [ "keywords": [
"cli", "cli",
"html-validator", "html-validator",
@ -23,55 +22,64 @@
], ],
"main": "build/index.js", "main": "build/index.js",
"bin": "build/index.js", "bin": "build/index.js",
"files": [
"build",
"./schema/schema.json",
"!**/*.test.js",
"!**/*.map"
],
"publishConfig": {
"access": "public",
"provenance": true
},
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0",
"npm": ">=8.0.0"
}, },
"files": [
"build"
],
"scripts": { "scripts": {
"build": "swc ./src --out-dir ./build --strip-leading-paths --delete-dir-on-start", "build": "rimraf ./build && swc ./src --out-dir ./build && tsc",
"build:typescript": "tsc", "lint:commit": "commitlint",
"start": "node --enable-source-maps build/index.js",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore", "lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"",
"lint:prettier": "prettier . --check", "lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
"test": "node --enable-source-maps --test", "lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
"release": "semantic-release" "lint:staged": "lint-staged",
"test": "tap",
"release": "semantic-release",
"postinstall": "husky install",
"prepublishOnly": "pinst --disable",
"postpublish": "pinst --enable"
}, },
"dependencies": { "dependencies": {
"chalk": "5.3.0", "chalk": "5.0.1",
"clipanion": "3.2.1", "clipanion": "3.1.0",
"log-symbols": "6.0.0", "html-validator": "6.0.0",
"ora": "8.1.1", "ora": "6.1.0",
"read-pkg": "9.0.1", "read-pkg": "7.1.0",
"table": "6.8.2" "table": "6.8.0"
}, },
"devDependencies": { "devDependencies": {
"@swc/cli": "0.5.0", "@commitlint/cli": "16.2.3",
"@swc/core": "1.9.1", "@commitlint/config-conventional": "16.2.1",
"@tsconfig/strictest": "2.0.5", "@swc/cli": "0.1.57",
"@types/node": "22.9.0", "@swc/core": "1.2.163",
"@types/sinon": "17.0.3", "@types/html-validator": "5.0.2",
"@typescript-eslint/eslint-plugin": "8.13.0", "@types/mock-fs": "4.13.1",
"@typescript-eslint/parser": "8.13.0", "@types/node": "17.0.23",
"editorconfig-checker": "6.0.0", "@types/tap": "15.0.6",
"eslint": "8.57.1", "@typescript-eslint/eslint-plugin": "5.18.0",
"eslint-config-conventions": "16.0.1", "editorconfig-checker": "4.0.2",
"eslint-plugin-promise": "7.1.0", "eslint": "8.12.0",
"eslint-plugin-unicorn": "55.0.0", "eslint-config-conventions": "2.0.0",
"execa": "9.5.1", "eslint-config-prettier": "8.5.0",
"prettier": "3.3.3", "eslint-plugin-import": "2.26.0",
"semantic-release": "23.1.1", "eslint-plugin-prettier": "4.0.0",
"serve": "14.2.4", "eslint-plugin-promise": "6.0.0",
"sinon": "19.0.2", "eslint-plugin-unicorn": "42.0.0",
"typescript": "5.6.3" "execa": "6.1.0",
"husky": "7.0.4",
"lint-staged": "12.3.7",
"markdownlint-cli": "0.31.1",
"mock-fs": "5.1.2",
"pinst": "3.0.0",
"prettier": "2.6.2",
"rimraf": "3.0.2",
"semantic-release": "19.0.2",
"serve": "13.0.2",
"tap": "16.0.1",
"typescript": "4.6.3"
} }
} }

View File

@ -1,48 +0,0 @@
{
"$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,26 +1,23 @@
import fs from "node:fs" import path from 'node:path'
import path from "node:path" import fs from 'node:fs'
import chalk from "chalk" import { Command } from 'clipanion'
import { Command, Option } from "clipanion" import chalk from 'chalk'
import logSymbols from "log-symbols" import ora from 'ora'
import ora from "ora" import validateHTML, {
import { table } from "table" ValidationMessageLocationObject,
import * as typanion from "typanion" ParsedJsonAsValidationResults
} from 'html-validator'
import { table } from 'table'
import type { ValidateHTMLResult } from "./validateHTML.js" import { isExistingPath } from './utils/isExistingPath.js'
import { getHTMLFromURL, validateHTML } from "./validateHTML.js"
export const CONFIG_FILE_NAME = ".html-w3c-validatorrc.json" const CURRENT_DIRECTORY = process.cwd()
const CONFIG_FILE_NAME = '.html-w3c-validatorrc.json'
export const SEVERITIES = ["error", "warning", "info"] as const
export type Severity = (typeof SEVERITIES)[number]
interface Config { interface Config {
urls?: string[] urls?: string[]
files?: string[] files?: string[]
severities?: Severity[]
} }
interface Error { interface Error {
@ -28,173 +25,103 @@ interface Error {
messagesTable: string[][] messagesTable: string[][]
} }
interface Result {
data: string
isSuccess: boolean
}
const printResults = (results: Result[]): void => {
for (const result of results) {
if (result.isSuccess) {
console.log(logSymbols.success, result.data)
} else {
console.log(logSymbols.error, result.data)
}
}
}
export class HTMLValidatorCommand extends Command { export class HTMLValidatorCommand extends Command {
static override usage = { static usage = {
description: description:
"CLI for validating HTML pages using <https://validator.w3.org/>.", 'CLI for validating multiple html pages using <https://validator.w3.org/>.'
} }
public currentWorkingDirectory = Option.String( async execute(): Promise<number> {
"--current-working-directory", const configPath = path.join(CURRENT_DIRECTORY, CONFIG_FILE_NAME)
process.cwd(),
{
description: "The current working directory.",
validator: typanion.isString(),
},
)
public async execute(): Promise<number> {
const configPath = path.join(this.currentWorkingDirectory, CONFIG_FILE_NAME)
try { try {
let configData: string if (!(await isExistingPath(configPath))) {
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 {
throw new Error( isValidConfig = false
`Invalid config file at "${configPath}". Please check the JSON syntax.`,
)
} }
if (config.urls != null && !Array.isArray(config.urls)) { isValidConfig =
isValidConfig &&
(Array.isArray(config.urls) || Array.isArray(config.urls))
if (!isValidConfig) {
throw new Error( throw new Error(
`Invalid config file at "${configPath}". Please include an array of URLs.`, `Invalid config file at ${configPath}. Please check the syntax.`
)
}
if (config.files != null && !Array.isArray(config.files)) {
throw new Error(
`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) {
throw new Error(
`Invalid config file at "${configPath}". Please add URLs or files.`,
)
}
const severities: Severity[] = config.severities ?? ["warning", "error"]
for (const severity of severities) {
if (!SEVERITIES.includes(severity)) {
throw new Error(
`Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
", ",
)}).`,
)
}
}
if (severities.length === 0) {
throw new Error(
`Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
", ",
)}).`,
)
}
const errors: Error[] = [] const errors: Error[] = []
let isValid = true let isValid = true
const loader = ora(`Validating HTML (W3C)...`).start() for (const { data, type } of dataToValidate) {
const results: Result[] = [] const loader = ora(`Validating ${data}`).start()
await Promise.all(
dataToValidate.map(async ({ data, type }) => {
try { try {
let result: ValidateHTMLResult | undefined const options = {
if (type === "url") { format: 'json' as 'json' | undefined
}
let result: ParsedJsonAsValidationResults | undefined
if (type === 'url') {
result = await validateHTML({ result = await validateHTML({
htmlData: await getHTMLFromURL(data), url: data,
isLocal: true,
...options
}) })
} else if (type === "file") { } else if (type === 'file') {
const htmlPath = path.resolve(this.currentWorkingDirectory, data) const htmlPath = path.resolve(CURRENT_DIRECTORY, data)
let html: string if (!(await isExistingPath(htmlPath))) {
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({
htmlData: html, data: html,
...options
}) })
} else { } else {
throw new Error("Invalid type") throw new Error('Invalid type')
} }
const hasErrors = result.messages.some((message) => { const isValidHTML = result.messages.length === 0
return ( if (isValidHTML) {
severities.includes(message.type as Severity) || loader.succeed()
severities.includes(message.subType as Severity)
)
})
if (!hasErrors) {
results.push({ data, isSuccess: true })
} else { } else {
results.push({ data, isSuccess: false }) loader.fail()
const messagesTable: string[][] = [] const messagesTable: string[][] = []
for (const message of result.messages) { for (const message of result.messages) {
if (
!severities.includes(message.type as Severity) &&
!severities.includes(message.subType as Severity)
) {
continue
}
const row: string[] = [] const row: string[] = []
if (message.type === "info") { if (message.type === 'error') {
if (message.subType === "warning") {
row.push(chalk.yellow(message.subType))
} else {
row.push(chalk.blue(message.type))
}
} else {
row.push(chalk.red(message.type)) row.push(chalk.red(message.type))
} else {
row.push(chalk.yellow(message.type))
} }
row.push(message.message) row.push(message.message)
if ( const violation = message as ValidationMessageLocationObject
message.extract != null && if (violation.extract != null) {
message.lastLine != null &&
message.firstColumn != null &&
message.lastColumn != null
) {
row.push( row.push(
`line: ${message.lastLine}, column: ${message.firstColumn}-${message.lastColumn}`, `line: ${violation.lastLine}, column: ${violation.firstColumn}-${violation.lastColumn}`
) )
} else {
row.push("")
} }
messagesTable.push(row) messagesTable.push(row)
} }
@ -202,36 +129,35 @@ export class HTMLValidatorCommand extends Command {
isValid = false isValid = false
} }
} catch (error) { } catch (error) {
loader.fail()
isValid = false isValid = false
if (error instanceof Error) { if (error instanceof Error) {
const messagesTable: string[][] = [[error.message]] const messagesTable: string[][] = [[error.message]]
errors.push({ data, messagesTable }) errors.push({ data, messagesTable })
} }
} }
}), }
)
if (!isValid) { if (!isValid) {
loader.fail()
printResults(results)
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( console.log()
`${chalk.bold.green("Success:")} HTML validation (W3C) passed! 🎉`, console.log(
`${chalk.bold.green('Success:')} HTML validation (W3C) passed! 🎉`
) )
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,468 +1,21 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import { execa } from 'execa'
import path from "node:path"
import fs from "node:fs"
import { PassThrough } from "node:stream"
import { fileURLToPath } from "node:url"
import sinon from "sinon" import { cli } from '../cli.js'
import { execa } from "execa" import { HTMLValidatorCommand } from '../HTMLValidatorCommand.js'
import { table } from "table"
import chalk from "chalk"
import logSymbols from "log-symbols"
import { cli } from "../cli.js" await tap.test('html-w3c-validator', async (t) => {
import { await t.test('should be instance of the command', async (t) => {
HTMLValidatorCommand,
CONFIG_FILE_NAME,
SEVERITIES,
} from "../HTMLValidatorCommand.js"
const FIXTURES_PATH = path.join(process.cwd(), "src", "__test__", "fixtures")
await test("html-w3c-validator", async (t) => {
t.afterEach(() => {
sinon.restore()
})
await t.test("should be instance of the command", async () => {
const command = cli.process([]) const command = cli.process([])
assert(command instanceof HTMLValidatorCommand) t.equal(command instanceof HTMLValidatorCommand, true)
}) })
await t.test( await t.test('succeeds and validate the html correctly', async (t) => {
"succeeds and validate the html correctly (example)", const exampleURL = new URL('../../example', import.meta.url)
async () => {
const exampleURL = new URL("../../example", import.meta.url)
process.chdir(exampleURL.pathname) process.chdir(exampleURL.pathname)
await fs.promises.rm( await execa('rimraf', ['node_modules'])
path.join(fileURLToPath(exampleURL), "node_modules"), await execa('npm', ['install'])
{ recursive: true, force: true }, const { exitCode } = await execa('npm', ['run', 'test:html-w3c-validator'])
) t.equal(exitCode, 0)
await execa("npm", ["install"])
const { exitCode } = await execa("node", [
"--run",
"test:html-w3c-validator",
])
assert.strictEqual(exitCode, 0)
},
)
await t.test(
"succeeds and validate the html correctly (example without working directory)",
async () => {
const logs: string[] = []
sinon.stub(console, "log").value((...log: string[]) => {
logs.push(...log)
})
const consoleLogSpy = sinon.spy(console, "log")
const stream = new PassThrough()
const exitCode = await cli.run([], {
stdin: process.stdin,
stdout: stream,
stderr: stream,
})
stream.end()
assert.strictEqual(exitCode, 0, logs.join("\n"))
assert.strictEqual(
consoleLogSpy.calledWith(
logSymbols.success,
"./example/build/index.html",
),
true,
logs.join("\n"),
)
assert.strictEqual(
consoleLogSpy.calledWith(
logSymbols.success,
"./example/build/about.html",
),
true,
logs.join("\n"),
)
},
)
await t.test(
"succeeds and validate the html correctly (fixture)",
async () => {
const workingDirectory = path.join(FIXTURES_PATH, "success")
const logs: string[] = []
sinon.stub(console, "log").value((...log: string[]) => {
logs.push(...log)
})
const consoleLogSpy = sinon.spy(console, "log")
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, 0, logs.join("\n"))
assert.strictEqual(
consoleLogSpy.calledWith(logSymbols.success, "./build/index.html"),
true,
logs.join("\n"),
)
assert.strictEqual(
consoleLogSpy.calledWith(logSymbols.success, "./build/about.html"),
true,
logs.join("\n"),
)
},
)
await t.test(
"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 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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` No config file found at ${configPath}. Please create "${CONFIG_FILE_NAME}".`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid JSON config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-json",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please check the JSON syntax.`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid URLs config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-urls",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please include an array of URLs.`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid files config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-files",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please include an array of files.`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid files and urls config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-files-and-urls",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add URLs or files.`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid severities config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-severities",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
", ",
)}).`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid empty severities config", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-config-invalid-severities-empty",
)
const configPath = path.join(workingDirectory, CONFIG_FILE_NAME)
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)
assert.strictEqual(
consoleErrorSpy.calledWith(
chalk.bold.red("Error:") +
` Invalid config file at "${configPath}". Please add valid severities (${SEVERITIES.join(
", ",
)}).`,
),
true,
errors.join("\n"),
)
})
await t.test("fails with invalid files paths to check", async () => {
const workingDirectory = path.join(
FIXTURES_PATH,
"error-invalid-files-paths-to-check",
)
const htmlPath = path.resolve(workingDirectory, "index.html")
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 = [
[`No file found at "${htmlPath}". Please check the path.`],
]
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 invalid W3C HTML", async () => {
const workingDirectory = path.join(FIXTURES_PATH, "error-invalid-w3c-html")
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.yellow("warning"),
"Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.",
"line: 2, column: 16-6",
],
]
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"),
)
}) })
}) })

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body></body>
</html>

View File

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

View File

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body></body>
</html>

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About</title>
</head>
<body></body>
</html>

View File

@ -1,9 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body></body>
</html>

View File

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

View File

@ -1,12 +0,0 @@
<!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,4 +0,0 @@
{
"$schema": "../../../../schema/schema.json",
"files": ["./build/index.html", "./build/about.html"]
}

View File

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About</title>
</head>
<body></body>
</html>

View File

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body></body>
</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

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

View File

@ -0,0 +1,10 @@
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
}
}

View File

@ -1,46 +0,0 @@
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

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