Commit 3602be73 authored by David Sveningsson's avatar David Sveningsson

fix(rules): better and more contextual messages for `deprecated`

parent 7fbf433f
......@@ -74,7 +74,9 @@ Array [
},
Object {
"column": 4,
"context": undefined,
"context": Object {
"tagName": "my-deprecated",
},
"line": 3,
"message": "<my-deprecated> is deprecated: replaced with <my-other>",
"offset": 39,
......@@ -123,7 +125,11 @@ Array [
},
Object {
"column": 6,
"context": undefined,
"context": Object {
"documentation": "\`<blink>\` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard",
"tagName": "blink",
},
"line": 8,
"message": "<blink> is deprecated",
"offset": 138,
......
......@@ -10,7 +10,9 @@ Array [
"messages": Array [
Object {
"column": 2,
"context": undefined,
"context": Object {
"tagName": "my-element",
},
"line": 1,
"message": "<my-element> is deprecated: replaced with <other-element>",
"offset": 1,
......@@ -34,9 +36,14 @@ Array [
"messages": Array [
Object {
"column": 2,
"context": undefined,
"context": Object {
"documentation": "Use the CSS \`text-align\` or \`margin: auto\` properties instead.",
"message": "use CSS instead",
"source": "html4",
"tagName": "center",
},
"line": 1,
"message": "<center> is deprecated",
"message": "<center> is deprecated: use CSS instead",
"offset": 1,
"ruleId": "deprecated",
"selector": "center",
......@@ -45,9 +52,14 @@ Array [
},
Object {
"column": 2,
"context": undefined,
"context": Object {
"documentation": "Use CSS \`font-size\` property instead.",
"message": "use CSS instead",
"source": "html5",
"tagName": "big",
},
"line": 2,
"message": "<big> is deprecated",
"message": "<big> is deprecated: use CSS instead",
"offset": 22,
"ruleId": "deprecated",
"selector": "big",
......
......@@ -32,7 +32,7 @@ export interface MetaElement {
interactive: boolean | PropertyExpression;
/* element properties */
deprecated: boolean | string;
deprecated: boolean | string | DeprecatedElement;
foreign: boolean;
void: boolean;
transparent: boolean;
......@@ -101,8 +101,35 @@ The available evaluators are:
### `deprecated`
If true the element will trigger the [deprecated](/rules/deprecated.html) rule
when used. Can optionally be set to a string which will be displayed as well.
If truthy the element will trigger the [deprecated](/rules/deprecated.html) rule
when used.
Can be set to `true`, a string or an object.
```typescript
interface DeprecatedElement {
message?: string;
documentation?: string;
source?: string;
}
```
Setting `true` is the same as the empty object.
Setting a string is the same as setting `message` to the string.
If the `message` property is set the text will be displayed in the error message.
If the `documentation` property is set the text will be rendered as markdown and shown in the contextual documentation shown by editors.
`$tagname` can be used as a placeholder in the text.
If the `source` property is set it is used to help the end-user to understand when and by what it is deprecated.
It can be set to:
- HTML major revision, e.g. `html5` or `html4`.
- HTML minor revision, e.g. `html53` or `html32`.
- `whatwg`: WHATWG Living Standard.
- `non-standard`: non-standard element implemented only by one or few browsers.
- your library or application name.
### `foreign`
......
This diff is collapsed.
......@@ -39,7 +39,11 @@
},
"acronym": {
"deprecated": true
"deprecated": {
"message": "use <abbr> instead",
"documentation": "`<abbr>` can be used as a replacement.",
"source": "html5"
}
},
"address": {
......@@ -51,7 +55,9 @@
},
"applet": {
"deprecated": true,
"deprecated": {
"source": "html5"
},
"deprecatedAttributes": ["datasrc", "datafld"]
},
......@@ -106,7 +112,11 @@
},
"basefont": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS `font-size` property instead.",
"source": "html4"
}
},
"bdi": {
......@@ -122,15 +132,26 @@
},
"bgsound": {
"deprecated": true
"deprecated": {
"message": "use <audio> instead",
"documentation": "Use the `<audio>` element instead but consider accessibility concerns with autoplaying sounds.",
"source": "non-standard"
}
},
"big": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS `font-size` property instead.",
"source": "html5"
}
},
"blink": {
"deprecated": true
"deprecated": {
"documentation": "`<blink>` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard"
}
},
"blockquote": {
......@@ -192,7 +213,11 @@
},
"center": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use the CSS `text-align` or `margin: auto` properties instead.",
"source": "html4"
}
},
"cite": {
......@@ -255,7 +280,10 @@
},
"dir": {
"deprecated": true
"deprecated": {
"documentation": "The non-standard `<dir>` element has no direct replacement but MDN recommends replacing with `<ul>` and CSS.",
"source": "html4"
}
},
"div": {
......@@ -318,7 +346,11 @@
},
"font": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS font properties instead.",
"source": "html4"
}
},
"footer": {
......@@ -341,13 +373,19 @@
},
"frame": {
"deprecated": true,
"deprecated": {
"documentation": "The `<frame>` element can be replaced with the `<iframe>` element but a better solution is to remove usage of frames entirely.",
"source": "html5"
},
"deprecatedAttributes": ["datasrc", "datafld"],
"requiredAttributes": ["title"]
},
"frameset": {
"deprecated": true
"deprecated": {
"documentation": "The `<frameset>` element can be replaced with the `<iframe>` element but a better solution is to remove usage of frames entirely.",
"source": "html5"
}
},
"h1": {
......@@ -547,7 +585,9 @@
},
"isindex": {
"deprecated": true
"deprecated": {
"source": "html4"
}
},
"kbd": {
......@@ -596,7 +636,9 @@
},
"listing": {
"deprecated": true
"deprecated": {
"source": "html32"
}
},
"main": {
......@@ -616,7 +658,10 @@
},
"marquee": {
"deprecated": true,
"deprecated": {
"documentation": "Marked as obsolete by both W3C and WHATWG standards but still implemented in most browsers. Animated text should be avoided for accessibility reasons as well.",
"source": "html5"
},
"deprecatedAttributes": ["datasrc", "datafld", "dataformatas"]
},
......@@ -647,7 +692,11 @@
},
"multicol": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS columns instead.",
"source": "html5"
}
},
"nav": {
......@@ -658,19 +707,29 @@
},
"nextid": {
"deprecated": true
"deprecated": {
"source": "html32"
}
},
"nobr": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS `white-space` property instead.",
"source": "non-standard"
}
},
"noembed": {
"deprecated": true
"deprecated": {
"source": "non-standard"
}
},
"noframes": {
"deprecated": true
"deprecated": {
"source": "html5"
}
},
"noscript": {
......@@ -792,7 +851,11 @@
},
"plaintext": {
"deprecated": true
"deprecated": {
"message": "use <pre> or CSS instead",
"documentation": "Use the `<pre>` element or use CSS to set a monospace font.",
"source": "html2"
}
},
"pre": {
......@@ -912,7 +975,11 @@
},
"spacer": {
"deprecated": true
"deprecated": {
"message": "use CSS instead",
"documentation": "Use CSS margin or padding instead.",
"source": "non-standard"
}
},
"span": {
......@@ -923,7 +990,11 @@
},
"strike": {
"deprecated": true
"deprecated": {
"message": "use <del> or <s> instead",
"documentation": "Use the `<del>` or `<s>` element instead.",
"source": "html5"
}
},
"strong": {
......@@ -1128,7 +1199,10 @@
},
"tt": {
"deprecated": true
"deprecated": {
"documentation": "Use a more semantically correct element such as `<code>`, `<var>` or `<pre>`.",
"source": "html4"
}
},
"u": {
......@@ -1170,6 +1244,9 @@
},
"xmp": {
"deprecated": true
"deprecated": {
"documentation": "Use `<pre>` or `<code>` and escape content using HTML entities instead.",
"source": "html32"
}
}
}
......@@ -175,7 +175,11 @@ Array [
"messages": Array [
Object {
"column": 2,
"context": undefined,
"context": Object {
"documentation": "\`<blink>\` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard",
"tagName": "blink",
},
"line": 1,
"message": "<blink> is deprecated",
"offset": 1,
......@@ -291,7 +295,9 @@ Array [
"messages": Array [
Object {
"column": 3,
"context": undefined,
"context": Object {
"tagName": "deprecated",
},
"line": 2,
"message": "<deprecated> is deprecated",
"offset": 8,
......
......@@ -14,6 +14,12 @@ export interface PermittedAttribute {
[key: string]: Array<string | RegExp>;
}
export interface DeprecatedElement {
message?: string;
documentation?: string;
source?: string;
}
export interface MetaData {
/* special keyword to extend metadata from another entry */
inherit?: string;
......@@ -28,7 +34,7 @@ export interface MetaData {
interactive: boolean | PropertyExpression;
/* element properties */
deprecated: boolean | string;
deprecated: boolean | string | DeprecatedElement;
foreign: boolean;
void: boolean;
transparent: boolean;
......@@ -98,6 +104,7 @@ export interface MetaElement extends MetaData {
[key: string]:
| undefined
| boolean
| DeprecatedElement
| PropertyExpression
| Permitted
| PermittedOrder
......
......@@ -2,14 +2,37 @@
exports[`rule deprecated should contain contextual documentation 1`] = `
Object {
"description": "HTML5 has deprecated the \`<center>\` element. It should not be used in new code.",
"description": "The \`<center>\` element is deprecated and should not be used in new code.",
"url": "https://html-validate.org/rules/deprecated.html",
}
`;
exports[`rule deprecated should contain contextual documentation 2`] = `
Object {
"description": "The \`<blink>\` element is deprecated in HTML 5 and should not be used in new code.",
"url": "https://html-validate.org/rules/deprecated.html",
}
`;
exports[`rule deprecated should contain contextual documentation 3`] = `
Object {
"description": "The \`<blink>\` element is deprecated in HTML 4.1 and should not be used in new code.",
"url": "https://html-validate.org/rules/deprecated.html",
}
`;
exports[`rule deprecated should contain contextual documentation 4`] = `
Object {
"description": "The \`<blink>\` element is deprecated and should not be used in new code.
extra documentation for <blink>",
"url": "https://html-validate.org/rules/deprecated.html",
}
`;
exports[`rule deprecated should contain documentation 1`] = `
Object {
"description": "HTML5 has deprecated many old elements and they should not be used in new code.",
"description": "This element is deprecated and should not be used in new code.",
"url": "https://html-validate.org/rules/deprecated.html",
}
`;
......@@ -22,7 +45,11 @@ Array [
"messages": Array [
Object {
"column": 2,
"context": undefined,
"context": Object {
"documentation": "Marked as obsolete by both W3C and WHATWG standards but still implemented in most browsers. Animated text should be avoided for accessibility reasons as well.",
"source": "html5",
"tagName": "marquee",
},
"line": 2,
"message": "<marquee> is deprecated",
"offset": 20,
......
......@@ -47,13 +47,69 @@ describe("rule deprecated", () => {
expect(report.results).toMatchSnapshot();
});
describe("metadata variants", () => {
it.each`
description | deprecated | message | documentation
${"boolean"} | ${true} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated and should not be used in new code."}
${"string"} | ${"lorem ipsum"} | ${"<my-element> is deprecated: lorem ipsum"} | ${"The `<my-element>` element is deprecated and should not be used in new code."}
${"message"} | ${{ message: "lorem ipsum" }} | ${"<my-element> is deprecated: lorem ipsum"} | ${"The `<my-element>` element is deprecated and should not be used in new code."}
${"documentation"} | ${{ documentation: "Lorem ipsum." }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated and should not be used in new code.\n\nLorem ipsum."}
${"source html5"} | ${{ source: "html5" }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated in HTML 5 and should not be used in new code."}
${"source html53"} | ${{ source: "html53" }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated in HTML 5.3 and should not be used in new code."}
${"source whatwg"} | ${{ source: "whatwg" }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated in HTML Living Standard and should not be used in new code."}
${"source non-standard"} | ${{ source: "non-standard" }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated and non-standard and should not be used in new code."}
${"source lib"} | ${{ source: "my-lib" }} | ${"<my-element> is deprecated"} | ${"The `<my-element>` element is deprecated by my-lib and should not be used in new code."}
`("$description", ({ deprecated, message, documentation }) => {
const htmlvalidate = new HtmlValidate({
rules: { deprecated: "error" },
elements: [
"html5",
{
"my-element": {
deprecated,
},
},
],
});
const report = htmlvalidate.validateString("<my-element></my-element>");
expect(report).toHaveError("deprecated", message);
const context = report.results[0].messages[0].context;
const doc = htmlvalidate.getRuleDocumentation(
"deprecated",
null,
context
);
expect(doc.description).toEqual(documentation);
});
});
it("should contain documentation", () => {
expect(htmlvalidate.getRuleDocumentation("deprecated")).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
expect(
htmlvalidate.getRuleDocumentation("deprecated", null, "center")
htmlvalidate.getRuleDocumentation("deprecated", null, {
tagName: "center",
})
).toMatchSnapshot();
expect(
htmlvalidate.getRuleDocumentation("deprecated", null, {
tagName: "blink",
source: "html5",
})
).toMatchSnapshot();
expect(
htmlvalidate.getRuleDocumentation("deprecated", null, {
tagName: "blink",
source: "html41",
})
).toMatchSnapshot();
expect(
htmlvalidate.getRuleDocumentation("deprecated", null, {
tagName: "blink",
documentation: "extra documentation for <$tagname>",
})
).toMatchSnapshot();
});
});
import { sliceLocation } from "../context";
import { sliceLocation, Location } from "../context";
import { HtmlElement } from "../dom";
import { TagOpenEvent } from "../event";
import { DeprecatedElement } from "../meta/element";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
class Deprecated extends Rule<string> {
public documentation(context?: string): RuleDocumentation {
interface Context extends DeprecatedElement {
tagName: string;
}
class Deprecated extends Rule<Context> {
public documentation(context?: Context): RuleDocumentation {
const doc: RuleDocumentation = {
description:
"HTML5 has deprecated many old elements and they should not be used in new code.",
"This element is deprecated and should not be used in new code.",
url: ruleDocumentationUrl(__filename),
};
if (context) {
doc.description = `HTML5 has deprecated the \`<${context}>\` element. It should not be used in new code.`;
const text: string[] = [];
if (context.source) {
const source = prettySource(context.source);
const message = `The \`<$tagname>\` element is deprecated ${source} and should not be used in new code.`;
text.push(message);
} else {
const message = `The \`<$tagname>\` element is deprecated and should not be used in new code.`;
text.push(message);
}
if (context.documentation) {
text.push(context.documentation);
}
doc.description = text
.map(cur => cur.replace(/\$tagname/g, context.tagName))
.join("\n\n");
}
return doc;
}
......@@ -27,18 +47,62 @@ class Deprecated extends Rule<string> {
const deprecated = node.meta.deprecated;
if (deprecated) {
const location = sliceLocation(event.location, 1);
if (typeof deprecated === "string") {
this.report(
node,
`<${node.tagName}> is deprecated: ${deprecated}`,
location
);
this.reportString(deprecated, node, location);
} else if (typeof deprecated === "boolean") {
this.reportBoolean(node, location);
} else {
this.report(node, `<${node.tagName}> is deprecated`, location);
this.reportObject(deprecated, node, location);
}
}
});
}
private reportString(
deprecated: string,
node: HtmlElement,
location: Location
): void {
const context: Context = { tagName: node.tagName };
const message = `<${node.tagName}> is deprecated: ${deprecated}`;
this.report(node, message, location, context);
}
private reportBoolean(node: HtmlElement, location: Location): void {
const context: Context = { tagName: node.tagName };
const message = `<${node.tagName}> is deprecated`;
this.report(node, message, location, context);
}
private reportObject(
deprecated: DeprecatedElement,
node: HtmlElement,
location: Location
): void {
const context: Context = { ...deprecated, tagName: node.tagName };
const message = `<${node.tagName}> is deprecated${
deprecated.message ? `: ${deprecated.message}` : ""
}`;
this.report(node, message, location, context);
}
}
function prettySource(source: string): string {
const match = source.match(/html(\d)(\d)?/);
if (match) {
const [, major, minor] = match;
return `in HTML ${major}${minor ? `.${minor}` : ""}`;
}
switch (source) {
case "whatwg":
return "in HTML Living Standard";
case "non-standard":
return "and non-standard";
default:
return `by ${source}`;
}
}
module.exports = Deprecated;
......@@ -59,7 +59,11 @@
"deprecated": {
"title": "Mark element as deprecated",
"description": "Deprecated elements should not be used. If a message is provided it will be included in the error",
"anyOf": [{ "type": "boolean" }, { "type": "string" }]
"anyOf": [
{ "type": "boolean" },
{ "type": "string" },
{ "$ref": "#/definitions/deprecatedElement" }
]
},
"foreign": {
......@@ -178,6 +182,26 @@
]
},
"deprecatedElement": {
"type": "object",
"additionalProperties": false,
"properties": {
"message": {
"type": "string",
"title": "A short text message shown next to the regular error message."
},
"documentation": {
"type": "string",
"title": "An extended markdown formatted message shown with the contextual rule documentation."
},
"source": {
"type": "string",
"title": "Element source, e.g. what standard or library deprecated this element.",
"default": "html5"
}
}
},
"Permitted": {
"type": "array",
"items": {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment