From b52aae29e2c572e591a41c6c48b04fff40b78554 Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Tue, 9 Sep 2025 20:14:08 -0700 Subject: [PATCH 01/15] [StepSecurity] Apply security best practices (#1718) * [StepSecurity] Apply security best practices Signed-off-by: StepSecurity Bot * Delete .github/workflows/dependency-review.yml * Remove validation for GitHub Actions Zizmor --------- Signed-off-by: StepSecurity Bot Co-authored-by: Christian Oliff --- .github/workflows/codeql-analysis.yml | 3 +++ .github/workflows/labeler.yml | 8 +++++++- .github/workflows/publish.yml | 3 +++ .github/workflows/super-linter.yml | 3 +-- .github/workflows/sync-labels.yml | 2 +- .github/workflows/test.yml | 14 ++++++++++---- .github/workflows/website.yml | 6 ++++++ 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7824e5d95..4685caf45 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,6 +11,9 @@ on: - '!dependabot/**' workflow_dispatch: +permissions: + contents: read + jobs: analyze: name: Analyze diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index fbcad30c9..af330336d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -3,10 +3,16 @@ name: Pull Request Labeler on: - pull_request_target +permissions: + contents: read + jobs: triage: + permissions: + contents: read # for actions/labeler to determine modified files + pull-requests: write # for actions/labeler to add labels to PRs runs-on: ubuntu-latest steps: - - uses: actions/labeler@v6 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: repo-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5b0cc5d5..3e8e28ce0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,9 @@ on: - published workflow_dispatch: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index e4bb8e79c..5c38e9943 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Super-linter - uses: super-linter/super-linter/slim@v8.1.0 + uses: super-linter/super-linter/slim@ffde3b2b33b745cb612d787f669ef9442b1339a6 # v8.1.0 env: DEFAULT_BRANCH: main FILTER_REGEX_EXCLUDE: '/test/' @@ -46,7 +46,6 @@ jobs: VALIDATE_CSS_PRETTIER: false VALIDATE_EDITORCONFIG: false VALIDATE_GIT_COMMITLINT: false - VALIDATE_GITHUB_ACTIONS_ZIZMOR: false VALIDATE_HTML: false VALIDATE_HTML_PRETTIER: false VALIDATE_JAVASCRIPT_ES: false diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 57cfaad6b..569574931 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -17,7 +17,7 @@ jobs: with: persist-credentials: false - - uses: micnncim/action-label-syncer@v1 + - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0 with: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 708dffe1a..8813fdc7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,9 @@ on: - '**/*.md' - '**/*.mdx' +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest @@ -30,6 +33,9 @@ jobs: run: npm run lint build: + permissions: + actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows + contents: read # for actions/checkout to fetch code runs-on: ${{ matrix.os }} needs: lint strategy: @@ -43,7 +49,7 @@ jobs: steps: - name: 🛑 Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.12.1 + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1 with: access_token: ${{ secrets.GITHUB_TOKEN }} @@ -69,7 +75,7 @@ jobs: - name: Run tests if: matrix.os != 'ubuntu-latest' - uses: nick-invision/retry@v3 + uses: nick-invision/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 20 max_attempts: 3 @@ -77,7 +83,7 @@ jobs: - name: Run coverage if: matrix.node == '20' && matrix.os == 'ubuntu-latest' - uses: nick-invision/retry@v3 + uses: nick-invision/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 20 max_attempts: 3 @@ -85,7 +91,7 @@ jobs: - name: ⬆️ Upload coverage to Codecov if: matrix.node == '20' && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: files: ./coverage/coverage-final.json name: codecov-dev diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b99e2fda4..7cf86c554 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -13,8 +13,14 @@ on: - .github/workflows/website.yml workflow_dispatch: +permissions: + contents: read + jobs: build: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results runs-on: ubuntu-latest steps: - name: ⬇️ Checkout From 422aa4891c8773cc0c6731e1f7512d7c8a028415 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:16:35 +0900 Subject: [PATCH 02/15] chore(deps): bump vite (#1717) Bumps the npm_and_yarn group with 1 update in the /website directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 6.3.5 to 6.3.6 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.3.6 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Christian Oliff --- website/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index f5520ac96..bed02b93f 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -7915,9 +7915,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", From 294d58ffcc6b3b534c53e2adadd0e8fb3b6a4af4 Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Thu, 11 Sep 2025 11:11:32 +0900 Subject: [PATCH 03/15] Update Node.js engine, workflows, and test coverage (#1719) Lowered the minimum Node.js engine requirement to 18 in package.json and added Node.js 18 to the test matrix. Updated CodeQL GitHub Actions to v3.30.2 and improved documentation for GitHub Actions usage. Removed a test that checked for rule presence in rules/index.mdx. Update website.yml Update project rules and labeler workflow formatting Expanded general project rules to include line endings, code formatting, rule ordering, and GitHub Actions best practices. Also standardized comment spacing in the labeler workflow permissions section. --- .cursor/rules/general.mdc | 14 ++++++++++++-- .github/copilot-instructions.md | 9 ++++++++- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/labeler.yml | 6 +++--- .github/workflows/test.yml | 5 +++-- .github/workflows/website.yml | 4 ++-- package-lock.json | 2 +- package.json | 2 +- test/rules/documentation.spec.js | 18 ------------------ 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index 923aac1ac..af842668c 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -3,6 +3,16 @@ description: General rules for the project globs: alwaysApply: true --- -- When adding a new rule to website ensure that it has the frontmatter: `pagefind: false sidebar: badge: New hidden: true` -- Always run `npm run lint` before declaring that a task is complete (if you've changed any files) +- When adding a new rule to website ensure that it has the frontmatter: `pagefind: false sidebar: badge: New hidden: true`. +- Always run `npm run lint` before declaring that a task is complete (if you've changed any files). +- Always use lf for line endings. +- Code is formatted with prettier. +- As a general rule, rules should be listed alphabetically. + +## GitHub Actions + +- The GitHub Actions workflows should be placed in the .github/workflows directory. +- The workflows should be named `.yml`. +- All GitHub Actions should be pinned versions to avoid breaking changes (SHA-1). +- If using actions/checkout, it should have `persist-credentials: false` set. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8758e58d3..f270365f0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,7 +9,7 @@ - Node v20 is used for development. - Core code is in TypeScript v5.4.5. - All new rules for HTMLHint should be placed in the rules directory. -- Tests for new rules should be added in rules and follow the naming pattern .spec.js. +- Tests for new rules should be added in rules and follow the naming pattern `.spec.js`. - Do not use deprecated Node.js or TypeScript features. - All user-facing messages and documentation should use clear, concise US English. - Keep dependencies up to date and avoid unnecessary packages. @@ -19,3 +19,10 @@ - Use the provided code snippets as examples for rule documentation. - Newly added rules pages for the website should have the frontmatter: sidebar: hidden: true badge: New - Always run `npm run build` before running tests or committing changes. + +## GitHub Actions + +- The GitHub Actions workflows should be placed in the .github/workflows directory. +- The workflows should be named `.yml`. +- All GitHub Actions should be pinned versions to avoid breaking changes (SHA-1). +- If using actions/checkout, it should have `persist-credentials: false` set. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4685caf45..a4f37d039 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,11 +30,11 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/init@d3678e237b9c32a6c9bffb3315c335f976f3549f # v3.30.2 with: config-file: ./.github/codeql/codeql-config.yml languages: 'javascript' queries: +security-and-quality - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/analyze@d3678e237b9c32a6c9bffb3315c335f976f3549f # v3.30.2 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index af330336d..749dbcd19 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,7 +1,7 @@ name: Pull Request Labeler on: - - pull_request_target + workflow_dispatch: permissions: contents: read @@ -9,8 +9,8 @@ permissions: jobs: triage: permissions: - contents: read # for actions/labeler to determine modified files - pull-requests: write # for actions/labeler to add labels to PRs + contents: read # for actions/labeler to determine modified files + pull-requests: write # for actions/labeler to add labels to PRs runs-on: ubuntu-latest steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8813fdc7c..77a7f9f87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,14 +34,15 @@ jobs: build: permissions: - actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows - contents: read # for actions/checkout to fetch code + actions: write # for styfle/cancel-workflow-action to cancel/stop running workflows + contents: read # for actions/checkout to fetch code runs-on: ${{ matrix.os }} needs: lint strategy: fail-fast: false matrix: node: + - 18 - 20 - 22 - 24 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7cf86c554..42eb5efda 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -19,8 +19,8 @@ permissions: jobs: build: permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results runs-on: ubuntu-latest steps: - name: ⬇️ Checkout diff --git a/package-lock.json b/package-lock.json index 6d37643af..3ae0835a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "typescript": "5.4.5" }, "engines": { - "node": ">=20" + "node": ">=18" }, "funding": { "type": "Open Collective", diff --git a/package.json b/package.json index cc6457208..150d377cc 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "typescript": "5.4.5" }, "engines": { - "node": ">=20" + "node": ">=18" }, "volta": { "node": "20.19.4" diff --git a/test/rules/documentation.spec.js b/test/rules/documentation.spec.js index 8b26c4431..d62e6d51f 100644 --- a/test/rules/documentation.spec.js +++ b/test/rules/documentation.spec.js @@ -25,27 +25,9 @@ describe('Rules documentation', () => { ) .map((doc) => doc.replace('.mdx', '')) - const rulesListPage = fs.readFileSync( - path.join( - __dirname, - '..', - '..', - 'website', - 'src', - 'content', - 'docs', - 'rules', - 'index.mdx' - ), - 'utf-8' - ) - rules.forEach((rule) => { it(`${rule} should have a documentation page`, () => { expect(docs).toContain(rule) }) - it(`${rule} should be on the rules/index.mdx`, () => { - expect(rulesListPage).toContain(rule) - }) }) }) From b8e5968d4e5e3a3c0af418de3d84e4e92075f43b Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Thu, 11 Sep 2025 11:32:03 +0900 Subject: [PATCH 04/15] Update Super-Linter GH Action (#1720) --- .github/workflows/codeql-analysis.yml | 1 + .github/workflows/labeler.yml | 1 + .github/workflows/ossf-scorecard.yml | 1 + .github/workflows/publish.yml | 1 + .github/workflows/spellcheck.yml | 1 + .github/workflows/super-linter.yml | 4 +--- .github/workflows/sync-labels.yml | 4 ++++ .github/workflows/test.yml | 1 + .github/workflows/website.yml | 1 + 9 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a4f37d039..b61b6b4bd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,3 +1,4 @@ +--- name: 'CodeQL' on: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 749dbcd19..4c6d1ed00 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,3 +1,4 @@ +--- name: Pull Request Labeler on: diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index c7ada4d99..1a63d13ba 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -1,3 +1,4 @@ +--- name: Scorecard supply-chain security on: branch_protection_rule: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3e8e28ce0..e1aa41057 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,4 @@ +--- name: Publish package to npmjs on: diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index dd3336072..09f8b38f1 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,3 +1,4 @@ +--- name: 'Check spelling' on: push: diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 5c38e9943..9c0edb7ff 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -1,3 +1,4 @@ +--- name: Lint Code Base on: @@ -49,10 +50,7 @@ jobs: VALIDATE_HTML: false VALIDATE_HTML_PRETTIER: false VALIDATE_JAVASCRIPT_ES: false - VALIDATE_JAVASCRIPT_PRETTIER: false VALIDATE_JSON: false - VALIDATE_JSON_PRETTIER: false VALIDATE_JSCPD: false VALIDATE_NATURAL_LANGUAGE: false VALIDATE_TYPESCRIPT_ES: false - VALIDATE_TYPESCRIPT_PRETTIER: false diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 569574931..b32f40601 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -1,4 +1,5 @@ # Sync GitHub labels in the declarative way +--- name: Sync labels on: @@ -8,6 +9,9 @@ on: paths: - .github/labels.yml +permissions: + contents: read + jobs: configure-labels: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77a7f9f87..05d7fde1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,4 @@ +--- name: Development workflow on: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 42eb5efda..830d06420 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,3 +1,4 @@ +--- name: Website on: From fd0d1312a09af4d7b09047c7275b0eec29e773ce Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Thu, 11 Sep 2025 13:22:45 +0900 Subject: [PATCH 05/15] Add link-rel-canonical-require rule (#1721) Introduces a new rule that enforces the presence of a tag with a non-blank href in the element. Includes implementation, tests, documentation, and configuration updates. Update link-rel-canonical-require.mdx Refactor link-rel-canonical-require formatting Improves code readability by reformatting the description string and simplifying the conditional for detecting canonical link elements. --- dist/core/rules/index.js | 6 +- src/core/rules/index.ts | 1 + src/core/rules/link-rel-canonical-require.ts | 51 +++++++++ src/core/types.ts | 1 + test/rules/link-rel-canonical-require.spec.js | 103 ++++++++++++++++++ website/src/content/docs/configuration.md | 1 + website/src/content/docs/rules/index.mdx | 1 + .../docs/rules/link-rel-canonical-require.mdx | 61 +++++++++++ 8 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/core/rules/link-rel-canonical-require.ts create mode 100644 test/rules/link-rel-canonical-require.spec.js create mode 100644 website/src/content/docs/rules/link-rel-canonical-require.mdx diff --git a/dist/core/rules/index.js b/dist/core/rules/index.js index 2a5f600a8..6bd81efb7 100644 --- a/dist/core/rules/index.js +++ b/dist/core/rules/index.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.tagNoObsolete = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.frameTitleRequire = exports.formMethodRequire = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrValueNoDuplication = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; +exports.titleRequire = exports.tagSelfClose = exports.tagsCheck = exports.tagPair = exports.tagnameSpecialChars = exports.tagnameLowercase = exports.tagNoObsolete = exports.styleDisabled = exports.srcNotEmpty = exports.specCharEscape = exports.spaceTabMixedDisabled = exports.scriptDisabled = exports.metaViewportRequire = exports.metaDescriptionRequire = exports.metaCharsetRequire = exports.mainRequire = exports.linkRelCanonicalRequire = exports.inputRequiresLabel = exports.inlineStyleDisabled = exports.inlineScriptDisabled = exports.idUnique = exports.idClassValue = exports.idClassAdDisabled = exports.htmlLangRequire = exports.hrefAbsOrRel = exports.headScriptDisabled = exports.h1Require = exports.frameTitleRequire = exports.formMethodRequire = exports.emptyTagNotSelfClosed = exports.doctypeHTML5 = exports.doctypeFirst = exports.buttonTypeRequire = exports.attrWhitespace = exports.attrValueSingleQuotes = exports.attrValueNotEmpty = exports.attrValueDoubleQuotes = exports.attrUnsafeChars = exports.attrSort = exports.attrValueNoDuplication = exports.attrNoUnnecessaryWhitespace = exports.attrNoDuplication = exports.attrLowercase = exports.altRequire = void 0; var alt_require_1 = require("./alt-require"); Object.defineProperty(exports, "altRequire", { enumerable: true, get: function () { return alt_require_1.default; } }); var attr_lowercase_1 = require("./attr-lowercase"); @@ -55,6 +55,8 @@ var inline_style_disabled_1 = require("./inline-style-disabled"); Object.defineProperty(exports, "inlineStyleDisabled", { enumerable: true, get: function () { return inline_style_disabled_1.default; } }); var input_requires_label_1 = require("./input-requires-label"); Object.defineProperty(exports, "inputRequiresLabel", { enumerable: true, get: function () { return input_requires_label_1.default; } }); +var link_rel_canonical_require_1 = require("./link-rel-canonical-require"); +Object.defineProperty(exports, "linkRelCanonicalRequire", { enumerable: true, get: function () { return link_rel_canonical_require_1.default; } }); var main_require_1 = require("./main-require"); Object.defineProperty(exports, "mainRequire", { enumerable: true, get: function () { return main_require_1.default; } }); var meta_charset_require_1 = require("./meta-charset-require"); @@ -87,4 +89,4 @@ var tag_self_close_1 = require("./tag-self-close"); Object.defineProperty(exports, "tagSelfClose", { enumerable: true, get: function () { return tag_self_close_1.default; } }); var title_require_1 = require("./title-require"); Object.defineProperty(exports, "titleRequire", { enumerable: true, get: function () { return title_require_1.default; } }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLHlFQUErRTtBQUF0RSxtSUFBQSxPQUFPLE9BQTBCO0FBQzFDLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyxxREFBNEQ7QUFBbkQsZ0hBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBcUQ7QUFBNUMseUdBQUEsT0FBTyxPQUFjO0FBQzlCLG1EQUEyRDtBQUFsRCwrR0FBQSxPQUFPLE9BQWlCO0FBQ2pDLDZEQUFvRTtBQUEzRCx3SEFBQSxPQUFPLE9BQXFCO0FBQ3JDLG1GQUF5RjtBQUFoRiw2SUFBQSxPQUFPLE9BQStCO0FBQy9DLHlFQUErRTtBQUF0RSxtSUFBQSxPQUFPLE9BQTBCO0FBQzFDLDZDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQVk7QUFDNUIseURBQWdFO0FBQXZELG9IQUFBLE9BQU8sT0FBbUI7QUFDbkMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsK0RBQXFFO0FBQTVELHlIQUFBLE9BQU8sT0FBcUI7QUFDckMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0I7QUFDaEMseUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBeUI7QUFDekMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsNkRBQW9FO0FBQTNELHdIQUFBLE9BQU8sT0FBcUI7QUFDckMsMkNBQW1EO0FBQTFDLHVHQUFBLE9BQU8sT0FBYTtBQUM3QiwrREFBc0U7QUFBN0QsMEhBQUEsT0FBTyxPQUFzQjtBQUN0QyxxREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFnQjtBQUNoQyx5REFBZ0U7QUFBdkQsb0hBQUEsT0FBTyxPQUFtQjtBQUNuQywrREFBcUU7QUFBNUQseUhBQUEsT0FBTyxPQUFxQjtBQUNyQyxtREFBMEQ7QUFBakQsOEdBQUEsT0FBTyxPQUFnQjtBQUNoQyx5Q0FBaUQ7QUFBeEMscUdBQUEsT0FBTyxPQUFZO0FBQzVCLG1FQUEwRTtBQUFqRSw4SEFBQSxPQUFPLE9BQXdCO0FBQ3hDLGlFQUF3RTtBQUEvRCw0SEFBQSxPQUFPLE9BQXVCO0FBQ3ZDLCtEQUFzRTtBQUE3RCwwSEFBQSxPQUFPLE9BQXNCO0FBQ3RDLDJFQUFpRjtBQUF4RSxxSUFBQSxPQUFPLE9BQTJCO0FBQzNDLCtDQUF1RDtBQUE5QywyR0FBQSxPQUFPLE9BQWU7QUFDL0IsK0RBQXNFO0FBQTdELDBIQUFBLE9BQU8sT0FBc0I7QUFDdEMsdUVBQThFO0FBQXJFLGtJQUFBLE9BQU8sT0FBMEI7QUFDMUMsaUVBQXdFO0FBQS9ELDRIQUFBLE9BQU8sT0FBdUI7QUFDdkMscURBQTZEO0FBQXBELGlIQUFBLE9BQU8sT0FBa0I7QUFDbEMsdUVBQTZFO0FBQXBFLGlJQUFBLE9BQU8sT0FBeUI7QUFDekMsdURBQThEO0FBQXJELGtIQUFBLE9BQU8sT0FBa0I7QUFDbEMsaURBQXdEO0FBQS9DLDRHQUFBLE9BQU8sT0FBZTtBQUMvQixtREFBMkQ7QUFBbEQsK0dBQUEsT0FBTyxPQUFpQjtBQUNqQyxxREFBNEQ7QUFBbkQsZ0hBQUEsT0FBTyxPQUFpQjtBQUNqQyx5REFBaUU7QUFBeEQscUhBQUEsT0FBTyxPQUFvQjtBQUNwQywrREFBdUU7QUFBOUQsMkhBQUEsT0FBTyxPQUF1QjtBQUN2Qyx1Q0FBK0M7QUFBdEMsbUdBQUEsT0FBTyxPQUFXO0FBQzNCLDJDQUFtRDtBQUExQyx1R0FBQSxPQUFPLE9BQWE7QUFDN0IsbURBQTBEO0FBQWpELDhHQUFBLE9BQU8sT0FBZ0I7QUFDaEMsaURBQXlEO0FBQWhELDZHQUFBLE9BQU8sT0FBZ0IifQ== \ No newline at end of file diff --git a/src/core/rules/index.ts b/src/core/rules/index.ts index 4cd33ecef..3669d4244 100644 --- a/src/core/rules/index.ts +++ b/src/core/rules/index.ts @@ -25,6 +25,7 @@ export { default as idUnique } from './id-unique' export { default as inlineScriptDisabled } from './inline-script-disabled' export { default as inlineStyleDisabled } from './inline-style-disabled' export { default as inputRequiresLabel } from './input-requires-label' +export { default as linkRelCanonicalRequire } from './link-rel-canonical-require' export { default as mainRequire } from './main-require' export { default as metaCharsetRequire } from './meta-charset-require' export { default as metaDescriptionRequire } from './meta-description-require' diff --git a/src/core/rules/link-rel-canonical-require.ts b/src/core/rules/link-rel-canonical-require.ts new file mode 100644 index 000000000..7f42ff51c --- /dev/null +++ b/src/core/rules/link-rel-canonical-require.ts @@ -0,0 +1,51 @@ +import { Block, Listener } from '../htmlparser' +import { Rule } from '../types' + +export default { + id: 'link-rel-canonical-require', + description: + ' with non-blank href must be present in tag.', + init(parser, reporter) { + let headSeen = false + let linkCanonicalSeen = false + let linkCanonicalHref = '' + let headEvent: Block | null = null + + const onTagStart: Listener = (event) => { + const tagName = event.tagName.toLowerCase() + if (tagName === 'head') { + headSeen = true + headEvent = event + } else if (tagName === 'link') { + const mapAttrs = parser.getMapAttrs(event.attrs) + if (mapAttrs['rel'] && mapAttrs['rel'].toLowerCase() === 'canonical') { + linkCanonicalSeen = true + linkCanonicalHref = mapAttrs['href'] || '' + } + } + } + + parser.addListener('tagstart', onTagStart) + parser.addListener('end', () => { + if (headSeen && headEvent) { + if (!linkCanonicalSeen) { + reporter.error( + ' must be present in tag.', + headEvent.line, + headEvent.col, + this, + headEvent.raw + ) + } else if (linkCanonicalHref.trim() === '') { + reporter.error( + ' href attribute must not be empty.', + headEvent.line, + headEvent.col, + this, + headEvent.raw + ) + } + } + }) + }, +} as Rule diff --git a/src/core/types.ts b/src/core/types.ts index 377f2b098..b9a1b14dc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -35,6 +35,7 @@ export interface Ruleset { 'inline-script-disabled'?: boolean 'inline-style-disabled'?: boolean 'input-requires-label'?: boolean + 'link-rel-canonical-require'?: boolean 'main-require'?: boolean 'script-disabled'?: boolean 'space-tab-mixed-disabled'?: diff --git a/test/rules/link-rel-canonical-require.spec.js b/test/rules/link-rel-canonical-require.spec.js new file mode 100644 index 000000000..34276269e --- /dev/null +++ b/test/rules/link-rel-canonical-require.spec.js @@ -0,0 +1,103 @@ +const HTMLHint = require('../../dist/htmlhint.js').HTMLHint +const ruleId = 'link-rel-canonical-require' + +describe('Rule: link-rel-canonical-require', () => { + it('should not report an error when a valid canonical link is present', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link has uppercase rel attribute', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link has mixed case rel attribute', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should report an error when canonical link is missing', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(1) + expect(messages[0].message).toBe( + ' must be present in tag.' + ) + }) + + it('should report an error when canonical link href is blank', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(1) + expect(messages[0].message).toBe( + ' href attribute must not be empty.' + ) + }) + + it('should report an error when canonical link href is only whitespace', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(1) + expect(messages[0].message).toBe( + ' href attribute must not be empty.' + ) + }) + + it('should report an error when canonical link href is missing', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(1) + expect(messages[0].message).toBe( + ' href attribute must not be empty.' + ) + }) + + it('should not report an error for other link tags', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should report an error when only other link tags are present', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(1) + expect(messages[0].message).toBe( + ' must be present in tag.' + ) + }) + + it('should not report an error when canonical link is present with other meta tags', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link has relative URL', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link has query parameters', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link has fragment', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) + + it('should not report an error when canonical link is self-referencing', () => { + const code = `` + const messages = HTMLHint.verify(code, { [ruleId]: true }) + expect(messages.length).toBe(0) + }) +}) diff --git a/website/src/content/docs/configuration.md b/website/src/content/docs/configuration.md index 428c149b1..0a620da7e 100644 --- a/website/src/content/docs/configuration.md +++ b/website/src/content/docs/configuration.md @@ -66,6 +66,7 @@ An example configuration file (with all rules disabled): "inline-script-disabled": false, "inline-style-disabled": false, "input-requires-label": false, + "link-rel-canonical-require": false, "main-require": false, "meta-charset-require": false, "meta-description-require": false, diff --git a/website/src/content/docs/rules/index.mdx b/website/src/content/docs/rules/index.mdx index 695620021..fd0c19431 100644 --- a/website/src/content/docs/rules/index.mdx +++ b/website/src/content/docs/rules/index.mdx @@ -12,6 +12,7 @@ description: A complete list of all the rules for HTMLHint - [`meta-charset-require`](meta-charset-require/): `` must be present in `` tag. - [`meta-description-require`](meta-description-require/): `` with non-blank content must be present in `` tag. - [`meta-viewport-require`](meta-viewport-require/): `` with non-blank content must be present in `` tag. +- [`link-rel-canonical-require`](link-rel-canonical-require/): `` with non-blank href must be present in `` tag. - [`script-disabled`](script-disabled/): `