Commits (13)
# html-validate changelog
## [4.3.0](https://gitlab.com/html-validate/html-validate/compare/v4.2.0...v4.3.0) (2021-01-19)
### Features
- **rules:** new rule `text-content` ([2fef395](https://gitlab.com/html-validate/html-validate/commit/2fef3950e5c2e407ca206fbcf82d90793488c2da)), closes [#101](https://gitlab.com/html-validate/html-validate/issues/101)
- **transform:** new helper `processElement` for writing tests ([3052f81](https://gitlab.com/html-validate/html-validate/commit/3052f81edcebca58551c77d378b2e5357db47f3a))
- add `browser` entry point without cli classes ([7840ec2](https://gitlab.com/html-validate/html-validate/commit/7840ec2a7f823c57e7e4f50055f4bb873f961dc7))
- set `sideEffects` to `false` ([41b47f8](https://gitlab.com/html-validate/html-validate/commit/41b47f8bc21501e4615cd8bc887a0ffaf2869454))
### Bug Fixes
- **dom:** `DOMTokenList` (such as `classlist`) handles newlines and tabs ([35e601e](https://gitlab.com/html-validate/html-validate/commit/35e601e22c6a04f93f252810caed6b8bbb182225))
## [4.2.0](https://gitlab.com/html-validate/html-validate/compare/v4.1.0...v4.2.0) (2021-01-15)
### Features
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/text-content.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/text-content.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 2,
"context": Object {
"tagName": "button",
"textContent": "accessible",
},
"line": 1,
"message": "<button> must have accessible text",
"offset": 1,
"ruleId": "text-content",
"selector": "button",
"severity": 2,
"size": 6,
},
],
"source": "<button type=\\"button\\"></button>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<button type="button"></button>`;
markup["correct"] = `<!-- regular static text -->
<button type="button">Add item</button>
<!-- text from aria-label -->
<button type="button" aria-label="Add item">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>`;
describe("docs/rules/text-content.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"text-content":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"text-content":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: text-content
category: a17y
summary: Require elements to have valid text content
---
# Require elements to have valid text (`text-content`)
Requires presence or absence of textual content on an element (or one of its children).
Whitespace is ignored.
It comes in three variants:
- Text must be absent.
- Text must be present.
- Text must be accessible (regular text or aria attributes).
Bundled HTML5 elements only specify accessible text but custom elements can specify others.
By default this rules validates:
- `<button>`
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="text-content">
<button type="button"></button>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="text-content">
<!-- regular static text -->
<button type="button">Add item</button>
<!-- text from aria-label -->
<button type="button" aria-label="Add item">
<i class="fa fa-plus" aria-hidden="true"></i>
</button>
</validate>
......@@ -51,6 +51,7 @@ export interface MetaElement {
permittedOrder?: PermittedOrder;
requiredAncestors?: string[];
requiredContent?: string[];
textContent?: "none" | "default" | "required" | "accessible";
/* inheritance */
inherit?: string;
......@@ -427,6 +428,20 @@ descendant of the element.
This is used by [element-required-content](/rules/element-required-content.html)
rule.
### `textContent`
Enforces presence or absence of text in an element.
If unset it defaults to `default`.
Must be set to one of the following values:
- `none` - the element cannot contain text (whitespace ignored).
- `default` - the element can optionally have text.
- `required` - the element must have non-whitespace text present.
- `accessible` - the element must have accessible text, either regular text or using aria attributes such as `aria-label`.
This is used by [text-content](/rules/text-content.html) rule.
## Global element
The special `*` element can be used to assign global metadata applying to all
......
......@@ -486,9 +486,9 @@ Array [
Object {
"column": 3,
"context": undefined,
"line": 22,
"line": 23,
"message": "Element <audio> is not permitted as descendant of <button>",
"offset": 456,
"offset": 464,
"ruleId": "element-permitted-content",
"selector": "button > audio",
"severity": 2,
......@@ -507,9 +507,9 @@ Array [
"element": "audio",
"value": "foobar",
},
"line": 26,
"line": 27,
"message": "Attribute \\"preload\\" has invalid value \\"foobar\\"",
"offset": 557,
"offset": 565,
"ruleId": "attribute-allowed-values",
"selector": "audio:nth-child(6)",
"severity": 2,
......@@ -537,6 +537,7 @@ Array [
<!-- should be interactive if missing controls attribute (and thus not allowed as content in button) -->
<button type=\\"button\\">
Audio:
<audio controls=\\"foo\\"></audio>
</button>
......@@ -898,7 +899,7 @@ exports[`HTML elements <br> valid markup 1`] = `Array []`;
exports[`HTML elements <button> invalid markup 1`] = `
Array [
Object {
"errorCount": 6,
"errorCount": 8,
"filePath": "test-files/elements/button-invalid.html",
"messages": Array [
Object {
......@@ -973,12 +974,40 @@ Array [
},
"line": 15,
"message": "<button> is missing required \\"type\\" attribute",
"offset": 453,
"offset": 456,
"ruleId": "element-required-attributes",
"selector": "button:nth-child(6)",
"severity": 2,
"size": 6,
},
Object {
"column": 2,
"context": Object {
"tagName": "button",
"textContent": "accessible",
},
"line": 18,
"message": "<button> must have accessible text",
"offset": 511,
"ruleId": "text-content",
"selector": "button:nth-child(7)",
"severity": 2,
"size": 6,
},
Object {
"column": 2,
"context": Object {
"tagName": "button",
"textContent": "accessible",
},
"line": 19,
"message": "<button> must have accessible text",
"offset": 543,
"ruleId": "text-content",
"selector": "button:nth-child(8)",
"severity": 2,
"size": 6,
},
],
"source": "<!-- should not allow flow as content -->
<button type=\\"button\\"><div>foo</div></button>
......@@ -991,10 +1020,14 @@ Array [
<button type=\\"button\\"><span><button type=\\"button\\">foo</button></span></button>
<!-- disallowed type variants -->
<button type=\\"foobar\\"></button>
<button type=\\"foobar\\">foo</button>
<!-- missing type -->
<button></button>
<button>foo</button>
<!-- missing accessible text -->
<button type=\\"button\\"></button>
<button type=\\"button\\"><span aria-hidden=\\"true\\">foo</span></button>
",
"warningCount": 0,
},
......@@ -3043,7 +3076,7 @@ Array [
<!-- should be interactive if usemap is set -->
<button type=\\"button\\">
<img src=\\"foo.png\\" usemap>
<img src=\\"foo.png\\" usemap aria-label=\\"Picture of a foo\\">
</button>
",
"warningCount": 0,
......@@ -3283,9 +3316,9 @@ Array [
Object {
"column": 3,
"context": undefined,
"line": 21,
"line": 22,
"message": "Element <input> is not permitted as descendant of <button>",
"offset": 594,
"offset": 599,
"ruleId": "element-permitted-content",
"selector": "button > input",
"severity": 2,
......@@ -3294,9 +3327,9 @@ Array [
Object {
"column": 20,
"context": undefined,
"line": 25,
"line": 26,
"message": "image used as submit button must have alt text",
"offset": 683,
"offset": 688,
"ruleId": "wcag/h36",
"selector": "input:nth-child(14)",
"severity": 2,
......@@ -3323,6 +3356,7 @@ Array [
<!-- should be interactive when type isn't hidden -->
<button type=\\"button\\">
foo
<input type=\\"text\\">
</button>
......@@ -3495,7 +3529,7 @@ Array [
"context": undefined,
"line": 16,
"message": "<label> is associated with multiple controls",
"offset": 265,
"offset": 271,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(4)",
"severity": 2,
......@@ -3506,7 +3540,7 @@ Array [
"context": undefined,
"line": 20,
"message": "<label> is associated with multiple controls",
"offset": 324,
"offset": 330,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(5)",
"severity": 2,
......@@ -3517,7 +3551,7 @@ Array [
"context": undefined,
"line": 24,
"message": "<label> is associated with multiple controls",
"offset": 361,
"offset": 367,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(6)",
"severity": 2,
......@@ -3528,7 +3562,7 @@ Array [
"context": undefined,
"line": 28,
"message": "<label> is associated with multiple controls",
"offset": 412,
"offset": 418,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(7)",
"severity": 2,
......@@ -3539,7 +3573,7 @@ Array [
"context": undefined,
"line": 32,
"message": "<label> is associated with multiple controls",
"offset": 467,
"offset": 473,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(8)",
"severity": 2,
......@@ -3550,7 +3584,7 @@ Array [
"context": undefined,
"line": 36,
"message": "<label> is associated with multiple controls",
"offset": 530,
"offset": 536,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(9)",
"severity": 2,
......@@ -3561,7 +3595,7 @@ Array [
"context": undefined,
"line": 40,
"message": "<label> is associated with multiple controls",
"offset": 585,
"offset": 591,
"ruleId": "multiple-labeled-controls",
"selector": "label:nth-child(10)",
"severity": 2,
......@@ -3580,8 +3614,8 @@ Array [
<!-- should not allow multiple controls -->
<label>
<button type=\\"button\\"></button>
<button type=\\"button\\"></button>
<button type=\\"button\\">foo</button>
<button type=\\"button\\">bar</button>
</label>
<label>
<input type=\\"text\\">
......@@ -4430,9 +4464,9 @@ Array [
Object {
"column": 3,
"context": undefined,
"line": 19,
"line": 20,
"message": "Element <object> is not permitted as descendant of <button>",
"offset": 305,
"offset": 310,
"ruleId": "element-permitted-content",
"selector": "button > object",
"severity": 2,
......@@ -4457,6 +4491,7 @@ Array [
<!-- should be interactive if usemap is set -->
<button type=\\"button\\">
foo
<object usemap></object>
</button>
",
......@@ -5308,9 +5343,9 @@ Array [
Object {
"column": 4,
"context": undefined,
"line": 4,
"line": 5,
"message": "Element <input> is not permitted as descendant of <button>",
"offset": 65,
"offset": 71,
"ruleId": "element-permitted-content",
"selector": "button > slot > input",
"severity": 2,
......@@ -5320,6 +5355,7 @@ Array [
"source": "<!-- should be transparent -->
<button type=\\"button\\">
<slot>
foo
<input type=\\"text\\">
</slot>
</button>
......@@ -6574,9 +6610,9 @@ Array [
Object {
"column": 3,
"context": undefined,
"line": 22,
"line": 23,
"message": "Element <video> is not permitted as descendant of <button>",
"offset": 456,
"offset": 464,
"ruleId": "element-permitted-content",
"selector": "button > video",
"severity": 2,
......@@ -6595,9 +6631,9 @@ Array [
"element": "video",
"value": "foobar",
},
"line": 26,
"line": 27,
"message": "Attribute \\"preload\\" has invalid value \\"foobar\\"",
"offset": 557,
"offset": 565,
"ruleId": "attribute-allowed-values",
"selector": "video:nth-child(6)",
"severity": 2,
......@@ -6625,6 +6661,7 @@ Array [
<!-- should be interactive if missing controls attribute (and thus not allowed as content in button) -->
<button type=\\"button\\">
Video:
<video controls=\\"foo\\"></video>
</button>
......
......@@ -197,7 +197,8 @@
"type": ["submit", "reset", "button"]
},
"permittedContent": ["@phrasing"],
"permittedDescendants": [{ "exclude": ["@interactive"] }]
"permittedDescendants": [{ "exclude": ["@interactive"] }],
"textContent": "accessible"
},
"canvas": {
......
{
"name": "html-validate",
"version": "4.2.0",
"version": "4.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -1747,9 +1747,9 @@
"dev": true
},
"@eslint/eslintrc": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz",
"integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz",
"integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
......@@ -1759,7 +1759,7 @@
"ignore": "^4.0.6",
"import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.19",
"lodash": "^4.17.20",
"minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1"
},
......@@ -2491,18 +2491,18 @@
}
},
"@octokit/openapi-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz",
"integrity": "sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.3.0.tgz",
"integrity": "sha512-Own8lHWVi5eEfLOnsIzAx16BoRbpkzac3QDUCxIqYMf4bjz+AGpv17UfRn1Va4lVmjwOpvZglpFI3mmxuQ+sIQ==",
"dev": true
},
"@octokit/plugin-paginate-rest": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz",
"integrity": "sha512-+zARyncLjt9b0FjqPAbJo4ss7HOlBi1nprq+cPlw5vu2+qjy7WvlXhtXFdRHQbSL1Pt+bfAKaLADEkkvg8sP8w==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.1.tgz",
"integrity": "sha512-dUsxsEIrBqhlQNfXRhMhXOTQi0SSG38+QWcPGO226HFPFJk44vWukegHfMG3496vLv9T2oT7IuAGssGpcUg5bQ==",
"dev": true,
"requires": {
"@octokit/types": "^6.0.1"
"@octokit/types": "^6.3.1"
}
},
"@octokit/plugin-request-log": {
......@@ -2569,12 +2569,12 @@
}
},
"@octokit/types": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.2.1.tgz",
"integrity": "sha512-jHs9OECOiZxuEzxMZcXmqrEO8GYraHF+UzNVH2ACYh8e/Y7YoT+hUf9ldvVd6zIvWv4p3NdxbQ0xx3ku5BnSiA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.3.1.tgz",
"integrity": "sha512-SyOaprLWVPS6QhbZY8hF9Oydx/UUnslKq1NyNUr4CN42UEPC3+9AvrYrDm4UvaU1D5u/vVMuSZOicFqOielRXQ==",
"dev": true,
"requires": {
"@octokit/openapi-types": "^2.2.0",
"@octokit/openapi-types": "^2.3.0",
"@types/node": ">= 8"
}
},
......@@ -2902,9 +2902,9 @@
}
},
"mime": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz",
"integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz",
"integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==",
"dev": true
},
"ms": {
......@@ -3516,9 +3516,9 @@
"dev": true
},
"@types/node": {
"version": "11.15.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.43.tgz",
"integrity": "sha512-EAUAH6KQiz/aLjyaLgfcKdcNCmuPTqPeaZHMg3bxbDUeZXnAABqsT+jQLqvINEN76+0HRi9pcuZQlvV3cy4kow==",
"version": "11.15.44",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.44.tgz",
"integrity": "sha512-fHAcNn2FlzDMA9rV5KP73r1cMZdbommWic2foHAEWoa/LITi91AueHDsJpnqjBEJ7bYoT2WC+KN1RL0vsM20zA==",
"dev": true
},
"@types/normalize-package-data": {
......@@ -7701,13 +7701,13 @@
}
},
"eslint": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz",
"integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz",
"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@eslint/eslintrc": "^0.2.2",
"@eslint/eslintrc": "^0.3.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
......@@ -7731,7 +7731,7 @@
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash": "^4.17.19",
"lodash": "^4.17.20",
"minimatch": "^3.0.4",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
......@@ -10523,9 +10523,9 @@
"dev": true
},
"husky": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.7.tgz",
"integrity": "sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw==",
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
"integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
......@@ -21068,9 +21068,9 @@
}
},
"semantic-release": {
"version": "17.3.2",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-17.3.2.tgz",
"integrity": "sha512-S8ppLzFFeznDXvRGEvltuwpRT7Tb7o3gBIqaVnahi0sAzrxD/WdWZQoHOpwypzTBD7i9gCxZ+RqVpaHa9WFD3w==",
"version": "17.3.3",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-17.3.3.tgz",
"integrity": "sha512-StUBghTuh98O0XpDWj5aRLmZwY9zV6gGgbXnw5faon3Br1fRk8r2VSaYozfQTx8o5C8Mtem+a0KWEiTTd4Iylw==",
"dev": true,
"requires": {
"@semantic-release/commit-analyzer": "^8.0.0",
......@@ -22726,9 +22726,9 @@
}
},
"table": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/table/-/table-6.0.6.tgz",
"integrity": "sha512-OInCtPmDNieVBkVFi6C8RwU2S2H0h8mF3e3TQK4nreaUNCpooQUkI+A/KuEkm5FawfhWIfNqG+qfelVVR+V00g==",
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
"integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==",
"dev": true,
"requires": {
"ajv": "^7.0.2",
......
{
"name": "html-validate",
"version": "4.2.0",
"version": "4.3.0",
"description": "html linter",
"keywords": [
"html",
......@@ -18,7 +18,9 @@
},
"license": "MIT",
"author": "David Sveningsson <ext@sidvind.com>",
"main": "dist/shim.js",
"sideEffects": false,
"main": "dist/main.js",
"browser": "dist/browser.js",
"bin": {
"html-validate": "bin/html-validate.js"
},
......@@ -73,7 +75,8 @@
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/index.ts",
"!src/shim.ts",
"!src/main.ts",
"!src/browser.ts",
"!src/cli/html-validate.ts"
],
"preset": "@html-validate/jest-config",
......@@ -120,7 +123,7 @@
"@types/jest": "26.0.20",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.43",
"@types/node": "11.15.44",
"@types/prompts": "2.0.9",
"autoprefixer": "10.2.1",
"babelify": "10.0.0",
......@@ -130,7 +133,7 @@
"dgeni": "0.4.13",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint": "7.17.0",
"eslint": "7.18.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"font-awesome": "4.7.0",
......@@ -142,7 +145,7 @@
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.5.0",
"husky": "4.3.7",
"husky": "4.3.8",
"jest": "26.6.3",
"jest-diff": "26.6.2",
"jquery": "3.5.1",
......@@ -155,7 +158,7 @@
"prettier": "2.2.1",
"pretty-format": "26.6.2",
"sass": "1.32.4",
"semantic-release": "17.3.2",
"semantic-release": "17.3.3",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
......
......@@ -2,7 +2,6 @@
export { default as HtmlValidate } from "./htmlvalidate";
export { AttributeData } from "./parser";
export { CLI } from "./cli/cli";
export { Config, ConfigData, ConfigError, ConfigLoader, Severity } from "./config";
export { DynamicValue, HtmlElement, NodeClosed, TextNode } from "./dom";
export { UserError } from "./error";
......
......@@ -13,6 +13,7 @@ const config: ConfigData = {
"no-redundant-role": "error",
"prefer-native-element": "error",
"svg-focusable": "error",
"text-content": "error",
"wcag/h30": "error",
"wcag/h32": "error",
"wcag/h36": "error",
......
......@@ -44,6 +44,7 @@ const config: ConfigData = {
"script-element": "error",
"script-type": "error",
"svg-focusable": "error",
"text-content": "error",
"unrecognized-char-ref": "error",
void: "off",
"void-content": "error",
......
......@@ -23,6 +23,18 @@ describe("DOMTokenList", () => {
expect(Array.from(list)).toEqual(["foo", "bar", "baz"]);
});
it("should handle newlines", () => {
expect.assertions(1);
const list = new DOMTokenList("foo\nbar\r\nbaz", location);
expect(Array.from(list)).toEqual(["foo", "bar", "baz"]);
});
it("should handle tabs", () => {
expect.assertions(1);
const list = new DOMTokenList("foo\tbar", location);
expect(Array.from(list)).toEqual(["foo", "bar"]);
});
it("should handle leading and trailing spaces", () => {
expect.assertions(1);
const list = new DOMTokenList(" foo bar baz ", location);
......
......@@ -47,7 +47,9 @@ export class DOMTokenList extends Array<string> {
public constructor(value: string | DynamicValue | null, location: Location | null) {
if (value && typeof value === "string") {
const { tokens, locations } = parse(value, location);
/* replace all whitespace with a single space for easier parsing */
const condensed = value.replace(/[\t\r\n ]+/g, " ");
const { tokens, locations } = parse(condensed, location);
super(...tokens);
this.locations = locations;
} else {
......
/* used when calling require('htmlvalidate'); */
export * from "./browser";
export { CLI } from "./cli/cli";
......@@ -223,6 +223,7 @@ describe("toHTMLValidate()", () => {
Anchor link must have a text describing its purpose [wcag/h30]
<button> is missing required \\"type\\" attribute [element-required-attributes]
<button> must have accessible text [text-content]
Element <button> is not permitted as descendant of <a> [element-permitted-content]
Mismatched close-tag, expected '</button>' but found '</i>'. [close-order]
Missing close-tag, expected '</a>' but document ended before it was found. [close-order]"
......
......@@ -10,6 +10,20 @@ export type PermittedOrder = string[];
export type RequiredAncestors = string[];
export type RequiredContent = string[];
export enum TextContent {
/* forbid node to have text content, inter-element whitespace is ignored */
NONE = "none",
/* node can have text but not required too */
DEFAULT = "default",
/* node requires text-nodes to be present (direct or by descendant) */
REQUIRED = "required",
/* node requires accessible text (hidden text is ignored, tries to get text from accessibility tree) */
ACCESSIBLE = "accessible",
}
export interface PermittedAttribute {
[key: string]: Array<string | RegExp>;
}
......@@ -54,6 +68,7 @@ export interface MetaData {
permittedOrder?: PermittedOrder;
requiredAncestors?: RequiredAncestors;
requiredContent?: RequiredContent;
textContent?: TextContent;
}
/**
......
export { MetaTable } from "./table";
export {
ElementTable,
MetaCopyableProperty,
MetaData,
MetaDataTable,
MetaElement,
ElementTable,
MetaLookupableProperty,
MetaCopyableProperty,
PropertyExpression,
TextContent,
} from "./element";
export { Validator } from "./validator";
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule text-content should contain contextual documentation accessible 1`] = `
Object {
"description": "The \`<my-element>\` element must have accessible text.",
"url": "https://html-validate.org/rules/text-content.html",
}
`;
exports[`rule text-content should contain contextual documentation default 1`] = `
Object {
"description": "The textual content for this element is not valid.",
"url": "https://html-validate.org/rules/text-content.html",
}
`;
exports[`rule text-content should contain contextual documentation none 1`] = `
Object {
"description": "The \`<my-element>\` element must not have textual content.",
"url": "https://html-validate.org/rules/text-content.html",
}
`;
exports[`rule text-content should contain contextual documentation required 1`] = `
Object {
"description": "The \`<my-element>\` element must have textual content.",
"url": "https://html-validate.org/rules/text-content.html",
}
`;
exports[`rule text-content should contain documentation 1`] = `
Object {
"description": "The textual content for this element is not valid.",
"url": "https://html-validate.org/rules/text-content.html",
}
`;
......@@ -51,6 +51,7 @@ import RequireSri from "./require-sri";
import ScriptElement from "./script-element";
import ScriptType from "./script-type";
import SvgFocusable from "./svg-focusable";
import TextContent from "./text-content";
import UnrecognizedCharRef from "./unrecognized-char-ref";
import Void from "./void";
import VoidContent from "./void-content";
......@@ -110,6 +111,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"script-element": ScriptElement,
"script-type": ScriptType,
"svg-focusable": SvgFocusable,
"text-content": TextContent,
"unrecognized-char-ref": UnrecognizedCharRef,
void: Void,
"void-content": VoidContent,
......