';
39 | const titleLookup = {
40 | html: `HTML String (characters: ${inputHtml?.length})`,
41 | filename: filename,
42 | website: settings.website,
43 | };
44 | const filterMessages = (response) => {
45 | const aboveInfo = (subType) => settings.ignoreLevel === 'info' && !!subType;
46 | const aboveIgnoreLevel = (message) => !settings.ignoreLevel || message.type !== 'info' || aboveInfo(message.subType);
47 | const matchesSkipPattern = (title) => settings.ignoreMessages.some(pattern => typeof pattern === 'string' ? title.includes(pattern) : pattern.test(title));
48 | const isImportant = (message) => aboveIgnoreLevel(message) && !matchesSkipPattern(message.message);
49 | if (json)
50 | response.body.messages = response.body.messages?.filter(isImportant) ?? [];
51 | return response;
52 | };
53 | const toValidatorResults = (response) => ({
54 | validates: json ? !response.body.messages.length : !!response.text?.includes(success),
55 | mode: mode,
56 | title: titleLookup[mode],
57 | html: inputHtml,
58 | filename: filename,
59 | website: settings.website || null,
60 | output: settings.output,
61 | status: response.statusCode || -1,
62 | messages: json ? response.body.messages : null,
63 | display: json ? null : response.text,
64 | dryRun: settings.dryRun,
65 | });
66 | const handleError = (reason) => {
67 | const errRes = reason.response ?? {};
68 | const getMsg = () => [errRes.status, errRes.res.statusMessage, errRes.request.url];
69 | const message = reason.response ? getMsg() : [reason.errno, reason.message];
70 | errRes.body = { messages: [{ type: 'network-error', message: message.join(' ') }] };
71 | return toValidatorResults(errRes);
72 | };
73 | const pseudoResponse = {
74 | statusCode: 200,
75 | body: { messages: [] },
76 | text: 'Validation bypassed.',
77 | };
78 | const pseudoRequest = () => new Promise(resolve => resolve(pseudoResponse));
79 | const validation = settings.dryRun ? pseudoRequest() : w3cRequest;
80 | return validation.then(filterMessages).then(toValidatorResults).catch(handleError);
81 | },
82 | dryRunNotice() {
83 | log(chalk.gray('w3c-html-validator'), chalk.yellowBright('dry run mode:'), chalk.whiteBright('validation being bypassed'));
84 | },
85 | summary(numFiles) {
86 | log(chalk.gray('w3c-html-validator'), chalk.magenta('files: ' + String(numFiles)));
87 | },
88 | reporter(results, options) {
89 | const defaults = {
90 | continueOnFail: false,
91 | maxMessageLen: null,
92 | quiet: false,
93 | title: null,
94 | };
95 | const settings = { ...defaults, ...options };
96 | if (typeof results?.validates !== 'boolean')
97 | throw new Error('[w3c-html-validator] Invalid results for reporter(): ' + String(results));
98 | const messages = results.messages ?? [];
99 | const title = settings.title ?? results.title;
100 | const status = results.validates ? chalk.green.bold('✔ pass') : chalk.red.bold('✘ fail');
101 | const count = results.validates ? '' : `(messages: ${messages.length})`;
102 | if (!results.validates || !settings.quiet)
103 | log(chalk.gray('w3c-html-validator'), status, chalk.blue.bold(title), chalk.white(count));
104 | const typeColorMap = {
105 | error: chalk.red.bold,
106 | warning: chalk.yellow.bold,
107 | info: chalk.white.bold,
108 | };
109 | const logMessage = (message) => {
110 | const type = message.subType ?? message.type;
111 | const typeColor = typeColorMap[type] ?? chalk.redBright.bold;
112 | const location = `line ${message.lastLine}, column ${message.firstColumn}:`;
113 | const lineText = message.extract?.replace(/\n/g, '\\n');
114 | const maxLen = settings.maxMessageLen ?? undefined;
115 | log(typeColor('HTML ' + type + ':'), message.message.substring(0, maxLen));
116 | if (message.lastLine)
117 | log(chalk.white(location), chalk.magenta(lineText));
118 | };
119 | messages.forEach(logMessage);
120 | const failDetails = () => {
121 | const toString = (message) => `${message.subType ?? message.type} line ${message.lastLine} column ${message.firstColumn}`;
122 | const fileDetails = () => `${results.filename} -- ${results.messages.map(toString).join(', ')}`;
123 | return !results.filename ? results.messages[0].message : fileDetails();
124 | };
125 | if (!settings.continueOnFail && !results.validates)
126 | throw new Error('[w3c-html-validator] Failed: ' + failDetails());
127 | return results;
128 | },
129 | };
130 | export { w3cHtmlValidator };
131 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from '@eslint/js';
4 | import tseslint from 'typescript-eslint';
5 |
6 | export default [
7 | eslint.configs.recommended,
8 | ...tseslint.configs.strictTypeChecked,
9 | { ignores: ['**/*.js'] },
10 | {
11 | languageOptions: { parserOptions: { projectService: true } },
12 | rules: {
13 | '@typescript-eslint/no-confusing-void-expression': 'off', //prefer minimal arrow functions
14 | '@typescript-eslint/no-floating-promises': 'off', //annimations may be fire-and-forget
15 | '@typescript-eslint/no-misused-promises': 'off', //annimations may be fire-and-forget
16 | '@typescript-eslint/no-non-null-assertion': 'off', //ts cannot always know value exists
17 | '@typescript-eslint/restrict-template-expressions': 'off', //numbers in templates are natural
18 | '@typescript-eslint/unbound-method': 'off', //safer to not use 'this'
19 | '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', //clarity over theoretical exceptions
20 | },
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/examples.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | ////////////////////////
3 | // W3C HTML Validator //
4 | // Examples //
5 | ////////////////////////
6 |
7 | // Command to run:
8 | // $ node examples.js
9 |
10 | import { w3cHtmlValidator } from './dist/w3c-html-validator.js';
11 |
12 | // Formatted output
13 | const options = { continueOnFail: true, maxMessageLen: 80 };
14 | const customReporter = (results) => w3cHtmlValidator.reporter(results, options);
15 | w3cHtmlValidator.validate({ website: 'https://pretty-print-json.js.org/' }).then(w3cHtmlValidator.reporter);
16 | w3cHtmlValidator.validate({ filename: 'spec/html/valid.html' }).then(w3cHtmlValidator.reporter);
17 | w3cHtmlValidator.validate({ filename: 'spec/html/invalid.html' }).then(customReporter);
18 |
19 | // JSON output
20 | const sleep = (data) => new Promise(resolve => setTimeout(() => resolve(data), 2000));
21 | const log = (results) => console.log('\nValidatorResults:', results);
22 | w3cHtmlValidator.validate({ filename: 'spec/html/invalid.html' }).then(sleep).then(log);
23 |
--------------------------------------------------------------------------------
/examples.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/center-key/w3c-html-validator/9d0f1f1f4769f659b30677f2c655de88d056cfa4/examples.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "w3c-html-validator",
3 | "version": "1.8.3",
4 | "description": "Check the markup validity of HTML files using the W3C validator",
5 | "license": "MIT",
6 | "type": "module",
7 | "module": "dist/w3c-html-validator.js",
8 | "types": "dist/w3c-html-validator.d.ts",
9 | "exports": "./dist/w3c-html-validator.js",
10 | "files": [
11 | "dist"
12 | ],
13 | "bin": {
14 | "html-validator": "bin/cli.js",
15 | "w3c-html-validator": "bin/cli.js"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/center-key/w3c-html-validator.git"
20 | },
21 | "homepage": "https://github.com/center-key/w3c-html-validator",
22 | "bugs": "https://github.com/center-key/w3c-html-validator/issues",
23 | "docs": "https://github.com/center-key/w3c-html-validator#readme",
24 | "author": "Thomas Davis A section without a heading ';
95 | const titleLookup = {
96 | html: `HTML String (characters: ${inputHtml?.length})`,
97 | filename: filename,
98 | website: settings.website,
99 | };
100 | const filterMessages = (response: request.Response): request.Response => {
101 | const aboveInfo = (subType: ValidatorResultsMessageSubType): boolean =>
102 | settings.ignoreLevel === 'info' && !!subType;
103 | const aboveIgnoreLevel = (message: ValidatorResultsMessage): boolean =>
104 | !settings.ignoreLevel || message.type !== 'info' || aboveInfo(message.subType);
105 | const matchesSkipPattern = (title: string): boolean =>
106 | settings.ignoreMessages.some(pattern =>
107 | typeof pattern === 'string' ? title.includes(pattern) : pattern.test(title));
108 | const isImportant = (message: ValidatorResultsMessage): boolean =>
109 | aboveIgnoreLevel(message) && !matchesSkipPattern(message.message);
110 | if (json)
111 | response.body.messages = response.body.messages?.filter(isImportant) ?? []; //eslint-disable-line
112 | return response;
113 | };
114 | const toValidatorResults = (response: request.Response): ValidatorResults => ({
115 | validates: json ? !response.body.messages.length : !!response.text?.includes(success), //eslint-disable-line
116 | mode: mode,
117 | title: Specification Page
9 | Inside Out
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/html/valid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Specification Page
9 |
10 |
11 |
--------------------------------------------------------------------------------
/spec/ignore-config.txt:
--------------------------------------------------------------------------------
1 | # Ignore Config for w3c-html-validator
2 |
3 | /^Duplicate ID/
4 | /^Element .blockquote. not allowed/
5 | /^Element .style. not allowed/
6 |
--------------------------------------------------------------------------------
/spec/mocha.spec.js:
--------------------------------------------------------------------------------
1 | // W3C HTML Validator
2 | // Mocha Specification Suite
3 |
4 | // Imports
5 | import { assertDeepStrictEqual } from 'assert-deep-strict-equal';
6 | import { cliArgvUtil } from 'cli-argv-util';
7 | import assert from 'assert';
8 | import fs from 'fs';
9 |
10 | // Setup
11 | import { w3cHtmlValidator } from '../dist/w3c-html-validator.js';
12 | const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
13 | const validHtml = fs.readFileSync('spec/html/valid.html', 'utf-8').replace(/\r/g, '');
14 | const invalidHtml = fs.readFileSync('spec/html/invalid.html', 'utf-8').replace(/\r/g, '');
15 |
16 | ////////////////////////////////////////////////////////////////////////////////
17 | describe('The "dist" folder', () => {
18 |
19 | it('contains the correct files', () => {
20 | const actual = fs.readdirSync('dist').sort();
21 | const expected = [
22 | 'w3c-html-validator.d.ts',
23 | 'w3c-html-validator.js',
24 | ];
25 | assertDeepStrictEqual(actual, expected);
26 | });
27 |
28 | });
29 |
30 | ////////////////////////////////////////////////////////////////////////////////
31 | describe('Library version number', () => {
32 |
33 | it('follows semantic version formatting', () => {
34 | const data = w3cHtmlValidator.version;
35 | const semVerPattern = /\d+[.]\d+[.]\d+/;
36 | const actual = { version: data, valid: semVerPattern.test(data) };
37 | const expected = { version: data, valid: true };
38 | assertDeepStrictEqual(actual, expected);
39 | });
40 |
41 | });
42 |
43 | ////////////////////////////////////////////////////////////////////////////////
44 | describe('Library module', () => {
45 |
46 | it('is an object', () => {
47 | const actual = { constructor: w3cHtmlValidator.constructor.name };
48 | const expected = { constructor: 'Object' };
49 | assertDeepStrictEqual(actual, expected);
50 | });
51 |
52 | it('has functions named validate(), dryRunNotice() summary(), and reporter()', () => {
53 | const module = w3cHtmlValidator;
54 | const actual = Object.keys(module).sort().map(key => [key, typeof module[key]]);
55 | const expected = [
56 | ['dryRunNotice', 'function'],
57 | ['reporter', 'function'],
58 | ['summary', 'function'],
59 | ['validate', 'function'],
60 | ['version', 'string'],
61 | ];
62 | assertDeepStrictEqual(actual, expected);
63 | });
64 |
65 | });
66 |
67 | ////////////////////////////////////////////////////////////////////////////////
68 | describe('Pretty-Print JSON website', () => {
69 |
70 | it('validates', (done) => {
71 | const handleData = (data) => {
72 | const actual = data;
73 | const expected = {
74 | validates: true,
75 | mode: 'website',
76 | title: 'https://pretty-print-json.js.org/',
77 | html: null,
78 | filename: null,
79 | website: 'https://pretty-print-json.js.org/',
80 | output: 'json',
81 | status: 200,
82 | messages: [],
83 | display: null,
84 | dryRun: false,
85 | };
86 | assertDeepStrictEqual(actual, expected, done);
87 | };
88 | w3cHtmlValidator.validate({ website: 'https://pretty-print-json.js.org/' }).then(handleData);
89 | });
90 |
91 | });
92 |
93 | ////////////////////////////////////////////////////////////////////////////////
94 | describe('Valid HTML string', () => {
95 |
96 | it('passes validator with JSON output', (done) => {
97 | const handleData = (data) => {
98 | const actual = data;
99 | const expected = {
100 | validates: true,
101 | mode: 'html',
102 | title: 'HTML String (characters: 153)',
103 | html: validHtml,
104 | filename: null,
105 | website: null,
106 | output: 'json',
107 | status: 200,
108 | messages: [],
109 | display: null,
110 | dryRun: false,
111 | };
112 | assertDeepStrictEqual(actual, expected, done);
113 | };
114 | w3cHtmlValidator.validate({ html: validHtml, output: 'json' }).then(handleData);
115 | });
116 |
117 | it('passes validator with HTML output', (done) => {
118 | const handleData = (data) => {
119 | const actual = data;
120 | delete actual.display;
121 | const expected = {
122 | validates: true,
123 | mode: 'html',
124 | title: 'HTML String (characters: 153)',
125 | html: validHtml,
126 | filename: null,
127 | website: null,
128 | output: 'html',
129 | status: 200,
130 | messages: null,
131 | dryRun: false,
132 | };
133 | assertDeepStrictEqual(actual, expected, done);
134 | };
135 | w3cHtmlValidator.validate({ html: validHtml, output: 'html' }).then(handleData);
136 | });
137 |
138 | });
139 |
140 | ////////////////////////////////////////////////////////////////////////////////
141 | describe('Invalid HTML string', () => {
142 |
143 | it('fails validator with JSON output', (done) => {
144 | const message = {
145 | heading: 'Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections, or else use a “div” element instead for any cases where no heading is needed.',
146 | child: 'Element “blockquote” not allowed as child of element “span” in this context. (Suppressing further errors from this subtree.)',
147 | };
148 | const handleData = (data) => {
149 | const actual = data;
150 | const expected = {
151 | validates: false,
152 | mode: 'html',
153 | title: 'HTML String (characters: 275)',
154 | html: invalidHtml,
155 | filename: null,
156 | website: null,
157 | output: 'json',
158 | status: 200,
159 | messages: [
160 | {
161 | type: 'info',
162 | subType: 'warning',
163 | message: message.heading,
164 | extract: 'e\n Inside',
175 | lastLine: 12,
176 | firstColumn: 10,
177 | lastColumn: 21,
178 | hiliteStart: 10,
179 | hiliteLength: 12,
180 | },
181 | ],
182 | display: null,
183 | dryRun: false,
184 | };
185 | assertDeepStrictEqual(actual, expected, done);
186 | };
187 | w3cHtmlValidator.validate({ html: invalidHtml, output: 'json' }).then(handleData);
188 | });
189 |
190 | it('fails validator with HTML output', (done) => {
191 | const handleData = (data) => {
192 | const actual = data;
193 | delete actual.display;
194 | const expected = {
195 | validates: false,
196 | mode: 'html',
197 | title: 'HTML String (characters: 275)',
198 | html: invalidHtml,
199 | filename: null,
200 | website: null,
201 | output: 'html',
202 | status: 200,
203 | messages: null,
204 | dryRun: false,
205 | };
206 | assertDeepStrictEqual(actual, expected, done);
207 | };
208 | w3cHtmlValidator.validate({ html: invalidHtml, output: 'html' }).then(handleData);
209 | });
210 |
211 | });
212 |
213 | ////////////////////////////////////////////////////////////////////////////////
214 | describe('HTML file', () => {
215 |
216 | it('that is valid passes validation', (done) => {
217 | const handleData = (data) => {
218 | const actual = data;
219 | const expected = {
220 | validates: true,
221 | mode: 'filename',
222 | title: 'spec/html/valid.html',
223 | html: validHtml,
224 | filename: 'spec/html/valid.html',
225 | website: null,
226 | output: 'json',
227 | status: 200,
228 | messages: [],
229 | display: null,
230 | dryRun: false,
231 | };
232 | assertDeepStrictEqual(actual, expected, done);
233 | };
234 | w3cHtmlValidator.validate({ filename: 'spec/html/valid.html' }).then(handleData);
235 | });
236 |
237 | it('that is invalid fails validation', (done) => {
238 | const handleData = (data) => {
239 | const actual = { validates: data.validates };
240 | const expected = { validates: false };
241 | assertDeepStrictEqual(actual, expected, done);
242 | };
243 | w3cHtmlValidator.validate({ filename: 'spec/html/invalid.html' }).then(handleData);
244 | });
245 |
246 | });
247 |
248 | ////////////////////////////////////////////////////////////////////////////////
249 | describe('Option ignoreLevel set to "warning"', () => {
250 |
251 | it('skips warning messages', (done) => {
252 | const handleData = (data) => {
253 | const actual = {
254 | validates: data.validates,
255 | messages: data.messages.map(message => message.type),
256 | };
257 | const expected = {
258 | validates: false,
259 | messages: ['error'],
260 | };
261 | assertDeepStrictEqual(actual, expected, done);
262 | };
263 | const options = { filename: 'spec/html/invalid.html', ignoreLevel: 'warning' };
264 | w3cHtmlValidator.validate(options).then(handleData);
265 | });
266 |
267 | });
268 |
269 | ////////////////////////////////////////////////////////////////////////////////
270 | describe('Option ignoreMessages', () => {
271 | // Example validation messgaes:
272 | // warning: 'Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections.',
273 | // error: 'Element “blockquote” not allowed as child of element “span” in this context. (Suppressing further errors from this subtree.)',
274 |
275 | it('as a substring can skip "Section lacks heading" messages', (done) => {
276 | const handleData = (data) => {
277 | const actual = {
278 | validates: data.validates,
279 | messages: data.messages.map(message => message.type),
280 | };
281 | const expected = {
282 | validates: false,
283 | messages: ['error'],
284 | };
285 | assertDeepStrictEqual(actual, expected, done);
286 | };
287 | const options = {
288 | filename: 'spec/html/invalid.html',
289 | ignoreMessages: ['Section lacks heading'],
290 | };
291 | w3cHtmlValidator.validate(options).then(handleData);
292 | });
293 |
294 | it('can skip messages matching a regular expression', (done) => {
295 | const handleData = (data) => {
296 | const actual = {
297 | validates: data.validates,
298 | messages: data.messages.map(message => message.type),
299 | };
300 | const expected = {
301 | validates: false,
302 | messages: ['info'],
303 | };
304 | assertDeepStrictEqual(actual, expected, done);
305 | };
306 | const options = {
307 | filename: 'spec/html/invalid.html',
308 | ignoreMessages: [/^Element .blockquote. not allowed/],
309 | };
310 | w3cHtmlValidator.validate(options).then(handleData);
311 | });
312 |
313 | });
314 |
315 | ////////////////////////////////////////////////////////////////////////////////
316 | describe('Correct error is thrown', () => {
317 |
318 | it('when no input is specified', () => {
319 | const options = {};
320 | const makeInvalidCall = () => w3cHtmlValidator.validate(options);
321 | const exception = { message: '[w3c-html-validator] Must specify the "html", "filename", or "website" option.' };
322 | assert.throws(makeInvalidCall, exception);
323 | });
324 |
325 | it('when "ignoreLevel" option is bogus', () => {
326 | const options = { html: validHtml, ignoreLevel: 'bogus' };
327 | const makeInvalidCall = () => w3cHtmlValidator.validate(options);
328 | const exception = { message: '[w3c-html-validator] Invalid ignoreLevel option: bogus' };
329 | assert.throws(makeInvalidCall, exception);
330 | });
331 |
332 | it('when "output" option is bogus', () => {
333 | const options = { html: validHtml, output: 'bogus' };
334 | const makeInvalidCall = () => w3cHtmlValidator.validate(options);
335 | const exception = { message: '[w3c-html-validator] Option "output" must be "json" or "html".' };
336 | assert.throws(makeInvalidCall, exception);
337 | });
338 |
339 | });
340 |
341 | ////////////////////////////////////////////////////////////////////////////////
342 | describe('Network request failure', () => {
343 |
344 | it('for service unavailable (HTTP status 503) is handled gracefully', (done) => {
345 | const handleData = (data) => {
346 | const actual = data;
347 | const expected = {
348 | validates: false,
349 | mode: 'html',
350 | title: 'HTML String (characters: 153)',
351 | html: validHtml,
352 | filename: null,
353 | website: null,
354 | output: 'json',
355 | status: 503,
356 | messages: [{
357 | type: 'network-error',
358 | message: '503 Service Unavailable https://centerkey.com/rest/status/503/?out=json',
359 | }],
360 | display: null,
361 | dryRun: false,
362 | };
363 | assertDeepStrictEqual(actual, expected, done);
364 | };
365 | const options = {
366 | html: validHtml,
367 | checkUrl: 'https://centerkey.com/rest/status/503/',
368 | output: 'json',
369 | };
370 | w3cHtmlValidator.validate(options).then(handleData);
371 | });
372 |
373 | });
374 |
375 | ////////////////////////////////////////////////////////////////////////////////
376 | describe('The reporter() function', () => {
377 |
378 | it('passes through valid results', (done) => {
379 | const handleData = (data) => {
380 | const actual = data;
381 | const expected = {
382 | validates: true,
383 | mode: 'filename',
384 | title: 'spec/html/valid.html',
385 | html: validHtml,
386 | filename: 'spec/html/valid.html',
387 | website: null,
388 | output: 'json',
389 | status: 200,
390 | messages: [],
391 | display: null,
392 | dryRun: false,
393 | };
394 | assertDeepStrictEqual(actual, expected, done);
395 | };
396 | w3cHtmlValidator.validate({ filename: 'spec/html/valid.html' })
397 | .then(w3cHtmlValidator.reporter)
398 | .then(handleData);
399 | });
400 |
401 | it('throws the correct error when validation fails', () => {
402 | const options = { filename: 'spec/html/invalid.html' };
403 | const fail = () => w3cHtmlValidator.validate(options).then(w3cHtmlValidator.reporter);
404 | const expected = {
405 | name: 'Error',
406 | message: '[w3c-html-validator] Failed: spec/html/invalid.html -- warning line 9 column 4, error line 12 column 10',
407 | };
408 | return assert.rejects(fail, expected);
409 | });
410 |
411 | });
412 |
413 | ////////////////////////////////////////////////////////////////////////////////
414 | describe('Executing the CLI', () => {
415 | const run = (posix) => cliArgvUtil.run(pkg, posix);
416 |
417 | it('to check a valid HTML file correctly outputs a "pass" message', () => {
418 | const actual = run('html-validator spec/html/valid.html --note=cli');
419 | const expected = null;
420 | assertDeepStrictEqual(actual, expected);
421 | });
422 |
423 | it('with a glob selects the correct files to validate', () => {
424 | const actual = run('html-validator "spec/**/valid.html" --note=glob');
425 | const expected = null;
426 | assertDeepStrictEqual(actual, expected);
427 | });
428 |
429 | it('skips validation message matching --ignore and --ignore-config regex patterns', () => {
430 | const actual = run('html-validator spec/html "--ignore=/^Section lacks heading/" --ignore-config=spec/ignore-config.txt');
431 | const expected = null;
432 | assertDeepStrictEqual(actual, expected);
433 | });
434 |
435 | });
436 |
--------------------------------------------------------------------------------
/src/w3c-html-validator.ts:
--------------------------------------------------------------------------------
1 | // W3C HTML Validator ~ MIT License
2 |
3 | // Imports
4 | import chalk, { ChalkInstance } from 'chalk';
5 | import fs from 'fs';
6 | import log from 'fancy-log';
7 | import request from 'superagent';
8 | import slash from 'slash';
9 |
10 | // Type Declarations
11 | export type ValidatorSettings = {
12 | html: string, //example: '