Skip to content

Commit

Permalink
Add rule to check the markup validity
Browse files Browse the repository at this point in the history
  • Loading branch information
qzhou1607-zz committed Jun 29, 2017
1 parent 185510e commit bdb51cd
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .sonarrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"maxLoadWaitTime": 30000
}
},
"formatter": "stylish",
"formatter": "codeframe",
"rulesTimeout": 120000,
"rules": {
"axe": "warning",
"content-type": "warning",
"disallowed-headers": "warning",
"disown-opener": "warning",
"highest-available-document-mode": "warning",
"html-checker": "warning",
"manifest-exists": "warning",
"manifest-file-extension": "warning",
"manifest-is-valid": "warning",
Expand Down
60 changes: 60 additions & 0 deletions docs/user-guide/rules/html-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# The Nu HTML Test (`html-checker`)

`html-checker` validates the markup of a website against the [Nu HTML checker](https://validator.github.io/validator/).

## Why is this important?

> Serving valid HTML nowadays have been commonly overlooked these days.
By running the HTML documents through a checker, it's easier to catch
unintended mistakes which might have otherwise been missed.
Adhering to the W3C' standards has a lot to offer to both the
developers and the web users: It provides better browser compatibility,
helps to avoid potential problems with accessibility/usability, and makes it easier for future maintainance.
>
> The Nu Html Checker(v.Nu) serves as the backend of [checker.html5.org](https://checker.html5.org/),
[html5.validator.nu](https://html5.validator.nu), and [validator.w3.org/nu](https://validator.w3.org/nu/).
It also provides a [web service interface](https://github.com/validator/validator/wiki/Service-%C2%BB-HTTP-interface).
This rule interacts with this service via [html-validator](https://www.npmjs.com/package/html-validator),
and is able to test both remote websites and local server instances.

## What does the rule check?

According to the Nu Html checker [documentation](https://validator.w3.org/nu/about.html), the positive cases contain two sections:

* Markup cases that are potential problems for accessibility, usability,
interoperability, security, or maintainability—or because they can result in poor performance,
or that might cause your scripts to fail in ways that are hard to troubleshoot.

* Markup cases that are defined as errors because they can cause you to run into potential
problems in HTML parsing and error-handling behavior—so that, say, you’d end up with some unintuitive, unexpected result in the DOM.

For explanation behind those requirements, please checkout:

* [rationale for syntax-level errors](https://www.w3.org/TR/html/introduction.html#syntax-errors)
* [rationale for restrictions on content models and on attribute values](https://www.w3.org/TR/html/introduction.html#restrictions-on-content-models-and-on-attribute-values)

## Can the rule be configured?

You can ignore certain error/warning by setting the `ignore` option for the `html-checker` rule.
You can either pass in a string or an array that contains all the messages to be ignored.

E.g. The following configuration will ignore the errors/warnings with the message of `Invalid attribute`:

```json
"html-checker": ["error", {
"ignore": "Invalid attribute"
}]
```

Alternative, you can pass in an array if you have more than one type of messages to ignore:

```json
"html-checker": ["error", {
"ignore": ["Invalid attribute", "Invalid tag"]
}]
```

## Further Reading

* [Why Validate Using the Nu Html Checker?](https://validator.w3.org/nu/about.html)
* [The Nu Html Checker Wiki](https://github.com/validator/validator/wiki)
1 change: 1 addition & 0 deletions docs/user-guide/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [`content-type`](content-type.md)
* [`highest-available-document-mode`](highest-available-document-mode.md)
* [`no-friendly-error-pages`](no-friendly-error-pages.md)
* [`html-checker`](html-checker.md)

## Performance

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"file-url": "^2.0.2",
"globby": "^6.1.0",
"handlebars": "^4.0.10",
"html-validator": "^2.2.1",
"iconv-lite": "^0.4.17",
"inquirer": "^3.0.6",
"is-ci": "^1.0.10",
Expand Down
4 changes: 2 additions & 2 deletions src/lib/rule-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class RuleContext {
}

/** Reports a problem with the resource. */
public async report(resource: string, element: IAsyncHTMLElement, message: string, content?: string, location?: IProblemLocation, severity?: Severity): Promise<void> { //eslint-disable-line require-await
public async report(resource: string, element: IAsyncHTMLElement, message: string, content?: string, location?: IProblemLocation, severity?: Severity, codeSnippet?: string): Promise<void> { //eslint-disable-line require-await
let position: IProblemLocation = location;
let sourceCode: string = null;

Expand All @@ -107,7 +107,7 @@ export class RuleContext {
this.sonar.report(
this.id,
severity || this.severity,
sourceCode,
codeSnippet || sourceCode,
position,
message,
resource
Expand Down
142 changes: 142 additions & 0 deletions src/lib/rules/html-checker/html-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @fileoverview Validating html using `the Nu html checker`;
* https://validator.w3.org/nu/
*/

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

import { debug as d } from '../../utils/debug';
import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars
import { IRule, IRuleBuilder, ITargetFetchEnd, IScanEnd, IProblemLocation, Severity } from '../../types'; // eslint-disable-line no-unused-vars

const debug: debug.IDebugger = 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 = () => {
// Up to now, the `ignore` setting in `html-validator` only works if `format` is set to `text`
// So we implement `ignore` in our code rather than pass it to `scanOptions`
// TODO: Pass `ignore` once this issue (https://github.com/zrrrzzt/html-validator/issues/58) is solved.
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 locateAndReport = (resource: string, 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);
};

const start = (data: ITargetFetchEnd) => {
const { response } = data;

/* HACK: Need to do a require here in order to be capable of mocking
when testing the rule and `import` doesn't work here. */
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}.`, e);
await context.report(resource, null, `Couldn't get results from Html Checker for ${resource}. Error: ${e}`);

return;
}

debug(`Received Html Checker results for ${resource}`);

const filteredMessages: Array<Error> = filter(result.messages);
const reportPromises: Array<Promise<void>> = filteredMessages.map((messageItem: Error): Promise<void> => {
return locateAndReport(resource, messageItem);
});

try {
await Promise.all(reportPromises);
} catch (e) {
debug(`Error reporting the html checker results.`, e);

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: [
{
items: { type: 'string' },
type: 'array'
}, { type: 'string' }
]
}
}],
worksWithLocalFiles: false
}
};

module.exports = rule;
129 changes: 129 additions & 0 deletions tests/lib/rules/html-checker/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* 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 error = 'error';

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}. Error: ${error}` }],
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);

0 comments on commit bdb51cd

Please sign in to comment.