forked from webhintio/hint
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rule to check the markup validity
Fix webhintio#28
- Loading branch information
1 parent
22f2e18
commit dbfc42f
Showing
6 changed files
with
274 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* @fileoverview Validating html using `the Nu html checker`; | ||
* https://validator.w3.org/nu/ | ||
*/ | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Requirements | ||
// ------------------------------------------------------------------------------ | ||
|
||
import { debug as d } from '../../utils/debug'; | ||
import { IRule, IRuleBuilder } from '../../types'; // eslint-disable-line no-unused-vars | ||
import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars | ||
|
||
import { ITargetFetchEnd, IScanEnd, IProblemLocation, Severity } from '../../types'; // eslint-disable-line no-unused-vars | ||
|
||
const debug = d(__filename); | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Public | ||
// ------------------------------------------------------------------------------ | ||
|
||
const rule: IRuleBuilder = { | ||
create(context: RuleContext): IRule { | ||
/** The promise that represents the scan by html checker. */ | ||
let htmlCheckerPromise: Promise<any>; | ||
/** Array of strings that needes to be ignored from the checker result. */ | ||
let ignoredMessages; | ||
/** The options to pass to the html checker. */ | ||
const scanOptions = { | ||
data: '', | ||
format: 'json' | ||
}; | ||
|
||
type Error = { // eslint-disable-line no-unused-vars | ||
extract: string, // code snippet | ||
firstColumn: number, | ||
lastLine: number, | ||
hiliteStart: number, | ||
message: string, | ||
subType: string | ||
}; | ||
|
||
const loadRuleConfig = () => { | ||
const ignore = context.ruleOptions && context.ruleOptions.ignore || []; | ||
|
||
ignoredMessages = Array.isArray(ignore) ? ignore : [ignore]; | ||
|
||
}; | ||
|
||
// Filter out ignored messages | ||
const filter = (messages) => { | ||
return messages.filter((message) => { | ||
return !ignoredMessages.includes(message.message); | ||
}); | ||
}; | ||
|
||
const start = (data: ITargetFetchEnd) => { | ||
const { response } = data; | ||
const htmlChecker = require('html-validator'); | ||
|
||
scanOptions.data = response.body.content; | ||
htmlCheckerPromise = htmlChecker(scanOptions); | ||
}; | ||
|
||
const end = async (data: IScanEnd) => { | ||
const { resource } = data; | ||
let result; | ||
|
||
if (!htmlCheckerPromise) { | ||
return; | ||
} | ||
|
||
debug(`Waiting for Html Checker results for ${resource}`); | ||
|
||
try { | ||
result = await htmlCheckerPromise; | ||
} catch (e) { | ||
debug(`Error getting html checker result for ${resource}.`); | ||
await context.report(resource, null, `Couldn't get results from Html Checker for ${resource}.`); | ||
|
||
return; | ||
} | ||
|
||
debug(`Received Html Checker results for ${resource}`); | ||
|
||
const filteredMessages = filter(result.messages); | ||
|
||
const reportPromises: Array<Promise<void>> = filteredMessages.map((messageItem: Error): Promise<void> => { | ||
const position: IProblemLocation = { | ||
column: messageItem.firstColumn, | ||
elementColumn: messageItem.hiliteStart + 1, | ||
elementLine: 1, // We will pass in the single-line code snippet generated from the html checker, so the elementLine is always 1 | ||
line: messageItem.lastLine | ||
}; | ||
|
||
return context.report(resource, null, messageItem.message, null, position, Severity[messageItem.subType], messageItem.extract); | ||
}); | ||
|
||
try { | ||
await Promise.all(reportPromises); | ||
} catch (e) { | ||
debug(`Error reporting the html checker results.`); | ||
|
||
return; | ||
} | ||
}; | ||
|
||
loadRuleConfig(); | ||
|
||
return { | ||
'scan::end': end, | ||
'targetfetch::end': start | ||
}; | ||
}, | ||
meta: { | ||
docs: { | ||
category: 'Interoperability', | ||
description: 'Validating html using `the Nu html checker`' | ||
}, | ||
fixable: 'code', | ||
recommended: true, | ||
schema: [{ | ||
properties: { | ||
anyOf: [ | ||
{ | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
} | ||
}, { | ||
type: 'string' | ||
} | ||
] | ||
} | ||
}], | ||
worksWithLocalFiles: false | ||
} | ||
}; | ||
|
||
module.exports = rule; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* eslint sort-keys: 0, no-undefined: 0 */ | ||
|
||
import * as mock from 'mock-require'; | ||
|
||
import { RuleTest } from '../../../helpers/rule-test-type'; // eslint-disable-line no-unused-vars | ||
import * as ruleRunner from '../../../helpers/rule-runner'; | ||
import { getRuleName } from '../../../../src/lib/utils/rule-helpers'; | ||
const ruleName = getRuleName(__dirname); | ||
const exampleUrl = 'https://example.com/'; | ||
|
||
const htmlCheckerMock = (messages) => { | ||
const mockedChecker = () => { | ||
if (!messages) { | ||
return Promise.reject('Error.'); | ||
} | ||
|
||
return Promise.resolve(messages); | ||
}; | ||
|
||
mock('html-validator', mockedChecker); | ||
}; | ||
|
||
// Html checker response that contains no errors | ||
const noErrorJSON = { | ||
url: exampleUrl, | ||
messages: [] | ||
}; | ||
|
||
// Html checker response that contains errors/warnings | ||
const errorJSON = { | ||
url: exampleUrl, | ||
messages: [ | ||
{ | ||
type: 'info', | ||
lastLine: 1, | ||
lastColumn: 3114, | ||
firstColumn: 3046, | ||
subType: 'error', | ||
message: '“role="none"” is not yet supported in all browsers. Consider instead either using “role="presentation"” or “role="none presentation"”.', | ||
extract: 'stration"><img src="/images/iceberg-left.svg" id="iceberg1" alt="" role="none"> <img ', | ||
hiliteStart: 10, | ||
hiliteLength: 69 | ||
}, | ||
{ | ||
type: 'info', | ||
lastLine: 1, | ||
lastColumn: 3462, | ||
firstColumn: 3459, | ||
subType: 'warning', | ||
message: 'Consider using the “h1” element as a top-level heading only (all “h1” elements are treated as top-level headings by many screen readers and other tools)', | ||
extract: '-section"><h1>example<', | ||
hiliteStart: 10, | ||
hiliteLength: 4 | ||
} | ||
] | ||
}; | ||
|
||
const testsForDefaults: Array<RuleTest> = [ | ||
{ | ||
name: 'No reports if html checker returns no messages', | ||
serverUrl: exampleUrl, | ||
before() { | ||
htmlCheckerMock(noErrorJSON); | ||
} | ||
}, | ||
{ | ||
name: 'Reports warnings/errors if the html checker returns messages', | ||
serverUrl: exampleUrl, | ||
reports: [{ | ||
message: errorJSON.messages[0].message, | ||
position: { column: errorJSON.messages[0].firstColumn, line: errorJSON.messages[0].lastLine } | ||
|
||
}, { | ||
message: errorJSON.messages[1].message, | ||
position: { column: errorJSON.messages[1].firstColumn, line: errorJSON.messages[1].lastLine } | ||
}], | ||
before() { | ||
htmlCheckerMock(errorJSON); | ||
} | ||
} | ||
]; | ||
|
||
const testsForStringConfigs: Array<RuleTest> = [ | ||
{ | ||
name: 'Ignore selected message(string) from the report', | ||
serverUrl: exampleUrl, | ||
reports: [{ | ||
message: errorJSON.messages[0].message, | ||
position: { column: errorJSON.messages[0].firstColumn, line: errorJSON.messages[0].lastLine } | ||
|
||
}], | ||
before() { | ||
htmlCheckerMock(errorJSON); | ||
} | ||
} | ||
]; | ||
|
||
const testsForArrayConfigs: Array<RuleTest> = [ | ||
{ | ||
name: 'Ignore selected messages(array) from the report', | ||
serverUrl: exampleUrl, | ||
before() { | ||
htmlCheckerMock(errorJSON); | ||
} | ||
} | ||
]; | ||
|
||
const testForErrors: Array<RuleTest> = [ | ||
{ | ||
name: 'Reports error when not able to get result from the HTML Checker', | ||
serverUrl: exampleUrl, | ||
reports: [{ message: `Couldn't get results from Html Checker for ${exampleUrl}.` }], | ||
before() { | ||
htmlCheckerMock(null); | ||
} | ||
} | ||
]; | ||
|
||
ruleRunner.testRule(ruleName, testsForDefaults, { serial: true }); | ||
ruleRunner.testRule(ruleName, testsForStringConfigs, { | ||
ruleOptions: { ignore: errorJSON.messages[1].message }, | ||
serial: true | ||
}); | ||
ruleRunner.testRule(ruleName, testsForArrayConfigs, { | ||
ruleOptions: { ignore: [errorJSON.messages[0].message, errorJSON.messages[1].message] }, | ||
serial: true | ||
}); | ||
ruleRunner.testRule(ruleName, testForErrors); |