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

15 Commits
v1.5.1 ... main

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
36 changed files with 4498 additions and 6459 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

@ -6,8 +6,8 @@ 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

@ -4,16 +4,16 @@ 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@v4.1.1" - uses: "actions/checkout@v4.2.2"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.0.1" uses: "actions/setup-node@v4.1.0"
with: with:
node-version: "lts/*" node-version: "lts/*"
cache: "npm" cache: "npm"
@ -22,6 +22,6 @@ jobs:
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

@ -4,16 +4,16 @@ 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@v4.1.1" - uses: "actions/checkout@v4.2.2"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.0.1" uses: "actions/setup-node@v4.1.0"
with: with:
node-version: "lts/*" node-version: "lts/*"
cache: "npm" cache: "npm"
@ -21,8 +21,6 @@ jobs:
- 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

@ -2,7 +2,7 @@ name: "Release"
on: on:
push: push:
branches: [master] branches: [main, beta]
jobs: jobs:
release: release:
@ -13,13 +13,13 @@ jobs:
pull-requests: "write" pull-requests: "write"
id-token: "write" id-token: "write"
steps: steps:
- uses: "actions/checkout@v4.1.1" - 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@v4.0.1" uses: "actions/setup-node@v4.1.0"
with: with:
node-version: "lts/*" node-version: "lts/*"
cache: "npm" cache: "npm"
@ -28,15 +28,15 @@ jobs:
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

@ -4,16 +4,16 @@ 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@v4.1.1" - uses: "actions/checkout@v4.2.2"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.0.1" uses: "actions/setup-node@v4.1.0"
with: with:
node-version: "lts/*" node-version: "lts/*"
cache: "npm" cache: "npm"
@ -22,7 +22,7 @@ jobs:
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,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"
] ]

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": "explicit"
},
"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,9 +1,7 @@
<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">
@ -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.1", "serve": "14.2.4",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.8",
"html-w3c-validator": "file:.." "html-w3c-validator": "file:.."
} }
} }

10008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "html-w3c-validator", "name": "html-w3c-validator",
"version": "0.0.0-development", "version": "0.0.0-development",
"description": "CLI for validating multiple html pages using validator.w3.org.", "description": "CLI for validating HTML pages using validator.w3.org.",
"public": true, "public": true,
"types": "module", "types": "module",
"type": "module", "type": "module",
@ -25,6 +25,7 @@
"bin": "build/index.js", "bin": "build/index.js",
"files": [ "files": [
"build", "build",
"./schema/schema.json",
"!**/*.test.js", "!**/*.test.js",
"!**/*.map" "!**/*.map"
], ],
@ -33,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": "6.0.0",
"ora": "8.0.1", "ora": "8.1.1",
"read-pkg": "9.0.1", "read-pkg": "9.0.1",
"table": "6.8.1" "table": "6.8.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "18.4.3", "@swc/cli": "0.5.0",
"@commitlint/config-conventional": "18.4.3", "@swc/core": "1.9.1",
"@swc/cli": "0.1.63", "@tsconfig/strictest": "2.0.5",
"@swc/core": "1.3.101", "@types/node": "22.9.0",
"@tsconfig/strictest": "2.0.2", "@types/sinon": "17.0.3",
"@types/html-validator": "5.0.6", "@typescript-eslint/eslint-plugin": "8.13.0",
"@types/mock-fs": "4.13.4", "@typescript-eslint/parser": "8.13.0",
"@types/node": "20.10.5", "editorconfig-checker": "6.0.0",
"@types/sinon": "17.0.2", "eslint": "8.57.1",
"@typescript-eslint/eslint-plugin": "6.16.0", "eslint-config-conventions": "16.0.1",
"@typescript-eslint/parser": "6.16.0", "eslint-plugin-promise": "7.1.0",
"editorconfig-checker": "5.1.2", "eslint-plugin-unicorn": "55.0.0",
"eslint": "8.56.0", "execa": "9.5.1",
"eslint-config-conventions": "13.1.0", "prettier": "3.3.3",
"eslint-config-prettier": "9.1.0", "semantic-release": "23.1.1",
"eslint-plugin-import": "2.29.1", "serve": "14.2.4",
"eslint-plugin-prettier": "5.1.2", "sinon": "19.0.2",
"eslint-plugin-promise": "6.1.1", "typescript": "5.6.3"
"eslint-plugin-unicorn": "50.0.1",
"execa": "8.0.1",
"lint-staged": "15.2.0",
"markdownlint-cli2": "0.11.0",
"markdownlint-rule-relative-links": "2.1.2",
"mock-fs": "5.2.0",
"prettier": "3.1.1",
"rimraf": "5.0.5",
"semantic-release": "22.0.12",
"serve": "14.2.1",
"sinon": "17.0.1",
"typescript": "5.3.3"
} }
} }

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,19 +1,15 @@
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 * as typanion from "typanion"
import chalk from "chalk" import chalk from "chalk"
import ora from "ora" import { Command, Option } from "clipanion"
import logSymbols from "log-symbols" import logSymbols from "log-symbols"
import type { import ora from "ora"
ValidationMessageLocationObject,
ParsedJsonAsValidationResults,
} from "html-validator"
import validateHTML from "html-validator"
import { table } from "table" import { table } from "table"
import * as typanion from "typanion"
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"
@ -50,7 +46,7 @@ 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(
@ -65,22 +61,20 @@ export class HTMLValidatorCommand extends Command {
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.`,
) )
@ -137,29 +131,25 @@ 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,
}
let result: ParsedJsonAsValidationResults | undefined
if (type === "url") { 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")
@ -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)
} }

View File

@ -1,7 +1,9 @@
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 fs from "node:fs"
import { PassThrough } from "node:stream" 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"
@ -33,10 +35,13 @@ await test("html-w3c-validator", async (t) => {
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(
path.join(fileURLToPath(exampleURL), "node_modules"),
{ recursive: true, force: true },
)
await execa("npm", ["install"]) await execa("npm", ["install"])
const { exitCode } = await execa("npm", [ const { exitCode } = await execa("node", [
"run", "--run",
"test:html-w3c-validator", "test:html-w3c-validator",
]) ])
assert.strictEqual(exitCode, 0) assert.strictEqual(exitCode, 0)
@ -47,8 +52,8 @@ await test("html-w3c-validator", async (t) => {
"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()
@ -58,7 +63,7 @@ await test("html-w3c-validator", async (t) => {
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,
@ -83,8 +88,8 @@ await test("html-w3c-validator", async (t) => {
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()
@ -97,7 +102,7 @@ await test("html-w3c-validator", async (t) => {
}, },
) )
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,
@ -111,6 +116,53 @@ await test("html-w3c-validator", async (t) => {
}, },
) )
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 () => { await t.test("fails with not found config", async () => {
const workingDirectory = path.join(FIXTURES_PATH, "error-config-not-found") 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)

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,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
}