...
 
Commits (17)
# html-validate changelog
# [2.23.0](https://gitlab.com/html-validate/html-validate/compare/v2.22.0...v2.23.0) (2020-05-18)
### Bug Fixes
- **cli:** `expandFiles` path normalization for windows ([b902853](https://gitlab.com/html-validate/html-validate/commit/b902853e696a04202959ae6c4cf086bd48911e4d))
### Features
- **config:** add two new config presets `html-validate:standard` and `html-validate:a17y` ([36bf9ec](https://gitlab.com/html-validate/html-validate/commit/36bf9ec3be7356d534d352d00610d8253885de22)), closes [#90](https://gitlab.com/html-validate/html-validate/issues/90)
- **rules:** add `include` and `exclude` options to `prefer-button` ([b046dc5](https://gitlab.com/html-validate/html-validate/commit/b046dc5943a4bd05dff9766ea6b9c9f522c09d1a)), closes [#90](https://gitlab.com/html-validate/html-validate/issues/90)
- **rules:** add `isKeywordExtended` method for rule authors ([ca7e835](https://gitlab.com/html-validate/html-validate/commit/ca7e835d384c7ed43967bec14f56836353a0b1f6))
# [2.22.0](https://gitlab.com/html-validate/html-validate/compare/v2.21.0...v2.22.0) (2020-05-15)
### Bug Fixes
......
......@@ -82,7 +82,7 @@ module.exports = new Package("html-validate-docs", [
.config(function (computePathsProcessor, computeIdsProcessor) {
computeIdsProcessor.idTemplates.push({
docTypes: ["content", "frontpage", "rule", "rules", "changelog", "error"],
docTypes: ["content", "frontpage", "rule", "rules", "presets", "error"],
getId: function (doc) {
const dir = path.dirname(doc.fileInfo.relativePath);
if (dir === ".") {
......@@ -109,7 +109,7 @@ module.exports = new Package("html-validate-docs", [
});
computePathsProcessor.pathTemplates.push({
docTypes: ["content", "frontpage", "rule", "rules"],
docTypes: ["content", "frontpage", "rule", "rules", "presets"],
getPath: function (doc) {
const dirname = path.dirname(doc.fileInfo.relativePath);
const p = path.join(dirname, doc.fileInfo.baseName);
......@@ -118,11 +118,25 @@ module.exports = new Package("html-validate-docs", [
outputPathTemplate: "${path}",
});
computeIdsProcessor.idTemplates.push({
docTypes: ["changelog"],
getId: function (doc) {
return doc.fileInfo.baseName.toLowerCase();
},
getAliases: function (doc) {
return [doc.id];
},
});
computePathsProcessor.pathTemplates.push({
docTypes: ["changelog"],
getPath: function (doc) {
const dirname = path.dirname(doc.fileInfo.relativePath);
return path.join(dirname, doc.fileInfo.baseName, "index.html");
return path.join(
dirname,
doc.fileInfo.baseName.toLowerCase(),
"index.html"
);
},
outputPathTemplate: "${path.toLowerCase()}",
});
......
/** @todo this will break when typescript is actually used */
const recommended = require("../../../src/config/recommended.ts");
const document = require("../../../src/config/document.ts");
const a17y = require("../../../build/config/presets/a17y");
const document = require("../../../build/config/presets/document");
const recommended = require("../../../build/config/presets/recommended");
const standard = require("../../../build/config/presets/standard");
/* sort order */
const availablePresets = ["recommended", "standard", "a17y", "document"];
/* preset configuration */
const presets = {
a17y,
document,
recommended,
standard,
};
function compareName(a, b) {
if (a.name < b.name) {
......@@ -48,13 +60,18 @@ module.exports = function rulesProcessor(renderDocsProcessor) {
url: doc.outputPath,
category: doc.category,
summary: doc.summary,
recommended: !!recommended.rules[doc.name],
document: !!document.rules[doc.name],
presets: availablePresets.reduce((result, presetName) => {
const config = presets[presetName];
if (config && config.rules) {
result[presetName] = Boolean(config.rules[doc.name]);
}
return result;
}, {}),
}))
.sort(compareName);
/* group rules into categories */
const categories = {};
const categories = { all: rules };
rules.forEach((rule) => {
const category = rule.category || "other";
if (!(category in categories)) {
......@@ -64,5 +81,6 @@ module.exports = function rulesProcessor(renderDocsProcessor) {
});
renderDocsProcessor.extraData.rules = categories;
renderDocsProcessor.extraData.presets = availablePresets;
}
};
......@@ -39,19 +39,20 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">User guide <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/usage">Getting started</a></li>
<li><a href="/usage/cli.html">Using CLI</a></li>
<li><a href="/usage/elements.html">Elements</a></li>
<li><a href="/usage/transformers.html">Transfomers</a></li>
<li>{@link usage Getting started}</li>
<li>{@link usage/cli Using CLI}</li>
<li>{@link usage/elements Elements}</li>
<li>{@link rules/presets Presets}</li>
<li>{@link usage/transformers Transfomers}</li>
<li role="separator" class="divider"></li>
<li><a href="/usage/vscode.html">VS Code</a></li>
<li>{@link usage/vscode VS Code}</li>
<li role="separator" class="divider"></li>
<li><a href="/frameworks/angularjs.html">AngularJS</a></li>
<li><a href="/usage/cypress.html">Cypress</a></li>
<li><a href="/usage/grunt.html">Grunt</a></li>
<li><a href="/frameworks/jest.html">Jest</a></li>
<li><a href="/usage/protractor.html">Protractor</a></li>
<li><a href="/frameworks/vue.html">Vue.js</a></li>
<li>{@link frameworks/angularjs AngularJS}</li>
<li>{@link usage/cypress Cypress}</li>
<li>{@link usage/grunt Grunt}</li>
<li>{@link frameworks/jest Jest}</li>
<li>{@link usage/protractor Protractor}</li>
<li>{@link frameworks/vue Vue.js}</li>
</ul>
</li>
<li class="dropdown">
......@@ -65,22 +66,22 @@
<li>{@link guide/metadata/writing-tests Writing tests}</li>
</ul>
</li>
<li><a href="/rules">Rules</a></li>
<li>{@link rules Rules}</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Developers guide <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ pkg.repository.url }}">Getting the source code</a></li>
<li><a href="{{ pkg.bugs.url }}">File issue</a></li>
<li role="separator" class="divider"></li>
<li><a href="/dev/using-api.html">Using API</a></li>
<li><a href="/dev/writing-rules.html">Writing rules</a></li>
<li><a href="/dev/writing-plugins.html">Writing plugins</a></li>
<li><a href="/dev/transformers.html">Writing transformers</a></li>
<li><a href="/dev/events.html">List of events</a></li>
<li>{@link dev/using-api Using API}</li>
<li>{@link dev/writing-rules Writing rules}</li>
<li>{@link dev/writing-plugins Writing plugins}</li>
<li>{@link dev/transformers Writing transformers}</li>
<li>{@link dev/events List of events}</li>
</ul>
</li>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/about">About</a></li>
<li>{@link changelog Changelog}</li>
<li>{@link about About}</li>
</ul>
<p class="navbar-text navbar-right hidden-xs hidden-sm">{{ pkg.name }}-{{ pkg.version }}</p>
</div>
......
{% extends "base.template.html" %}
{%- macro ruleTable(rules) %}
<table class="table table-striped preset-table">
<colgroup>
<col class="name">
{% for preset in presets %}
<col class="preset-name">
{% endfor %}
</colgroup>
<thead>
<tr>
<td>Rule</td>
{% for preset in presets %}
<td><code>{{ preset }}</code></td>
{% endfor %}
</tr>
</thead>
<tbody>
{% for rule in rules %}
<tr>
<td>{@link {{ rule.name }} {{ rule.name }}}</td>
{% for preset in presets %}
<td>
{% if rule.presets[preset] %}
<span class="fa fa-check"></span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro -%}
{% block content %}
{{ doc.description | marked }}
{{ ruleTable(rules['all']) }}
{% endblock %}
......@@ -7,22 +7,22 @@
<col class="name">
<col class="summary">
</colgroup>
{% for rule in rules %}
<tbody>
{% for rule in rules %}
<tr>
<td>
{% if rule.recommended %}
{% if rule.presets.recommended %}
<span class="fa fa-check"></span>
{% endif %}
{% if rule.document %}
{% if rule.presets.document %}
<span class="fa fa-file-text-o"></span>
{% endif %}
</td>
<td><a href="/{{ rule.url }}">{{ rule.name }}</a></td>
<td>{{ rule.summary | escape }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
{% endmacro -%}
......
......@@ -10,7 +10,9 @@ Array [
"messages": Array [
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "button",
},
"line": 1,
"message": "Prefer to use <button> instead of <input type=\\"button\\"> when adding buttons",
"offset": 13,
......
......@@ -7,3 +7,5 @@ Rules with <span class="fa fa-check"></span> are enabled by
`html-validate:recommended`.<br>
Rules with <span class="fa fa-file-text-o"></span> are enabled by
`html-validate:document`.
Additional presets can be compared in {@link rules/presets preset comparision}.
......@@ -4,20 +4,52 @@ name: prefer-button
summary: Prefer to use <button> instead of <input> for buttons
---
# prefer to use `<button>` (`prefer-button`)
# Prefer to use `<button>` (`prefer-button`)
HTML5 introduces the generic `<button>` element which replaces `<input type="button">` and similar constructs.
The `<button>` elements has some advantages:
- It can contain markup as content compared to the `value` attribute of `<input>` which can only hold text. Especially useful to add `<svg>` icons.
- The button text is a regular text node, no need to quote characters in the `value` attribute.
- Styling is easier, compare the selector `button` to `input[type="submit"], input[type="button"], ...`.
This rule will target the following input types:
- `<input type="button">`
- `<input type="submit">`
- `<input type="reset">`
- `<input type="image">`
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="prefer-button">
<input type="button">
<input type="button">
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="prefer-button">
<button type="button"></button>
<button type="button"></button>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"include": [],
"exclude": [],
}
```
### `include`
If set only types listed in this array generates errors.
### `exclude`
If set types listed in this array is ignored.
---
docType: presets
name: Configuration presets
---
# Configuration presets
HTML-validate comes with a few predefined presets.
Presets can be configured in `.htmlvalidate.json` using:
```json
{
"extends": ["html-validate:PRESET"]
}
```
Multiple presets can be set and will be enabled in the order they appear in `"extends"`.
See {@link usage configuration usage guide} for more details.
## Available presets
### `html-validate:recommended`
This is the default preset and enables most rules including standards validation, WCAG and best practices.
It is a superset of the other presets.
### `html-validate:standard`
Enables rules related to validating according to the WHATWG HTML standard (Living Standard).
Use this preset if you want validation similar to the Nu Html Checker and similar tools.
### `html-validate:a17y`
Enables rules related to accessibility.
Most rules but not all enabled rules relates to WCAG compliance.
On its own it will not validate if the document/template itself is valid but only if accessibility issues can be found.
This preset should be used togeher with `html-validate:standard` to ensure the document structure is valid (a requirement of WCAG) and if possible `html-validate:document` (to ensure references are valid, etc).
### `html-validate:document`
Enables rules requiring a full document to validate, i.e. not a partial template.
Examples include missing doctype and invalid references.
Use this preset together with other presets for full coverage.
This preset is enabled by plugins such as {@link usage/cypress cypress-html-validate} and {@link usage/protractor protractor-html-validate}.
## Comparision
......@@ -31,19 +31,31 @@ Run with:
### `extends`
Configuration can be extended from sharable configuration.
Configuration can be extended from bundled preset or sharable configurations.
```js
```json
{
"extends": [
/* bundled preset */
"html-validate:recommended",
/* npm package */
"my-npm-package",
"./file"
],
/* plugin with custom preset */
"my-plugin:custom",
/* local file */
"./file"
]
}
```
Each package and file must export a valid configuration object. Plugins may also
create [configuration presets](/dev/writing-plugins.html).
A list of bundled presets is available at the {@link rules/presets preset list}.
By default `html-validate:recommended` is used.
When using NPM packages and files each must export a valid configuration object.
Plugins may create [custom configuration presets](/dev/writing-plugins.html) by exposing one or more preset in the plugin declaration.
### `rules`
......
This diff is collapsed.
import path from "path";
import minimatch from "minimatch";
const glob: any = jest.requireActual("glob");
......@@ -9,7 +10,7 @@ interface Options {
let mockFiles: string[] = null;
function setMockFiles(files: string[]): void {
mockFiles = files;
mockFiles = files.map((cur) => path.normalize(cur));
}
function resetMock(): void {
......@@ -25,7 +26,10 @@ function syncMock(pattern: string, options: Options = {}): string[] {
/* slice the cwd away since it is prepended automatically and makes it harder
* to test with */
const cwd = options.cwd || "";
const dir = cwd.replace(process.cwd(), "").replace(/^\/(.*)/, "$1/");
const dir = cwd
.replace(process.cwd(), "")
.replace("\\", "/")
.replace(/^\/(.*)/, `$1${path.sep}`);
let src = mockFiles;
if (dir) {
......
......@@ -2,6 +2,7 @@ jest.mock("fs");
jest.mock("glob");
import fs from "fs";
import path from "path";
import glob from "glob";
import { CLI } from "./cli";
......@@ -41,9 +42,9 @@ describe("expandFiles()", () => {
expect.assertions(3);
const spy = jest.spyOn(glob, "sync");
expect(cli.expandFiles(["foo.html", "bar/**/*.html"])).toEqual([
"foo.html",
"bar/fred.html",
"bar/barney.html",
path.normalize("foo.html"),
path.normalize("bar/fred.html"),
path.normalize("bar/barney.html"),
]);
expect(spy).toHaveBeenCalledWith("foo.html", expect.anything());
expect(spy).toHaveBeenCalledWith("bar/**/*.html", expect.anything());
......@@ -52,26 +53,26 @@ describe("expandFiles()", () => {
it("should expand directories (default extensions)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"])).toEqual([
"bar/fred.html",
"bar/barney.html",
path.normalize("bar/fred.html"),
path.normalize("bar/barney.html"),
]);
});
it("should expand directories (explicit extensions)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"], { extensions: ["js", "json"] })).toEqual([
"bar/fred.json",
"bar/barney.js",
path.normalize("bar/fred.json"),
path.normalize("bar/barney.js"),
]);
});
it("should expand directories (no extensions => all files)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"], { extensions: [] })).toEqual([
"bar/fred.html",
"bar/fred.json",
"bar/barney.html",
"bar/barney.js",
path.normalize("bar/fred.html"),
path.normalize("bar/fred.json"),
path.normalize("bar/barney.html"),
path.normalize("bar/barney.js"),
]);
});
......
......@@ -27,9 +27,9 @@ function directoryPattern(extensions: string[]): string {
case 0:
return "**";
case 1:
return `**/*.${extensions[0]}`;
return path.join("**", `*.${extensions[0]}`);
default:
return `**/*.{${extensions.join(",")}}`;
return path.join("**", `*.{${extensions.join(",")}}`);
}
}
......
......@@ -13,6 +13,7 @@ import { ConfigData, TransformMap } from "./config-data";
import defaultConfig from "./default";
import { ConfigError } from "./error";
import { parseSeverity, Severity } from "./severity";
import Presets from "./presets";
interface TransformerEntry {
pattern: RegExp;
......@@ -28,9 +29,6 @@ interface LoadedPlugin extends Plugin {
originalName: string;
}
const recommended = require("./recommended");
const document = require("./document");
let rootDirCache: string = null;
const ajv = new Ajv({ jsonPointers: true });
......@@ -378,12 +376,9 @@ export class Config {
const configs: Map<string, ConfigData> = new Map();
/* builtin presets */
configs.set("html-validate:recommended", recommended);
configs.set("html-validate:document", document);
/* aliases for convenience */
configs.set("htmlvalidate:recommended", recommended);
configs.set("htmlvalidate:document", document);
for (const [name, config] of Object.entries(Presets)) {
configs.set(name, config);
}
/* presets from plugins */
for (const plugin of plugins) {
......
import { ConfigData } from "../config-data";
const config: ConfigData = {
rules: {
"deprecated-rule": "warn",
"empty-heading": "error",
"empty-title": "error",
"meta-refresh": "error",
"no-autoplay": ["error", { include: ["audio", "video"] }],
"no-dup-id": "error",
"no-redundant-role": "error",
"prefer-native-element": "error",
"svg-focusable": "error",
"wcag/h30": "error",
"wcag/h32": "error",
"wcag/h36": "error",
"wcag/h37": "error",
"wcag/h67": "error",
"wcag/h71": "error",
},
};
export = config;
module.exports = {
import { ConfigData } from "../config-data";
const config: ConfigData = {
rules: {
"input-missing-label": "error",
"heading-level": "error",
......@@ -7,3 +9,5 @@ module.exports = {
"require-sri": "error",
},
};
export = config;
import { ConfigData } from "../config-data";
import a17y from "./a17y";
import document from "./document";
import recommended from "./recommended";
import standard from "./standard";
const presets: Record<string, ConfigData> = {
"html-validate:a17y": a17y,
"html-validate:document": document,
"html-validate:recommended": recommended,
"html-validate:standard": standard,
/* @deprecated aliases */
"htmlvalidate:recommended": recommended,
"htmlvalidate:document": document,
};
export = presets;
module.exports = {
import { ConfigData } from "../config-data";
const config: ConfigData = {
rules: {
"attr-case": "error",
"attr-quotes": "error",
......@@ -51,3 +53,5 @@ module.exports = {
"wcag/h71": "error",
},
};
export = config;
import { ConfigData } from "../config-data";
const config: ConfigData = {
rules: {
"attribute-allowed-values": "error",
"close-attr": "error",
"close-order": "error",
deprecated: "error",
"deprecated-rule": "warn",
"doctype-html": "error",
"element-name": "error",
"element-permitted-content": "error",
"element-permitted-occurrences": "error",
"element-permitted-order": "error",
"element-required-attributes": "error",
"element-required-content": "error",
"no-deprecated-attr": "error",
"no-dup-attr": "error",
"no-dup-id": "error",
"no-raw-characters": ["error", { relaxed: true }],
"script-element": "error",
"unrecognized-char-ref": "error",
"void-content": "error",
},
};
export = config;
......@@ -5,7 +5,7 @@ import { HtmlElement } from "./dom";
import { Event } from "./event";
import { Parser } from "./parser";
import { Reporter } from "./reporter";
import { Rule, ruleDocumentationUrl } from "./rule";
import { Rule, ruleDocumentationUrl, IncludeExcludeOptions } from "./rule";
import { MetaTable } from "./meta";
interface RuleContext {
......@@ -221,6 +221,47 @@ describe("rule base class", () => {
expect(rule.documentation()).toBeNull();
});
describe("isKeywordIgnored()", () => {
class RuleWithOption extends Rule<void, IncludeExcludeOptions> {
public setup(): void {
/* do nothing */
}
}
let rule: RuleWithOption;
let options: IncludeExcludeOptions;
beforeEach(() => {
options = {
include: null,
exclude: null,
};
rule = new RuleWithOption(options);
});
it('should return true if keyword is not present in "include"', () => {
expect.assertions(2);
options.include = ["foo"];
expect(rule.isKeywordIgnored("foo")).toBeFalsy();
expect(rule.isKeywordIgnored("bar")).toBeTruthy();
});
it('should return true if keyword is present in "exclude"', () => {
expect.assertions(2);
options.exclude = ["foo"];
expect(rule.isKeywordIgnored("foo")).toBeTruthy();
expect(rule.isKeywordIgnored("bar")).toBeFalsy();
});
it('should return true if keyword satisfies both "include" and "exclude"', () => {
expect.assertions(2);
options.include = ["foo", "bar"];
options.exclude = ["bar"];
expect(rule.isKeywordIgnored("foo")).toBeFalsy();
expect(rule.isKeywordIgnored("bar")).toBeTruthy();
});
});
it("getTagsWithProperty() should lookup properties from metadata", () => {
expect.assertions(2);
const spy = jest.spyOn(meta, "getTagsWithProperty");
......
......@@ -27,6 +27,11 @@ export interface RuleDocumentation {
export type RuleConstructor<T, U> = new (options?: any) => Rule<T, U>;
export interface IncludeExcludeOptions {
include: string[] | null;
exclude: string[] | null;
}
export abstract class Rule<ContextType = void, OptionsType = void> {
private reporter: Reporter;
private parser: Parser;
......@@ -82,6 +87,49 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
return this.enabled && this.severity >= Severity.WARN;
}
/**
* Check if keyword is being ignored by the current rule configuration.
*
* This method requires the [[RuleOption]] type to include two properties:
*
* - include: string[] | null
* - exclude: string[] | null
*
* This methods checks if the given keyword is included by "include" but not
* excluded by "exclude". If any property is unset it is skipped by the
* condition. Usually the user would use either one but not both but there is
* no limitation to use both but the keyword must satisfy both conditions. If
* either condition fails `true` is returned.
*
* For instance, given `{ include: ["foo"] }` the keyword `"foo"` would match
* but not `"bar"`.
*
* Similarly, given `{ exclude: ["foo"] }` the keyword `"bar"` would match but
* not `"foo"`.
*
* @param keyword - Keyword to match against `include` and `exclude` options.
* @returns `true` if keyword is not present in `include` or is present in
* `exclude`.
*/
public isKeywordIgnored<T extends IncludeExcludeOptions>(
this: { options: T },
keyword: string
): boolean {
const { include, exclude } = this.options;
/* ignore keyword if not present in "include" */
if (include && !include.includes(keyword)) {
return true;
}
/* ignore keyword if present in "excludes" */
if (exclude && exclude.includes(keyword)) {
return true;
}
return false;
}
/**
* Find all tags which has enabled given property.
*/
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule prefer-button should contain documentation 1`] = `
Object {
"description": "Prefer to use the generic \`<button>\` element instead of \`<input>\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button smoketest 1`] = `
exports[`rule prefer-button default config smoketest 1`] = `
Array [
Object {
"errorCount": 4,
......@@ -15,7 +8,9 @@ Array [
"messages": Array [
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "button",
},
"line": 5,
"message": "Prefer to use <button> instead of <input type=\\"button\\"> when adding buttons",
"offset": 64,
......@@ -26,7 +21,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "submit",
},
"line": 8,
"message": "Prefer to use <button> instead of <input type=\\"submit\\"> when adding buttons",
"offset": 119,
......@@ -37,7 +34,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "reset",
},
"line": 11,
"message": "Prefer to use <button> instead of <input type=\\"reset\\"> when adding buttons",
"offset": 174,
......@@ -48,7 +47,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "image",
},
"line": 14,
"message": "Prefer to use <button> instead of <input type=\\"image\\"> when adding buttons",
"offset": 227,
......@@ -78,3 +79,45 @@ Array [
},
]
`;
exports[`rule prefer-button should contain contextual documentation for type "button" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"button\\">\` instead of \`\\"<input type=\\"button\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "image" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"button\\">\` instead of \`\\"<input type=\\"image\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "reset" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"reset\\">\` instead of \`\\"<input type=\\"reset\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "submit" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"submit\\">\` instead of \`\\"<input type=\\"submit\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "unknown" 1`] = `
Object {
"description": "Prefer to use \`<button>\` instead of \`\\"<input type=\\"unknown\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain documentation 1`] = `
Object {
"description": "Prefer to use the generic \`<button>\` element instead of \`<input>\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
......@@ -48,7 +48,7 @@ export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
/* ignore tagnames configured to be ignored */
const tagName = event.target.tagName;
if (this.isIgnored(tagName)) {
if (this.isKeywordIgnored(tagName)) {
return;
}
......@@ -63,20 +63,4 @@ export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
);
});
}
private isIgnored(tagName: string): boolean {
const { include, exclude } = this.options;
/* ignore tagnames not present in "include" */
if (include && !include.includes(tagName)) {
return true;
}
/* ignore tagnames present in "excludes" */
if (exclude && exclude.includes(tagName)) {
return true;
}
return false;
}
}
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
import { types } from "./prefer-button";
describe("rule prefer-button", () => {
let htmlvalidate: HtmlValidate;
describe("default config", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
});
});
it("should not report error when type attribute is missing type attribute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing type attribute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing value", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input type>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing value", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input type>");
expect(report).toBeValid();
});
it("should not report error when using regular input fields", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type="text">');
expect(report).toBeValid();
});
it("should not report error for dynamic attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<input dynamic-type="inputType">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it("should report error when using type button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="button">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="button"> when adding buttons'
);
});
it("should not report error when using regular input fields", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type="text">');
expect(report).toBeValid();
});
it("should report error when using type submit", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="submit">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="submit"> when adding buttons'
);
});
it("should report error when using type button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="button">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="button"> when adding buttons'
);
});
it("should report error when using type reset", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="reset">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="reset"> when adding buttons'
);
it("should report error when using type submit", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="submit">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="submit"> when adding buttons'
);
});
it("should report error when using type reset", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="reset">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="reset"> when adding buttons'
);
});
it("should report error when using type image", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="image">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="image"> when adding buttons'
);
});
it("smoketest", () => {
expect.assertions(1);
const report = htmlvalidate.validateFile(
"test-files/rules/prefer-button.html"
);
expect(report.results).toMatchSnapshot();
});
});
it("should report error when using type image", () => {
it("should not report error when type is excluded", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="image">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="image"> when adding buttons'
);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": ["error", { exclude: ["submit"] }] },
});
const valid = htmlvalidate.validateString('<input type="submit">');
const invalid = htmlvalidate.validateString('<input type="reset">');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("smoketest", () => {
expect.assertions(1);
const report = htmlvalidate.validateFile(
"test-files/rules/prefer-button.html"
);
expect(report.results).toMatchSnapshot();
it("should report error only for included types", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": ["error", { include: ["submit"] }] },
});
const valid = htmlvalidate.validateString('<input type="reset">');
const invalid = htmlvalidate.validateString('<input type="submit">');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("prefer-button")
).toMatchSnapshot();
});
describe("should contain contextual documentation", () => {
it.each([...types, "unknown"])('for type "%s"', (type) => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("prefer-button", null, { type })
).toMatchSnapshot();
});
});
});
import { TagCloseEvent } from "../event";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
import { DynamicValue } from "../dom";
export default class PreferButton extends Rule {
public documentation(): RuleDocumentation {
return {
interface RuleContext {
type: string;
}
interface RuleOptions {
include: string[] | null;
exclude: string[] | null;
}
export const types = ["button", "submit", "reset", "image"];
const replacement: Record<string, string> = {
button: '<button type="button">',
submit: '<button type="submit">',
reset: '<button type="reset">',
image: '<button type="button">',
};
const defaults: RuleOptions = {
include: null,
exclude: null,
};
export default class PreferButton extends Rule<RuleContext, RuleOptions> {
public constructor(options: RuleOptions) {
super({ ...defaults, ...options });
}
public documentation(context: RuleContext): RuleDocumentation {
const doc: RuleDocumentation = {
description: `Prefer to use the generic \`<button>\` element instead of \`<input>\`.`,
url: ruleDocumentationUrl(__filename),
};
if (context) {
const src = `<input type="${context.type}">`;
const dst = replacement[context.type] || `<button>`;
doc.description = `Prefer to use \`${dst}\` instead of \`"${src}\`.`;
}
return doc;
}
public setup(): void {
this.on("tag:close", (event: TagCloseEvent) => {
const node = event.previous;
this.on("attr", (event: AttributeEvent) => {
const node = event.target;
/* only handle input elements */
if (node.tagName !== "input") {
return;
}
const type = node.getAttribute("type");
/* sanity check: handle missing, boolean and dynamic attributes */
if (!event.value || event.value instanceof DynamicValue) {
return;
}
/* sanity check: handle missing and boolean attributes */
if (!type || type.value === null) {
/* ignore types configured to be ignored */
if (this.isKeywordIgnored(event.value)) {
return;
}
if (type.valueMatches(/^(button|submit|reset|image)$/, false)) {
this.report(
node,
`Prefer to use <button> instead of <input type="${type.value}"> when adding buttons`,
type.valueLocation
);
/* only values matching known type triggers error */
if (!types.includes(event.value)) {
return;
}
const context: RuleContext = { type: event.value };
const message = `Prefer to use <button> instead of <input type="${event.value}"> when adding buttons`;
this.report(node, message, event.valueLocation, context);
});
}
}
......@@ -101,7 +101,7 @@ export default class PreferNativeElement extends Rule<
}
private isIgnored(role: string): boolean {
const { mapping, include, exclude } = this.options;
const { mapping } = this.options;
/* ignore roles not mapped to native elements */
const replacement = mapping[role];
......@@ -109,17 +109,7 @@ export default class PreferNativeElement extends Rule<
return true;
}
/* ignore roles not present in "include" */
if (include && !include.includes(role)) {
return true;
}
/* ignore roles present in "excludes" */
if (exclude && exclude.includes(role)) {
return true;
}
return false;
return this.isKeywordIgnored(role);
}
private getLocation(event: AttributeEvent): Location {
......