From 5a4aece5630627ad8041f9f36484ffa0887697aa Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 13 Apr 2025 21:16:05 +0900 Subject: [PATCH 01/11] poc: add use-standard-html --- package.json | 5 +- packages/eslint-plugin/lib/rules/index.js | 2 + .../lib/rules/use-standard-html.js | 57 +++++++++++++++++++ packages/eslint-plugin/package.json | 3 +- .../tests/rules/use-standard-html.test.js | 13 +++++ yarn.lock | 16 ++++++ 6 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/lib/rules/use-standard-html.js create mode 100644 packages/eslint-plugin/tests/rules/use-standard-html.test.js diff --git a/package.json b/package.json index 38f736a5..4bd0321f 100644 --- a/package.json +++ b/package.json @@ -49,5 +49,8 @@ "packageManager": "yarn@4.0.2", "workspaces": [ "packages/**" - ] + ], + "dependencies": { + "html-standard": "^0.0.2" + } } diff --git a/packages/eslint-plugin/lib/rules/index.js b/packages/eslint-plugin/lib/rules/index.js index ec04b013..d74fff0f 100644 --- a/packages/eslint-plugin/lib/rules/index.js +++ b/packages/eslint-plugin/lib/rules/index.js @@ -47,6 +47,7 @@ const maxElementDepth = require("./max-element-depth"); const requireExplicitSize = require("./require-explicit-size"); const useBaseLine = require("./use-baseline"); const noDuplicateClass = require("./no-duplicate-class"); +const useStandardHtml = require("./use-standard-html"); // import new rule here ↑ // DO NOT REMOVE THIS COMMENT @@ -100,6 +101,7 @@ module.exports = { "require-explicit-size": requireExplicitSize, "use-baseline": useBaseLine, "no-duplicate-class": noDuplicateClass, + "use-standard-html": useStandardHtml, // export new rule here ↑ // DO NOT REMOVE THIS COMMENT }; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html.js new file mode 100644 index 00000000..2338df38 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/use-standard-html.js @@ -0,0 +1,57 @@ +/** + * @typedef { import("../types").RuleFixer } RuleFixer + * @typedef { import("@html-eslint/types").Attribute } Attribute + * @typedef { import("@html-eslint/types").Text } Text + * + * @typedef {Object} Option + * @property {string[]} [Option.priority] + * @typedef { import("../types").RuleModule<[Option]> } RuleModule + */ + +const { RULE_CATEGORY } = require("../constants"); +const { getElementSpec } = require("html-standard"); + +const MESSAGE_IDS = { + UNSORTED: "unsorted", +}; + +/** + * @type {RuleModule} + */ +module.exports = { + meta: { + type: "code", + + docs: { + description: "TBD", + category: RULE_CATEGORY.BEST_PRACTICE, + recommended: false, + }, + fixable: "code", + schema: [ + { + type: "object", + properties: { + priority: { + type: "array", + items: { + type: "string", + uniqueItems: true, + }, + }, + }, + }, + ], + messages: { + [MESSAGE_IDS.UNSORTED]: "TBD", + }, + }, + create(context) { + return { + Tag(node) { + const spec = getElementSpec(node.name); + console.log(spec); + }, + }; + }, +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 3b54a083..85b3a41b 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -40,7 +40,8 @@ "dependencies": { "@eslint/plugin-kit": "0.2.8", "@html-eslint/template-parser": "^0.40.0", - "@html-eslint/template-syntax-parser": "^0.40.0" + "@html-eslint/template-syntax-parser": "^0.40.0", + "html-standard": "^0.0.3" }, "devDependencies": { "@eslint/core": "0.13.0", diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js new file mode 100644 index 00000000..7ecb6053 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -0,0 +1,13 @@ +const createRuleTester = require("../rule-tester"); +const rule = require("../../lib/rules/use-standard-html"); + +const ruleTester = createRuleTester(); + +ruleTester.run("use-standard-html", rule, { + valid: [ + { + code: ``, + }, + ], + invalid: [], +}); diff --git a/yarn.lock b/yarn.lock index 98be0881..e0f56e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1305,6 +1305,7 @@ __metadata: es-html-parser: "npm:0.2.0" eslint: "npm:^9.21.0" espree: "npm:^10.3.0" + html-standard: "npm:^0.0.3" typescript: "npm:^5.7.2" languageName: unknown linkType: soft @@ -1320,6 +1321,7 @@ __metadata: eslint: "npm:^9.19.0" eslint-plugin-jest: "npm:^28.11.0" eslint-plugin-n: "npm:^17.15.1" + html-standard: "npm:^0.0.2" husky: "npm:^9.1.4" jest: "npm:^29.7.0" lerna: "npm:^8.1.9" @@ -8818,6 +8820,20 @@ __metadata: languageName: node linkType: hard +"html-standard@npm:^0.0.2": + version: 0.0.2 + resolution: "html-standard@npm:0.0.2" + checksum: 73823c8f664ac499e6415fa15b7f8771f92465ca2d5e98ae006ff79619f9ba4b7a69685ca212d82635c83cd9322811641f986073b7ca0b27ff0ab92484bb4247 + languageName: node + linkType: hard + +"html-standard@npm:^0.0.3": + version: 0.0.3 + resolution: "html-standard@npm:0.0.3" + checksum: 782dcd5161cb044c18c9239f332cbcae73072c96bcf2336b0dad4f3162f45d47076aaccd679f69276108e3172171bf45a0f5374c876976793ac165e85d62923f + languageName: node + linkType: hard + "htmlnano@npm:^2.0.0": version: 2.1.1 resolution: "htmlnano@npm:2.1.1" From 1f0bbdfe7f207457a344f56fb1f0743508b67b15 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Fri, 18 Apr 2025 01:22:05 +0900 Subject: [PATCH 02/11] feat: implement --- docs/rules/use-standard-html.md | 0 .../lib/rules/use-standard-html.js | 35 +++++++++++++++++-- packages/eslint-plugin/package.json | 2 +- .../tests/rules/use-standard-html.test.js | 13 ++++++- yarn.lock | 10 +++--- 5 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 docs/rules/use-standard-html.md diff --git a/docs/rules/use-standard-html.md b/docs/rules/use-standard-html.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/eslint-plugin/lib/rules/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html.js index 2338df38..2e6e5b48 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html.js @@ -10,9 +10,10 @@ const { RULE_CATEGORY } = require("../constants"); const { getElementSpec } = require("html-standard"); +const { isTag } = require("./utils/node"); const MESSAGE_IDS = { - UNSORTED: "unsorted", + DISALLOW_CHILD: "disallowChild", }; /** @@ -43,14 +44,42 @@ module.exports = { }, ], messages: { - [MESSAGE_IDS.UNSORTED]: "TBD", + [MESSAGE_IDS.DISALLOW_CHILD]: "TBD", }, }, create(context) { return { Tag(node) { const spec = getElementSpec(node.name); - console.log(spec); + if (!spec || !spec.contents) { + return; + } + + node.children.forEach((child) => { + if (!spec.contents) { + return; + } + if (!isTag(child)) { + return; + } + const isAllowed = spec.contents.some((model) => { + if ( + model.type === "required" || + model.type === "oneOrMore" || + model.type === "zeroOrMore" || + model.type === "optional" + ) { + return model.contents.has(child.name.toLowerCase()); + } + return false; + }); + if (!isAllowed) { + context.report({ + node: child, + messageId: MESSAGE_IDS.DISALLOW_CHILD, + }); + } + }); }, }; }, diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 85b3a41b..47cac969 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -41,7 +41,7 @@ "@eslint/plugin-kit": "0.2.8", "@html-eslint/template-parser": "^0.40.0", "@html-eslint/template-syntax-parser": "^0.40.0", - "html-standard": "^0.0.3" + "html-standard": "^0.0.4" }, "devDependencies": { "@eslint/core": "0.13.0", diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 7ecb6053..4cb87749 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -9,5 +9,16 @@ ruleTester.run("use-standard-html", rule, { code: ``, }, ], - invalid: [], + invalid: [ + { + code: ` +
+ Date: Sat, 19 Apr 2025 00:37:00 +0900 Subject: [PATCH 03/11] update version --- packages/eslint-plugin/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 47cac969..95035533 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -41,7 +41,7 @@ "@eslint/plugin-kit": "0.2.8", "@html-eslint/template-parser": "^0.40.0", "@html-eslint/template-syntax-parser": "^0.40.0", - "html-standard": "^0.0.4" + "html-standard": "^0.0.5" }, "devDependencies": { "@eslint/core": "0.13.0", diff --git a/yarn.lock b/yarn.lock index b681cc6b..f33444db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1305,7 +1305,7 @@ __metadata: es-html-parser: "npm:0.2.0" eslint: "npm:^9.21.0" espree: "npm:^10.3.0" - html-standard: "npm:^0.0.4" + html-standard: "npm:^0.0.5" typescript: "npm:^5.7.2" languageName: unknown linkType: soft @@ -8827,10 +8827,10 @@ __metadata: languageName: node linkType: hard -"html-standard@npm:^0.0.4": - version: 0.0.4 - resolution: "html-standard@npm:0.0.4" - checksum: 268df7985c491c757ed035d741e9f9d6ac1b545e18ff4b06ec55a8e291e7912168be0306fe8ed85648193cd756351a29b679b8f457394d6cba5e9a172225cf28 +"html-standard@npm:^0.0.5": + version: 0.0.5 + resolution: "html-standard@npm:0.0.5" + checksum: 0574011a20adcad72e8684cc8ab300bc9680d46a621fddc3f14e62882802583eb0e59ca8379b74f8224b11b1276ec548f310ac241b6420791b5d4308bad27bcc languageName: node linkType: hard From c3dd8768c21d7c1627559e6b774941df285bc121 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Tue, 22 Apr 2025 22:33:43 +0900 Subject: [PATCH 04/11] implement --- .../lib/rules/use-standard-html.js | 86 ------- .../use-standard-html/check-content-model.js | 225 ++++++++++++++++++ .../lib/rules/use-standard-html/helpers.js | 47 ++++ .../lib/rules/use-standard-html/index.js | 2 + .../use-standard-html/use-standard-html.js | 66 +++++ .../eslint-plugin/lib/rules/utils/node.js | 9 + packages/eslint-plugin/package.json | 2 +- .../tests/rules/use-standard-html.test.js | 56 ++++- yarn.lock | 10 +- 9 files changed, 406 insertions(+), 97 deletions(-) delete mode 100644 packages/eslint-plugin/lib/rules/use-standard-html.js create mode 100644 packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js create mode 100644 packages/eslint-plugin/lib/rules/use-standard-html/helpers.js create mode 100644 packages/eslint-plugin/lib/rules/use-standard-html/index.js create mode 100644 packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js diff --git a/packages/eslint-plugin/lib/rules/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html.js deleted file mode 100644 index 2e6e5b48..00000000 --- a/packages/eslint-plugin/lib/rules/use-standard-html.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @typedef { import("../types").RuleFixer } RuleFixer - * @typedef { import("@html-eslint/types").Attribute } Attribute - * @typedef { import("@html-eslint/types").Text } Text - * - * @typedef {Object} Option - * @property {string[]} [Option.priority] - * @typedef { import("../types").RuleModule<[Option]> } RuleModule - */ - -const { RULE_CATEGORY } = require("../constants"); -const { getElementSpec } = require("html-standard"); -const { isTag } = require("./utils/node"); - -const MESSAGE_IDS = { - DISALLOW_CHILD: "disallowChild", -}; - -/** - * @type {RuleModule} - */ -module.exports = { - meta: { - type: "code", - - docs: { - description: "TBD", - category: RULE_CATEGORY.BEST_PRACTICE, - recommended: false, - }, - fixable: "code", - schema: [ - { - type: "object", - properties: { - priority: { - type: "array", - items: { - type: "string", - uniqueItems: true, - }, - }, - }, - }, - ], - messages: { - [MESSAGE_IDS.DISALLOW_CHILD]: "TBD", - }, - }, - create(context) { - return { - Tag(node) { - const spec = getElementSpec(node.name); - if (!spec || !spec.contents) { - return; - } - - node.children.forEach((child) => { - if (!spec.contents) { - return; - } - if (!isTag(child)) { - return; - } - const isAllowed = spec.contents.some((model) => { - if ( - model.type === "required" || - model.type === "oneOrMore" || - model.type === "zeroOrMore" || - model.type === "optional" - ) { - return model.contents.has(child.name.toLowerCase()); - } - return false; - }); - if (!isAllowed) { - context.report({ - node: child, - messageId: MESSAGE_IDS.DISALLOW_CHILD, - }); - } - }); - }, - }; - }, -}; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js new file mode 100644 index 00000000..cd4bc9ee --- /dev/null +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -0,0 +1,225 @@ +/** + * @typedef { import("html-standard").ElementSpec } ElementSpec + * @typedef { import("html-standard").ContentModel } ContentModel + * @typedef { import("@html-eslint/types").AnyHTMLNode} AnyHTMLNode + * @typedef { import("@html-eslint/types").Tag} Tag + * @typedef { import("@html-eslint/types").Text} Text + * + * @typedef { import("./use-standard-html").Option} Option + * @typedef { import("../../types").Context<[Option]> } Context + * + * @typedef {Object} State + * @property {number} contentModelIndex + * @property {ContentModel[] | null} contentModels + * @property {number} childIndex + * @property {AnyHTMLNode[]} children + */ + +const { shouldIgnoreChild, getNodeName } = require("./helpers"); + +const EXIT = false; +const CONTINUE = true; + +const MESSAGE_IDS = { + REQUIRED: "required", + NOT_ALLOWED: "notAllowed", +}; + +/** + * @param {State} state + * @returns {AnyHTMLNode | null} + */ +function getChild(state) { + return state.children[state.childIndex]; +} + +/** + * @param {State} state + * @returns {ContentModel | null} + */ +function getContentModel(state) { + if (!state.contentModels) { + return null; + } + return state.contentModels[state.contentModelIndex] || null; +} + +/** + * @param {Context} context + * @param {ElementSpec} spec + * @param {AnyHTMLNode} node + * @param {AnyHTMLNode[]} children + */ +function checkContentModel(context, spec, node, children) { + /** + * @type {State} + */ + const state = { + contentModels: spec.contents, + contentModelIndex: 0, + childIndex: 0, + children, + }; + let result = CONTINUE; + while (result && state.contentModels && !!getContentModel(state)) { + const contentModel = getContentModel(state); + if (!contentModel) { + return; + } + switch (contentModel.type) { + case "required": { + result = required(contentModel, context, state, node); + break; + } + case "zeroOrMore": { + result = zeroOrMore(contentModel, state); + break; + } + case "oneOrMore": { + result = oneOrMore(contentModel, context, state, node); + break; + } + case "optional": { + result = optional(state); + break; + } + case "either": { + break; + } + default: { + result = EXIT; + } + } + } + const remain = getChild(state); + const contentModel = getContentModel(state); + if (remain && !contentModel) { + context.report({ + node: remain, + messageId: MESSAGE_IDS.NOT_ALLOWED, + }); + } +} + +/** + * @param {ContentModel & {type: "required"}} model + * @param {Context} context + * @param {State} state + * @param {AnyHTMLNode} node + * @returns {boolean} + */ +function required(model, context, state, node) { + let child = getChild(state); + if (!child) { + context.report({ + node, + messageId: MESSAGE_IDS.REQUIRED, + }); + return EXIT; + } + if (shouldIgnoreChild(child)) { + state.childIndex++; + } + const name = getNodeName(child); + if (model.contents.has(name)) { + state.childIndex++; + state.contentModelIndex++; + return CONTINUE; + } + context.report({ + node, + messageId: MESSAGE_IDS.REQUIRED, + }); + return EXIT; +} + +/** + * @param {ContentModel & {type: "zeroOrMore"}} model + * @param {State} state + * @returns {boolean} + */ +function zeroOrMore(model, state) { + let child = getChild(state); + if (!child) { + state.childIndex++; + state.contentModelIndex++; + return CONTINUE; + } + while ((child = getChild(state))) { + if (shouldIgnoreChild(child)) { + state.childIndex++; + continue; + } + const name = getNodeName(child); + if (model.contents.has(name)) { + state.childIndex++; + } else { + break; + } + } + + state.contentModelIndex++; + return CONTINUE; +} + +/** + * @param {ContentModel & {type: "oneOrMore"}} model + * @param {Context} context + * @param {State} state + * @param {AnyHTMLNode} node + * @returns {boolean} + */ +function oneOrMore(model, context, state, node) { + let child = getChild(state); + if (!child) { + state.childIndex++; + state.contentModelIndex++; + return CONTINUE; + } + let count = 0; + while ((child = getChild(state))) { + if (shouldIgnoreChild(child)) { + state.childIndex++; + continue; + } + const name = getNodeName(child); + if (model.contents.has(name)) { + count++; + state.childIndex++; + } else { + break; + } + } + + if (count <= 0) { + context.report({ + node, + messageId: MESSAGE_IDS.REQUIRED, + }); + return EXIT; + } + + state.contentModelIndex++; + return CONTINUE; +} + +/** + * @param {State} state + * @returns {boolean} + */ +function optional(state) { + let child = getChild(state); + if (!child) { + state.childIndex++; + state.contentModelIndex++; + return CONTINUE; + } + state.childIndex++; + state.contentModelIndex++; + return CONTINUE; +} + +module.exports = { + MESSAGE_IDS, + checkContentModel, +}; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js new file mode 100644 index 00000000..7d222bda --- /dev/null +++ b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js @@ -0,0 +1,47 @@ +/** + * @typedef { import("@html-eslint/types").AnyHTMLNode} AnyHTMLNode + */ + +const { + isWhitespacesText, + isComment, + isTag, + isText, + isScript, + isStyle, +} = require("../utils/node"); + +/** + * @param {AnyHTMLNode} child + */ +function shouldIgnoreChild(child) { + return isWhitespacesText(child) || isComment(child); +} + +/** + * @param {AnyHTMLNode} node + * @returns {string} + */ +function getNodeName(node) { + if (isTag(node)) { + return node.name; + } + if (isText(node)) { + return "#text"; + } + if (isScript(node)) { + return "script"; + } + if (isStyle(node)) { + return "style"; + } + if (isComment(node)) { + return "#comment"; + } + return "#unknown"; +} + +module.exports = { + shouldIgnoreChild, + getNodeName, +}; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/index.js b/packages/eslint-plugin/lib/rules/use-standard-html/index.js new file mode 100644 index 00000000..b49ae814 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/use-standard-html/index.js @@ -0,0 +1,2 @@ +const useStandardHtml = require("./use-standard-html"); +module.exports = useStandardHtml; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js new file mode 100644 index 00000000..cc336f90 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js @@ -0,0 +1,66 @@ +/** + * @typedef { import("../../types").RuleFixer } RuleFixer + * @typedef { import("@html-eslint/types").Attribute } Attribute + * @typedef { import("@html-eslint/types").Text } Text + * @typedef { import("@html-eslint/types").AnyHTMLNode } AnyHTMLNode + * @typedef { import("html-standard").ElementSpec } ElementSpec + * @typedef {import("../../types").Context<[Option]> } Context + * + * @typedef {Object} Option + * @property {string[]} [Option.priority] + * @typedef { import("../../types").RuleModule<[Option]> } RuleModule + */ + +const { RULE_CATEGORY } = require("../../constants"); +const { getElementSpec } = require("html-standard"); +const { checkContentModel, MESSAGE_IDS } = require("./check-content-model"); + +/** + * @type {RuleModule} + */ +module.exports = { + meta: { + type: "code", + + docs: { + description: "TBD", + category: RULE_CATEGORY.BEST_PRACTICE, + recommended: false, + }, + fixable: "code", + schema: [ + { + type: "object", + properties: { + priority: { + type: "array", + items: { + type: "string", + uniqueItems: true, + }, + }, + }, + }, + ], + messages: { + [MESSAGE_IDS.REQUIRED]: "TBD", + [MESSAGE_IDS.NOT_ALLOWED]: "TBD", + }, + }, + create(context) { + return { + Tag(node) { + const name = node.name.toLowerCase(); + if (name === "template") { + return; + } + const spec = getElementSpec(node.name); + if (!spec) { + return; + } + + checkContentModel(context, spec, node, node.children); + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/rules/utils/node.js b/packages/eslint-plugin/lib/rules/utils/node.js index 34e44939..0ca52eec 100644 --- a/packages/eslint-plugin/lib/rules/utils/node.js +++ b/packages/eslint-plugin/lib/rules/utils/node.js @@ -194,6 +194,14 @@ function isText(node) { return node.type === NODE_TYPES.Text; } +/** + * @param {BaseNode} node + * @returns {node is Text} + */ +function isWhitespacesText(node) { + return isText(node) && node.value.trim().length <= 0; +} + /** * @param {BaseNode} node * @returns {node is Line} @@ -262,6 +270,7 @@ module.exports = { isTag, isComment, isText, + isWhitespacesText, isLine, isScript, isStyle, diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 95035533..527b7ee0 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -41,7 +41,7 @@ "@eslint/plugin-kit": "0.2.8", "@html-eslint/template-parser": "^0.40.0", "@html-eslint/template-syntax-parser": "^0.40.0", - "html-standard": "^0.0.5" + "html-standard": "^0.0.6" }, "devDependencies": { "@eslint/core": "0.13.0", diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 4cb87749..97e52969 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -1,5 +1,5 @@ const createRuleTester = require("../rule-tester"); -const rule = require("../../lib/rules/use-standard-html"); +const rule = require("../../lib/rules/use-standard-html/use-standard-html"); const ruleTester = createRuleTester(); @@ -8,15 +8,61 @@ ruleTester.run("use-standard-html", rule, { { code: ``, }, + { + code: ``, + }, + { + code: `
  • `, + }, + { + code: `
  • `, + }, + { + code: ``, + }, + { + code: ``, + }, + { + code: ``, + }, + { + code: `text
    `, + }, ], invalid: [ + // required + { + code: `
    `, + errors: [ + { + messageId: "required", + }, + ], + }, + { + code: ``, + errors: [ + { + messageId: "required", + }, + ], + }, + // zeroOrMore + { + code: `
    `, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, + // oneOreMore { - code: ` -
    -
    `, errors: [ { - messageId: "disallowChild", + messageId: "required", }, ], }, diff --git a/yarn.lock b/yarn.lock index f33444db..0527d360 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1305,7 +1305,7 @@ __metadata: es-html-parser: "npm:0.2.0" eslint: "npm:^9.21.0" espree: "npm:^10.3.0" - html-standard: "npm:^0.0.5" + html-standard: "npm:^0.0.6" typescript: "npm:^5.7.2" languageName: unknown linkType: soft @@ -8827,10 +8827,10 @@ __metadata: languageName: node linkType: hard -"html-standard@npm:^0.0.5": - version: 0.0.5 - resolution: "html-standard@npm:0.0.5" - checksum: 0574011a20adcad72e8684cc8ab300bc9680d46a621fddc3f14e62882802583eb0e59ca8379b74f8224b11b1276ec548f310ac241b6420791b5d4308bad27bcc +"html-standard@npm:^0.0.6": + version: 0.0.6 + resolution: "html-standard@npm:0.0.6" + checksum: d944d2a4e833a02ad8a62db5eeedf8623e7849f72b2eccdc3250a60d44e611c3a42730c993b1b597e5a9a4c6d2ebab6605575ccac7f6447b0c701aec2a25ecd0 languageName: node linkType: hard From 713df13c8ff3118e6bdbe4595bb43f62fb306b22 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Wed, 23 Apr 2025 01:22:40 +0900 Subject: [PATCH 05/11] implement --- .../rules/use-standard-html/check-content-model.js | 12 +++++++++--- .../tests/rules/use-standard-html.test.js | 11 +++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index cd4bc9ee..53830651 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -62,6 +62,7 @@ function checkContentModel(context, spec, node, children) { }; let result = CONTINUE; while (result && state.contentModels && !!getContentModel(state)) { + debugger; const contentModel = getContentModel(state); if (!contentModel) { return; @@ -80,7 +81,7 @@ function checkContentModel(context, spec, node, children) { break; } case "optional": { - result = optional(state); + result = optional(contentModel, state); break; } case "either": { @@ -204,17 +205,22 @@ function oneOrMore(model, context, state, node) { } /** + * @param {ContentModel & {type: "optional"}} model * @param {State} state * @returns {boolean} */ -function optional(state) { +function optional(model, state) { let child = getChild(state); if (!child) { state.childIndex++; state.contentModelIndex++; return CONTINUE; } - state.childIndex++; + const name = getNodeName(child); + if (model.contents.has(name)) { + state.childIndex++; + } + state.contentModelIndex++; return CONTINUE; } diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 97e52969..6d05b082 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -29,6 +29,9 @@ ruleTester.run("use-standard-html", rule, { { code: `text
    `, }, + { + code: `
    `, + }, ], invalid: [ // required @@ -66,5 +69,13 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + { + code: `
    `, + errors: [ + { + messageId: "required", + }, + ], + }, ], }); From a0c873896f552440629ee983a8966bb50329a1b4 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Wed, 23 Apr 2025 01:34:56 +0900 Subject: [PATCH 06/11] impl --- .../lib/rules/use-standard-html/check-content-model.js | 6 +++++- .../tests/rules/use-standard-html.test.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 53830651..15bf993e 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -192,7 +192,7 @@ function oneOrMore(model, context, state, node) { } } - if (count <= 0) { + if (count <= 0 && !model.contents.has("#text")) { context.report({ node, messageId: MESSAGE_IDS.REQUIRED, @@ -216,6 +216,10 @@ function optional(model, state) { state.contentModelIndex++; return CONTINUE; } + if (shouldIgnoreChild(child)) { + state.childIndex++; + return CONTINUE; + } const name = getNodeName(child); if (model.contents.has(name)) { state.childIndex++; diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 6d05b082..e8379d6c 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -32,6 +32,12 @@ ruleTester.run("use-standard-html", rule, { { code: `
    `, }, + { + code: `
    + + +
    `, + }, ], invalid: [ // required @@ -65,7 +71,7 @@ ruleTester.run("use-standard-html", rule, { code: `
    `, errors: [ { - messageId: "required", + messageId: "notAllowed", }, ], }, @@ -73,7 +79,7 @@ ruleTester.run("use-standard-html", rule, { code: `
    `, errors: [ { - messageId: "required", + messageId: "notAllowed", }, ], }, From f2e53cab83f05eb9743b76f873af142b2f702035 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sat, 26 Apr 2025 23:38:39 +0900 Subject: [PATCH 07/11] add tests --- .../use-standard-html/check-content-model.js | 3 +- packages/eslint-plugin/package.json | 2 +- .../tests/rules/use-standard-html.test.js | 63 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 15bf993e..8a992ade 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -62,7 +62,6 @@ function checkContentModel(context, spec, node, children) { }; let result = CONTINUE; while (result && state.contentModels && !!getContentModel(state)) { - debugger; const contentModel = getContentModel(state); if (!contentModel) { return; @@ -129,7 +128,7 @@ function required(model, context, state, node) { } context.report({ node, - messageId: MESSAGE_IDS.REQUIRED, + messageId: MESSAGE_IDS.NOT_ALLOWED, }); return EXIT; } diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 527b7ee0..39abffe8 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -41,7 +41,7 @@ "@eslint/plugin-kit": "0.2.8", "@html-eslint/template-parser": "^0.40.0", "@html-eslint/template-syntax-parser": "^0.40.0", - "html-standard": "^0.0.6" + "html-standard": "file:../../../html-standard" }, "devDependencies": { "@eslint/core": "0.13.0", diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index e8379d6c..c3a1c1f4 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -29,6 +29,9 @@ ruleTester.run("use-standard-html", rule, { { code: `text
    `, }, + { + code: `
    `, + }, { code: `
    `, }, @@ -38,6 +41,15 @@ ruleTester.run("use-standard-html", rule, { `, }, + { + code: '', + }, + { + code: "content", + }, + { + code: " ", + }, ], invalid: [ // required @@ -45,7 +57,18 @@ ruleTester.run("use-standard-html", rule, { code: `
    `, errors: [ { - messageId: "required", + messageId: "notAllowed", + }, + ], + }, + { + code: ` + +
    + `, + errors: [ + { + messageId: "notAllowed", }, ], }, @@ -57,6 +80,14 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + { + code: ``, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, // zeroOrMore { code: `
    `, @@ -66,6 +97,17 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + { + code: ` +
    + +
    `, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, // oneOreMore { code: `
    `, @@ -75,6 +117,25 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + { + code: ` +
    +
    `, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, + { + code: ` + `, + errors: [ + { + messageId: "required", + }, + ], + }, { code: `
    `, errors: [ From d86c5073e3a5ced5a4e0b8b79ac8e45eac1114a9 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sat, 26 Apr 2025 23:53:25 +0900 Subject: [PATCH 08/11] impl --- .../lib/rules/use-standard-html/check-content-model.js | 2 +- .../lib/rules/use-standard-html/use-standard-html.js | 8 ++++++++ .../eslint-plugin/tests/rules/use-standard-html.test.js | 7 +++++++ packages/website/eslint.config.js | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 8a992ade..3641be0f 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -183,7 +183,7 @@ function oneOrMore(model, context, state, node) { continue; } const name = getNodeName(child); - if (model.contents.has(name)) { + if (model.contents.has(name) || model.contents.has("#transparent")) { count++; state.childIndex++; } else { diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js index cc336f90..a3ed097f 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js @@ -14,6 +14,7 @@ const { RULE_CATEGORY } = require("../../constants"); const { getElementSpec } = require("html-standard"); const { checkContentModel, MESSAGE_IDS } = require("./check-content-model"); +const { isText } = require("../utils/node"); /** * @type {RuleModule} @@ -54,6 +55,13 @@ module.exports = { if (name === "template") { return; } + if ( + node.children.some( + (child) => isText(child) && child.parts && !!child.parts.length + ) + ) { + return; + } const spec = getElementSpec(node.name); if (!spec) { return; diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index c3a1c1f4..79054849 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -50,6 +50,13 @@ ruleTester.run("use-standard-html", rule, { { code: " ", }, + { + code: ` + + github + + `, + }, ], invalid: [ // required diff --git a/packages/website/eslint.config.js b/packages/website/eslint.config.js index 416689c5..2639951c 100644 --- a/packages/website/eslint.config.js +++ b/packages/website/eslint.config.js @@ -62,6 +62,7 @@ module.exports = [{ }, rules: { + "@html-eslint/use-standard-html": "error", "@html-eslint/indent": ["error", 2], "@html-eslint/require-doctype": "off", "@html-eslint/no-target-blank": "error", From 55a7bfcd8b095ce75d16bfbbbe09ac2479783a1c Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 27 Apr 2025 13:12:08 +0900 Subject: [PATCH 09/11] implement --- .../use-standard-html/check-content-model.js | 46 ++++++++++++++----- .../use-standard-html/use-standard-html.js | 32 +++++++++---- .../tests/rules/use-standard-html.test.js | 20 ++++++++ .../website/src/components/header/header.html | 4 +- 4 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 3641be0f..477dbc07 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -15,6 +15,8 @@ * @property {AnyHTMLNode[]} children */ +const { getElementSpec } = require("html-standard"); +const { isTag } = require("../utils/node"); const { shouldIgnoreChild, getNodeName } = require("./helpers"); const EXIT = false; @@ -47,10 +49,23 @@ function getContentModel(state) { /** * @param {Context} context * @param {ElementSpec} spec - * @param {AnyHTMLNode} node + * @param {Tag} node * @param {AnyHTMLNode[]} children + * @param {boolean} allowUnknownChildren */ -function checkContentModel(context, spec, node, children) { +function checkContentModel( + context, + spec, + node, + children, + allowUnknownChildren +) { + if ( + allowUnknownChildren && + children.some((child) => isTag(child) && !getElementSpec(child.name)) + ) { + return; + } /** * @type {State} */ @@ -66,6 +81,7 @@ function checkContentModel(context, spec, node, children) { if (!contentModel) { return; } + switch (contentModel.type) { case "required": { result = required(contentModel, context, state, node); @@ -91,7 +107,12 @@ function checkContentModel(context, spec, node, children) { } } } - const remain = getChild(state); + let remain = getChild(state); + while (remain && shouldIgnoreChild(remain)) { + state.childIndex++; + remain = getChild(state); + } + const contentModel = getContentModel(state); if (remain && !contentModel) { context.report({ @@ -105,21 +126,24 @@ function checkContentModel(context, spec, node, children) { * @param {ContentModel & {type: "required"}} model * @param {Context} context * @param {State} state - * @param {AnyHTMLNode} node + * @param {Tag} node * @returns {boolean} */ + function required(model, context, state, node) { let child = getChild(state); + while (child && shouldIgnoreChild(child)) { + state.childIndex++; + child = getChild(state); + } if (!child) { context.report({ - node, + node: node.openStart, messageId: MESSAGE_IDS.REQUIRED, }); return EXIT; } - if (shouldIgnoreChild(child)) { - state.childIndex++; - } + const name = getNodeName(child); if (model.contents.has(name)) { state.childIndex++; @@ -127,7 +151,7 @@ function required(model, context, state, node) { return CONTINUE; } context.report({ - node, + node: node.openStart, messageId: MESSAGE_IDS.NOT_ALLOWED, }); return EXIT; @@ -166,7 +190,7 @@ function zeroOrMore(model, state) { * @param {ContentModel & {type: "oneOrMore"}} model * @param {Context} context * @param {State} state - * @param {AnyHTMLNode} node + * @param {Tag} node * @returns {boolean} */ function oneOrMore(model, context, state, node) { @@ -193,7 +217,7 @@ function oneOrMore(model, context, state, node) { if (count <= 0 && !model.contents.has("#text")) { context.report({ - node, + node: node.openStart, messageId: MESSAGE_IDS.REQUIRED, }); return EXIT; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js index a3ed097f..18d03a72 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js @@ -7,7 +7,7 @@ * @typedef {import("../../types").Context<[Option]> } Context * * @typedef {Object} Option - * @property {string[]} [Option.priority] + * @property {boolean} [Option.allowUnknownChildren] * @typedef { import("../../types").RuleModule<[Option]> } RuleModule */ @@ -33,22 +33,28 @@ module.exports = { { type: "object", properties: { - priority: { - type: "array", - items: { - type: "string", - uniqueItems: true, - }, + allowUnknownChildren: { + type: "boolean", }, }, }, ], messages: { - [MESSAGE_IDS.REQUIRED]: "TBD", - [MESSAGE_IDS.NOT_ALLOWED]: "TBD", + [MESSAGE_IDS.REQUIRED]: "required", + [MESSAGE_IDS.NOT_ALLOWED]: "not allowed", }, }, create(context) { + /** + * @type {Option} + */ + const options = + context.options && context.options[0] + ? context.options[0] + : { + allowUnknownChildren: true, + }; + const allowUnknownChildren = options.allowUnknownChildren; return { Tag(node) { const name = node.name.toLowerCase(); @@ -67,7 +73,13 @@ module.exports = { return; } - checkContentModel(context, spec, node, node.children); + checkContentModel( + context, + spec, + node, + node.children, + allowUnknownChildren === true + ); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 79054849..1143e2f9 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -5,6 +5,18 @@ const ruleTester = createRuleTester(); ruleTester.run("use-standard-html", rule, { valid: [ + { + code: ` + TITLE + + `, + }, + { + code: "TITLE", + }, + { + code: "TITLE", + }, { code: ``, }, @@ -143,6 +155,14 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + { + code: `
    `, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, { code: `
    `, errors: [ diff --git a/packages/website/src/components/header/header.html b/packages/website/src/components/header/header.html index 135b47b3..66d088fd 100644 --- a/packages/website/src/components/header/header.html +++ b/packages/website/src/components/header/header.html @@ -41,7 +41,7 @@ class="menuInput peer sr-only hidden" aria-hidden="true" > - + From b7e5e85fc65f44936789b6851577f7f58a8f78b2 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 27 Apr 2025 13:24:20 +0900 Subject: [PATCH 10/11] implement --- .../use-standard-html/check-content-model.js | 14 ++++++++++- .../lib/rules/use-standard-html/helpers.js | 24 +++++++++++++++++++ .../use-standard-html/use-standard-html.js | 3 ++- .../tests/rules/use-standard-html.test.js | 2 +- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 477dbc07..57d6a6eb 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -17,7 +17,11 @@ const { getElementSpec } = require("html-standard"); const { isTag } = require("../utils/node"); -const { shouldIgnoreChild, getNodeName } = require("./helpers"); +const { + shouldIgnoreChild, + getNodeName, + getDisplayNodeName, +} = require("./helpers"); const EXIT = false; const CONTINUE = true; @@ -117,6 +121,10 @@ function checkContentModel( if (remain && !contentModel) { context.report({ node: remain, + data: { + child: getDisplayNodeName(remain), + parent: getDisplayNodeName(node), + }, messageId: MESSAGE_IDS.NOT_ALLOWED, }); } @@ -152,6 +160,10 @@ function required(model, context, state, node) { } context.report({ node: node.openStart, + data: { + child: getDisplayNodeName(child), + parent: getDisplayNodeName(node), + }, messageId: MESSAGE_IDS.NOT_ALLOWED, }); return EXIT; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js index 7d222bda..73f05e49 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js @@ -38,10 +38,34 @@ function getNodeName(node) { if (isComment(node)) { return "#comment"; } + return "Unknown"; +} + +/** + * @param {AnyHTMLNode} node + * @returns {string} + */ +function getDisplayNodeName(node) { + if (isTag(node)) { + return node.name; + } + if (isText(node)) { + return "Text"; + } + if (isScript(node)) { + return "script"; + } + if (isStyle(node)) { + return "style"; + } + if (isComment(node)) { + return "Comment"; + } return "#unknown"; } module.exports = { shouldIgnoreChild, getNodeName, + getDisplayNodeName, }; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js index 18d03a72..ac8457c1 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js @@ -41,7 +41,8 @@ module.exports = { ], messages: { [MESSAGE_IDS.REQUIRED]: "required", - [MESSAGE_IDS.NOT_ALLOWED]: "not allowed", + [MESSAGE_IDS.NOT_ALLOWED]: + "Element '{{child}}' not allowed as child of element '{{parent}}'", }, }, create(context) { diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 1143e2f9..2c9231ff 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -76,7 +76,7 @@ ruleTester.run("use-standard-html", rule, { code: `
    `, errors: [ { - messageId: "notAllowed", + message: `Element 'div' not allowed as child of element 'html'`, }, ], }, From 172db70738733f93453d8edb4da9a582c096ef20 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 27 Apr 2025 17:08:50 +0900 Subject: [PATCH 11/11] impl --- docs/rules.md | 1 + .../use-standard-html/check-content-model.js | 32 +++++++++++++++++++ .../lib/rules/use-standard-html/helpers.js | 2 +- .../use-standard-html/use-standard-html.js | 3 +- .../tests/rules/use-standard-html.test.js | 13 ++++++-- .../website/src/components/header/header.html | 17 +++++----- .../website/src/components/playground.html | 16 +++++----- 7 files changed, 64 insertions(+), 20 deletions(-) diff --git a/docs/rules.md b/docs/rules.md index 80cd06fe..4fa528d5 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -29,6 +29,7 @@ | [require-li-container](rules/require-li-container) | Enforce `
  • ` to be in `
      `, `
        ` or ``. | ⭐ | | [require-meta-charset](rules/require-meta-charset) | Enforce to use `` in `` | | | [use-baseline](rules/use-baseline) | Enforce the use of baseline features. | ⭐ | +| [use-standard-html](rules/use-standard-html) | TBD | 🔧 | ## SEO diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js index 57d6a6eb..8e70d9b0 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/check-content-model.js @@ -29,6 +29,7 @@ const CONTINUE = true; const MESSAGE_IDS = { REQUIRED: "required", NOT_ALLOWED: "notAllowed", + NOT_ALLOWED_DESCENDANT: "notAllowedDescendant", }; /** @@ -104,6 +105,7 @@ function checkContentModel( break; } case "either": { + result = EXIT; break; } default: { @@ -145,8 +147,15 @@ function required(model, context, state, node) { child = getChild(state); } if (!child) { + if (model.contents.has("#text")) { + return EXIT; + } context.report({ node: node.openStart, + data: { + parent: getDisplayNodeName(node), + child: Array.from(model.contents.keys()).join(","), + }, messageId: MESSAGE_IDS.REQUIRED, }); return EXIT; @@ -206,6 +215,29 @@ function zeroOrMore(model, state) { * @returns {boolean} */ function oneOrMore(model, context, state, node) { + if (model.constraints && model.constraints.children) { + const childrenConstraints = Array.from( + model.constraints.children.entries() + ); + const required = childrenConstraints.filter( + ([, value]) => value.required === true + ); + const missings = required.filter( + ([name]) => !node.children.some((child) => getNodeName(child) === name) + ); + if (missings.length) { + context.report({ + messageId: MESSAGE_IDS.REQUIRED, + data: { + parent: getDisplayNodeName(node), + child: missings.map(([name]) => name).join(","), + }, + node, + }); + return EXIT; + } + } + let child = getChild(state); if (!child) { state.childIndex++; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js index 73f05e49..8138ae6b 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/helpers.js @@ -24,7 +24,7 @@ function shouldIgnoreChild(child) { */ function getNodeName(node) { if (isTag(node)) { - return node.name; + return node.name.toLowerCase(); } if (isText(node)) { return "#text"; diff --git a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js index ac8457c1..e0ac3cd1 100644 --- a/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js +++ b/packages/eslint-plugin/lib/rules/use-standard-html/use-standard-html.js @@ -40,7 +40,8 @@ module.exports = { }, ], messages: { - [MESSAGE_IDS.REQUIRED]: "required", + [MESSAGE_IDS.REQUIRED]: + "Element '{{parent}}' is missing a required instance of child element '{{child}}'", [MESSAGE_IDS.NOT_ALLOWED]: "Element '{{child}}' not allowed as child of element '{{parent}}'", }, diff --git a/packages/eslint-plugin/tests/rules/use-standard-html.test.js b/packages/eslint-plugin/tests/rules/use-standard-html.test.js index 2c9231ff..45f49f8c 100644 --- a/packages/eslint-plugin/tests/rules/use-standard-html.test.js +++ b/packages/eslint-plugin/tests/rules/use-standard-html.test.js @@ -21,7 +21,7 @@ ruleTester.run("use-standard-html", rule, { code: ``, }, { - code: ``, + code: ``, }, { code: `
      1. `, @@ -60,7 +60,7 @@ ruleTester.run("use-standard-html", rule, { code: "content", }, { - code: " ", + code: " ", }, { code: ` @@ -171,5 +171,14 @@ ruleTester.run("use-standard-html", rule, { }, ], }, + // constraints + { + code: `
        `, + errors: [ + { + messageId: "notAllowed", + }, + ], + }, ], }); diff --git a/packages/website/src/components/header/header.html b/packages/website/src/components/header/header.html index 66d088fd..dbfcbb84 100644 --- a/packages/website/src/components/header/header.html +++ b/packages/website/src/components/header/header.html @@ -5,19 +5,20 @@ flex-direction: column; } -.burger > div { +.burger > span { + display: block; height: 2px; transition: 0.2s ease-out; z-index: 999; } -.menuInput:checked ~ .burger > div:nth-child(1) { +.menuInput:checked ~ .burger > span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); } -.menuInput:checked ~ .burger > div:nth-child(2) { +.menuInput:checked ~ .burger > span:nth-child(2) { opacity: 0; } -.menuInput:checked ~ .burger > div:nth-child(3) { +.menuInput:checked ~ .burger > span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); } @@ -44,11 +45,11 @@ -
        -
        -
        + + +
        diff --git a/packages/website/src/components/playground.html b/packages/website/src/components/playground.html index c0718d4d..2b0e3864 100644 --- a/packages/website/src/components/playground.html +++ b/packages/website/src/components/playground.html @@ -1,12 +1,12 @@ -
        - + +

        Playground