Skip to content

Commit

Permalink
New --ignore-config flag to specify a rules file
Browse files Browse the repository at this point in the history
  • Loading branch information
dpilafian committed Sep 18, 2023
1 parent 79c6c5d commit 21349da
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 34 deletions.
40 changes: 29 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ You can also install **w3c-html-validator** globally (`--global`) and then run i

### 3. CLI flags
Command-line flags:
| Flag | Description | Value |
| ------------ | --------------------------------------------------------------- | ---------- |
| `--continue` | Report messages but do not throw an error if validation failed. | N/A |
| `--delay` | Debounce pause in milliseconds between each file validation. | **number** |
| `--exclude` | Comma separated list of strings to match in paths to skip. | **string** |
| `--ignore` | Skip messages containing a string or matching a RegEx. | **string** |
| `--note` | Place to add a comment only for humans. | **string** |
| `--quiet` | Suppress messages for successful validations. | N/A |
| `--trim` | Truncate validation messages to not exceed a maximum length. | **number** |
| Flag | Description | Value |
| ----------------- | --------------------------------------------------------------- | ---------- |
| `--continue` | Report messages but do not throw an error if validation failed. | N/A |
| `--delay` | Debounce pause in milliseconds between each file validation. | **number** |
| `--exclude` | Comma separated list of strings to match in paths to skip. | **string** |
| `--ignore` | Skip messages containing a string or matching a regex. | **string** |
| `--ignore-config` | File listing regex patterns of messages to skip. | **string** |
| `--note` | Place to add a comment only for humans. | **string** |
| `--quiet` | Suppress messages for successful validations. | N/A |
| `--trim` | Truncate validation messages to not exceed a maximum length. | **number** |

### 4. Example CLI usage
Examples:
Expand All @@ -71,10 +72,13 @@ Examples:
Allow the ugly slashes of self-closing tags despite XHTML being a hideous scourge on the web.

- `html-validator docs '--ignore=/^Duplicate ID/'`<br>
Use a RegEx (regular expression) to skip all validation messages that start with "Duplicate ID".
Use a regex (regular expression) to skip all validation messages that start with "Duplicate ID".

- `html-validator docs '--ignore=/^Duplicate ID|^Section lacks|^Element .blockquote. not allowed/'`<br>
Use a RegEx with "or" operators (`|`) to skip multiple validation messages.
Use a regex with "or" operators (`|`) to skip multiple validation messages.

- `html-validator docs --ignore-config=config/regex-patterns.txt`<br>
Similar to the pervious command but regex patterns are stored in a configuration file (see the _Ignore Configuration File_ section below).

- `html-validator --quiet`<br>
Suppress "pass" status messages.
Expand All @@ -85,6 +89,20 @@ Examples:
- `html-validator docs --trim=30 --continue`<br>
Truncate validation messages to 30 characters and do not abort CI if validation fails.

### 5. Ignore Configuration File
The optional `--ignore-config=FILENAME` flag specifies a configuration file with one regex pattern per line.&nbsp;
Empty lines and lines starting with a double slash (`//`) are considered comments.
Validation messages matching any of the regex patterns will be skipped.

Example configuration file with 3 regex patterns:
```config
// Ignore Config for w3c-html-validator
^Duplicate ID
^Element .blockquote. not allowed
^Element .style. not allowed
```

## D) Application Code and Testing Frameworks
In addition to the CLI interface, the **w3c-html-validator** package can also be imported and called directly in ESM and TypeScript projects.

Expand Down
31 changes: 21 additions & 10 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import { w3cHtmlValidator } from '../dist/w3c-html-validator.js';
import fs from 'fs';

// Parameters and flags
const validFlags = ['continue', 'delay', 'exclude', 'ignore', 'note', 'quiet', 'trim'];
const cli = cliArgvUtil.parse(validFlags);
const files = cli.params;
const ignore = cli.flagMap.ignore ?? null;
const delay = Number(cli.flagMap.delay) || 500; //default half second debounce pause
const trim = Number(cli.flagMap.trim) || null;
const validFlags = ['continue', 'delay', 'exclude', 'ignore', 'ignore-config', 'note', 'quiet', 'trim'];
const cli = cliArgvUtil.parse(validFlags);
const files = cli.params;
const ignore = cli.flagMap.ignore ?? null;
const ignoreConfig = cli.flagMap.ignoreConfig ?? null;
const delay = Number(cli.flagMap.delay) || 500; //default half second debounce pause
const trim = Number(cli.flagMap.trim) || null;

// Validator
const keep = (filename) => !filename.includes('node_modules/');
Expand All @@ -55,9 +56,19 @@ const reporterOptions = {
quiet: cli.flagOn.quiet,
maxMessageLen: trim,
};
const isRegex = /^\/.*\/$/; //starts and ends with a slash indicating it's a regex
const skip = isRegex.test(ignore) ? new RegExp(ignore.slice(1, -1)) : ignore;
const getIgnoreMessages = () => {
const toLines = (text) => text.replace(/\r/g, '').split('\n').map(line => line.trim());
const readLines = (file) => toLines(fs.readFileSync(file).toString());
const notComment = (line) => line.length > 1 && !line.startsWith('//');
const lines = ignoreConfig ? readLines(ignoreConfig).filter(notComment) : [];
const regexes = lines.map(line => new RegExp(line));
const isRegex = /^\/.*\/$/; //starts and ends with a slash indicating it's a regex
if (isRegex.test(ignore))
regexes.push(new RegExp(ignore.slice(1, -1)));
return regexes.length ? regexes : ignore;
};
const handleReport = (report) => w3cHtmlValidator.reporter(report, reporterOptions);
const options = (filename) => ({ filename: filename, ignoreMessages: skip });
const options = (filename) => ({ filename: filename, ignoreMessages: getIgnoreMessages() });
const getReport = (filename) => w3cHtmlValidator.validate(options(filename)).then(handleReport);
filenames.forEach((filename, i) => globalThis.setTimeout(() => getReport(filename), i * delay));
const processFile = (filename, i) => globalThis.setTimeout(() => getReport(filename), i * delay);
filenames.forEach(processFile);
5 changes: 5 additions & 0 deletions spec/ignore-config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Ignore Config for w3c-html-validator

^Duplicate ID
^Element .blockquote. not allowed
^Element .style. not allowed
6 changes: 6 additions & 0 deletions spec/mocha.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,10 @@ describe('Executing the CLI', () => {
assertDeepStrictEqual(actual, expected);
});

it('skips validation message matching --ignore and --ignore-config regex patterns', () => {
const actual = run('html-validator spec/html "--ignore=/^Section lacks heading/" --ignore-config=spec/ignore-config.txt');
const expected = null;
assertDeepStrictEqual(actual, expected);
});

});
27 changes: 14 additions & 13 deletions w3c-html-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import request from 'superagent';

// Type Declarations
export type ValidatorSettings = {
html: string, //example: '<!doctype html><html><head><title>Home</title></html>''
filename: string, //example: 'docs/index.html'
website: string, //example: 'https://pretty-print-json.js.org/'
checkUrl: string, //default: 'https://validator.w3.org/nu/'
ignoreLevel: 'info' | 'warning', //skip unwanted messages ('warning' also skips 'info')
ignoreMessages: string | RegExp, //matcher to skip unwanted messages
html: string, //example: '<!doctype html><html><head><title>Home</title></html>''
filename: string, //example: 'docs/index.html'
website: string, //example: 'https://pretty-print-json.js.org/'
checkUrl: string, //default: 'https://validator.w3.org/nu/'
ignoreLevel: 'info' | 'warning', //skip unwanted messages ('warning' also skips 'info')
ignoreMessages: string | RegExp | RegExp[], //matcher to skip unwanted messages
output: ValidatorResultsOutput,
};
export type ValidatorResultsMessage = {
Expand Down Expand Up @@ -74,8 +74,8 @@ const w3cHtmlValidator = {
throw Error('[w3c-html-validator] Invalid ignoreLevel option: ' + settings.ignoreLevel);
if (settings.output !== 'json' && settings.output !== 'html')
throw Error('[w3c-html-validator] Option "output" must be "json" or "html".');
const mode = settings.html ? 'html' : settings.filename ? 'filename' : 'website';
const readFile = (filename: string) => fs.readFileSync(filename, 'utf-8').replace(/\r/g, '');
const mode = settings.html ? 'html' : settings.filename ? 'filename' : 'website';
const readFile = (filename: string) => fs.readFileSync(filename, 'utf-8').replace(/\r/g, '');
const inputHtml = settings.html ?? (settings.filename ? readFile(settings.filename) : null);
const makePostRequest = () => request.post(settings.checkUrl)
.set('Content-Type', 'text/html; encoding=utf-8')
Expand All @@ -85,25 +85,26 @@ const w3cHtmlValidator = {
const w3cRequest = inputHtml ? makePostRequest() : makeGetRequest();
w3cRequest.set('User-Agent', 'W3C HTML Validator ~ github.com/center-key/w3c-html-validator');
w3cRequest.query({ out: settings.output });
const json = settings.output === 'json';
const json = settings.output === 'json';
const success = '<p class="success">';
const titleLookup = {
html: 'HTML String (characters: ' + inputHtml?.length + ')',
filename: settings.filename,
website: settings.website,
};
const noRegex = !settings.ignoreMessages || typeof settings.ignoreMessages === 'string';
const regexes = noRegex ? [] : <RegExp[]>[settings.ignoreMessages].flat();
const filterMessages = (response: request.Response): request.Response => {
const aboveInfo = (subType: ValidatorResultsMessageSubType): boolean =>
settings.ignoreLevel === 'info' && !!subType;
const aboveIgnoreLevel = (message: ValidatorResultsMessage): boolean =>
!settings.ignoreLevel || message.type !== 'info' || aboveInfo(message.subType);
const skipSubstr = (title: string) =>
typeof settings.ignoreMessages === 'string' && title.includes(settings.ignoreMessages);
const skipRegEx = (title: string) =>
settings.ignoreMessages?.constructor.name === 'RegExp' &&
(<RegExp>settings.ignoreMessages).test(title);
const skipRegex = (title: string) =>
regexes.some(regex => regex.test(title));
const isImportant = (message: ValidatorResultsMessage): boolean =>
aboveIgnoreLevel(message) && !skipSubstr(message.message) && !skipRegEx(message.message);
aboveIgnoreLevel(message) && !skipSubstr(message.message) && !skipRegex(message.message);
if (json)
response.body.messages = response.body.messages?.filter(isImportant) ?? [];
return response;
Expand Down

0 comments on commit 21349da

Please sign in to comment.