@redocly/recheck 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1023 -56
- package/dist/cli.js +40 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/markdoc-schema.d.ts +17 -0
- package/dist/commands/markdoc-schema.d.ts.map +1 -0
- package/dist/commands/markdoc-schema.js +127 -0
- package/dist/commands/markdoc-schema.js.map +1 -0
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +87 -10
- package/dist/commands/run.js.map +1 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +12 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/presets/google.d.ts +3 -0
- package/dist/config/presets/google.d.ts.map +1 -0
- package/dist/config/presets/google.js +1671 -0
- package/dist/config/presets/google.js.map +1 -0
- package/dist/config/presets/inclusive-language.d.ts +3 -0
- package/dist/config/presets/inclusive-language.d.ts.map +1 -0
- package/dist/config/presets/inclusive-language.js +321 -0
- package/dist/config/presets/inclusive-language.js.map +1 -0
- package/dist/config/presets/index.d.ts +67 -0
- package/dist/config/presets/index.d.ts.map +1 -0
- package/dist/config/presets/index.js +137 -0
- package/dist/config/presets/index.js.map +1 -0
- package/dist/config/presets/markdoc.d.ts +21 -0
- package/dist/config/presets/markdoc.d.ts.map +1 -0
- package/dist/config/presets/markdoc.js +101 -0
- package/dist/config/presets/markdoc.js.map +1 -0
- package/dist/config/presets/markdown-relaxed.d.ts +3 -0
- package/dist/config/presets/markdown-relaxed.d.ts.map +1 -0
- package/dist/config/presets/markdown-relaxed.js +98 -0
- package/dist/config/presets/markdown-relaxed.js.map +1 -0
- package/dist/config/presets/markdown.d.ts +43 -0
- package/dist/config/presets/markdown.d.ts.map +1 -0
- package/dist/config/presets/markdown.js +132 -0
- package/dist/config/presets/markdown.js.map +1 -0
- package/dist/config/presets/microsoft.d.ts +3 -0
- package/dist/config/presets/microsoft.d.ts.map +1 -0
- package/dist/config/presets/microsoft.js +2268 -0
- package/dist/config/presets/microsoft.js.map +1 -0
- package/dist/config/presets/minimal.d.ts +3 -0
- package/dist/config/presets/minimal.d.ts.map +1 -0
- package/dist/config/presets/minimal.js +21 -0
- package/dist/config/presets/minimal.js.map +1 -0
- package/dist/config/presets/plain-language.d.ts +3 -0
- package/dist/config/presets/plain-language.d.ts.map +1 -0
- package/dist/config/presets/plain-language.js +351 -0
- package/dist/config/presets/plain-language.js.map +1 -0
- package/dist/config/presets/prose.d.ts +52 -0
- package/dist/config/presets/prose.d.ts.map +1 -0
- package/dist/config/presets/prose.js +138 -0
- package/dist/config/presets/prose.js.map +1 -0
- package/dist/config/schema.d.ts +128 -22
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +105 -21
- package/dist/config/schema.js.map +1 -1
- package/dist/config/validate.d.ts +12 -2
- package/dist/config/validate.d.ts.map +1 -1
- package/dist/config/validate.js +1200 -44
- package/dist/config/validate.js.map +1 -1
- package/dist/core/auto-fix.d.ts +8 -13
- package/dist/core/auto-fix.d.ts.map +1 -1
- package/dist/core/auto-fix.js +94 -75
- package/dist/core/auto-fix.js.map +1 -1
- package/dist/core/case-preserve.d.ts +46 -0
- package/dist/core/case-preserve.d.ts.map +1 -0
- package/dist/core/case-preserve.js +57 -0
- package/dist/core/case-preserve.js.map +1 -0
- package/dist/core/directives.d.ts +9 -0
- package/dist/core/directives.d.ts.map +1 -0
- package/dist/core/directives.js +73 -0
- package/dist/core/directives.js.map +1 -0
- package/dist/core/files.d.ts +63 -0
- package/dist/core/files.d.ts.map +1 -1
- package/dist/core/files.js +185 -0
- package/dist/core/files.js.map +1 -1
- package/dist/core/inline-code.d.ts +87 -0
- package/dist/core/inline-code.d.ts.map +1 -0
- package/dist/core/inline-code.js +104 -0
- package/dist/core/inline-code.js.map +1 -0
- package/dist/core/line-endings.d.ts +32 -0
- package/dist/core/line-endings.d.ts.map +1 -0
- package/dist/core/line-endings.js +65 -0
- package/dist/core/line-endings.js.map +1 -0
- package/dist/core/markdoc-tags.d.ts +79 -0
- package/dist/core/markdoc-tags.d.ts.map +1 -0
- package/dist/core/markdoc-tags.js +131 -0
- package/dist/core/markdoc-tags.js.map +1 -0
- package/dist/core/rule-filters.d.ts +17 -0
- package/dist/core/rule-filters.d.ts.map +1 -1
- package/dist/core/rule-filters.js +64 -0
- package/dist/core/rule-filters.js.map +1 -1
- package/dist/core/runner.d.ts +92 -3
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +348 -110
- package/dist/core/runner.js.map +1 -1
- package/dist/core/timing.d.ts.map +1 -1
- package/dist/data/markdoc-realm-schema.d.ts +3 -0
- package/dist/data/markdoc-realm-schema.d.ts.map +1 -0
- package/dist/data/markdoc-realm-schema.js +760 -0
- package/dist/data/markdoc-realm-schema.js.map +1 -0
- package/dist/data/proper-nouns.d.ts +2 -0
- package/dist/data/proper-nouns.d.ts.map +1 -0
- package/dist/data/proper-nouns.js +47 -0
- package/dist/data/proper-nouns.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +152 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/formulas.d.ts +17 -0
- package/dist/metrics/formulas.d.ts.map +1 -0
- package/dist/metrics/formulas.js +70 -0
- package/dist/metrics/formulas.js.map +1 -0
- package/dist/metrics/index.d.ts +5 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/statistics.d.ts +27 -0
- package/dist/metrics/statistics.d.ts.map +1 -0
- package/dist/metrics/statistics.js +56 -0
- package/dist/metrics/statistics.js.map +1 -0
- package/dist/parser/index.d.ts +18 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/markdoc/extract-statics.d.ts +45 -0
- package/dist/parser/markdoc/extract-statics.d.ts.map +1 -0
- package/dist/parser/markdoc/extract-statics.js +139 -0
- package/dist/parser/markdoc/extract-statics.js.map +1 -0
- package/dist/parser/markdoc/pairing.d.ts +63 -0
- package/dist/parser/markdoc/pairing.d.ts.map +1 -0
- package/dist/parser/markdoc/pairing.js +94 -0
- package/dist/parser/markdoc/pairing.js.map +1 -0
- package/dist/parser/markdoc/schema.d.ts +85 -0
- package/dist/parser/markdoc/schema.d.ts.map +1 -0
- package/dist/parser/markdoc/schema.js +86 -0
- package/dist/parser/markdoc/schema.js.map +1 -0
- package/dist/parser/markdoc/span.d.ts +64 -0
- package/dist/parser/markdoc/span.d.ts.map +1 -0
- package/dist/parser/markdoc/span.js +729 -0
- package/dist/parser/markdoc/span.js.map +1 -0
- package/dist/parser/markdoc/structure.d.ts +28 -0
- package/dist/parser/markdoc/structure.d.ts.map +1 -0
- package/dist/parser/markdoc/structure.js +153 -0
- package/dist/parser/markdoc/structure.js.map +1 -0
- package/dist/parser/markdoc/syntax.d.ts +44 -0
- package/dist/parser/markdoc/syntax.d.ts.map +1 -0
- package/dist/parser/markdoc/syntax.js +317 -0
- package/dist/parser/markdoc/syntax.js.map +1 -0
- package/dist/parser/types.d.ts +18 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/reporter/fixes.d.ts.map +1 -1
- package/dist/reporter/fixes.js +22 -1
- package/dist/reporter/fixes.js.map +1 -1
- package/dist/reporter/statistics.d.ts +1 -8
- package/dist/reporter/statistics.d.ts.map +1 -1
- package/dist/reporter/statistics.js.map +1 -1
- package/dist/rules/registry.d.ts +15 -0
- package/dist/rules/registry.d.ts.map +1 -0
- package/dist/rules/registry.js +81 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/rules/scope/capitalization.d.ts +3 -0
- package/dist/rules/scope/capitalization.d.ts.map +1 -0
- package/dist/rules/scope/capitalization.js +163 -0
- package/dist/rules/scope/capitalization.js.map +1 -0
- package/dist/rules/scope/conditional.d.ts +3 -0
- package/dist/rules/scope/conditional.d.ts.map +1 -0
- package/dist/rules/scope/conditional.js +112 -0
- package/dist/rules/scope/conditional.js.map +1 -0
- package/dist/rules/scope/consistency.d.ts +3 -0
- package/dist/rules/scope/consistency.d.ts.map +1 -0
- package/dist/rules/scope/consistency.js +178 -0
- package/dist/rules/scope/consistency.js.map +1 -0
- package/dist/rules/scope/length.d.ts +3 -0
- package/dist/rules/scope/length.d.ts.map +1 -0
- package/dist/rules/scope/length.js +69 -0
- package/dist/rules/scope/length.js.map +1 -0
- package/dist/rules/scope/max-image-size.d.ts +3 -0
- package/dist/rules/scope/max-image-size.d.ts.map +1 -0
- package/dist/rules/scope/max-image-size.js +65 -0
- package/dist/rules/scope/max-image-size.js.map +1 -0
- package/dist/rules/scope/metric.d.ts +17 -0
- package/dist/rules/scope/metric.d.ts.map +1 -0
- package/dist/rules/scope/metric.js +216 -0
- package/dist/rules/scope/metric.js.map +1 -0
- package/dist/rules/scope/occurrence.d.ts +3 -0
- package/dist/rules/scope/occurrence.d.ts.map +1 -0
- package/dist/rules/scope/occurrence.js +48 -0
- package/dist/rules/scope/occurrence.js.map +1 -0
- package/dist/rules/scope/pattern.d.ts +3 -0
- package/dist/rules/scope/pattern.d.ts.map +1 -0
- package/dist/rules/scope/pattern.js +75 -0
- package/dist/rules/scope/pattern.js.map +1 -0
- package/dist/rules/scope/repetition.d.ts +3 -0
- package/dist/rules/scope/repetition.d.ts.map +1 -0
- package/dist/rules/scope/repetition.js +139 -0
- package/dist/rules/scope/repetition.js.map +1 -0
- package/dist/rules/scope/semantic-line-breaks.d.ts +3 -0
- package/dist/rules/scope/semantic-line-breaks.d.ts.map +1 -0
- package/dist/rules/scope/semantic-line-breaks.js +213 -0
- package/dist/rules/scope/semantic-line-breaks.js.map +1 -0
- package/dist/rules/scope/spelling.d.ts +27 -0
- package/dist/rules/scope/spelling.d.ts.map +1 -0
- package/dist/rules/scope/spelling.js +227 -0
- package/dist/rules/scope/spelling.js.map +1 -0
- package/dist/rules/scope/swap.d.ts +3 -0
- package/dist/rules/scope/swap.d.ts.map +1 -0
- package/dist/rules/scope/swap.js +149 -0
- package/dist/rules/scope/swap.js.map +1 -0
- package/dist/rules/scope/title-case.d.ts +46 -0
- package/dist/rules/scope/title-case.d.ts.map +1 -0
- package/dist/rules/scope/title-case.js +301 -0
- package/dist/rules/scope/title-case.js.map +1 -0
- package/dist/rules/token/blanks-around-fences.d.ts +3 -0
- package/dist/rules/token/blanks-around-fences.d.ts.map +1 -0
- package/dist/rules/token/blanks-around-fences.js +44 -0
- package/dist/rules/token/blanks-around-fences.js.map +1 -0
- package/dist/rules/token/blanks-around-headings.d.ts +3 -0
- package/dist/rules/token/blanks-around-headings.d.ts.map +1 -0
- package/dist/rules/token/blanks-around-headings.js +108 -0
- package/dist/rules/token/blanks-around-headings.js.map +1 -0
- package/dist/rules/token/blanks-around-lists.d.ts +3 -0
- package/dist/rules/token/blanks-around-lists.d.ts.map +1 -0
- package/dist/rules/token/blanks-around-lists.js +56 -0
- package/dist/rules/token/blanks-around-lists.js.map +1 -0
- package/dist/rules/token/blanks-around-tables.d.ts +3 -0
- package/dist/rules/token/blanks-around-tables.d.ts.map +1 -0
- package/dist/rules/token/blanks-around-tables.js +42 -0
- package/dist/rules/token/blanks-around-tables.js.map +1 -0
- package/dist/rules/token/code-block-style.d.ts +3 -0
- package/dist/rules/token/code-block-style.d.ts.map +1 -0
- package/dist/rules/token/code-block-style.js +30 -0
- package/dist/rules/token/code-block-style.js.map +1 -0
- package/dist/rules/token/code-fence-style.d.ts +3 -0
- package/dist/rules/token/code-fence-style.d.ts.map +1 -0
- package/dist/rules/token/code-fence-style.js +35 -0
- package/dist/rules/token/code-fence-style.js.map +1 -0
- package/dist/rules/token/commands-show-output.d.ts +3 -0
- package/dist/rules/token/commands-show-output.d.ts.map +1 -0
- package/dist/rules/token/commands-show-output.js +38 -0
- package/dist/rules/token/commands-show-output.js.map +1 -0
- package/dist/rules/token/descriptive-link-text.d.ts +3 -0
- package/dist/rules/token/descriptive-link-text.d.ts.map +1 -0
- package/dist/rules/token/descriptive-link-text.js +53 -0
- package/dist/rules/token/descriptive-link-text.js.map +1 -0
- package/dist/rules/token/emphasis-style.d.ts +3 -0
- package/dist/rules/token/emphasis-style.d.ts.map +1 -0
- package/dist/rules/token/emphasis-style.js +51 -0
- package/dist/rules/token/emphasis-style.js.map +1 -0
- package/dist/rules/token/fenced-code-language.d.ts +3 -0
- package/dist/rules/token/fenced-code-language.d.ts.map +1 -0
- package/dist/rules/token/fenced-code-language.js +35 -0
- package/dist/rules/token/fenced-code-language.js.map +1 -0
- package/dist/rules/token/first-line-h1.d.ts +3 -0
- package/dist/rules/token/first-line-h1.d.ts.map +1 -0
- package/dist/rules/token/first-line-h1.js +107 -0
- package/dist/rules/token/first-line-h1.js.map +1 -0
- package/dist/rules/token/heading-increment.d.ts +3 -0
- package/dist/rules/token/heading-increment.d.ts.map +1 -0
- package/dist/rules/token/heading-increment.js +27 -0
- package/dist/rules/token/heading-increment.js.map +1 -0
- package/dist/rules/token/heading-start-left.d.ts +3 -0
- package/dist/rules/token/heading-start-left.d.ts.map +1 -0
- package/dist/rules/token/heading-start-left.js +31 -0
- package/dist/rules/token/heading-start-left.js.map +1 -0
- package/dist/rules/token/heading-style.d.ts +3 -0
- package/dist/rules/token/heading-style.d.ts.map +1 -0
- package/dist/rules/token/heading-style.js +41 -0
- package/dist/rules/token/heading-style.js.map +1 -0
- package/dist/rules/token/helpers.d.ts +313 -0
- package/dist/rules/token/helpers.d.ts.map +1 -0
- package/dist/rules/token/helpers.js +746 -0
- package/dist/rules/token/helpers.js.map +1 -0
- package/dist/rules/token/hr-style.d.ts +3 -0
- package/dist/rules/token/hr-style.d.ts.map +1 -0
- package/dist/rules/token/hr-style.js +28 -0
- package/dist/rules/token/hr-style.js.map +1 -0
- package/dist/rules/token/index.d.ts +75 -0
- package/dist/rules/token/index.d.ts.map +1 -0
- package/dist/rules/token/index.js +226 -0
- package/dist/rules/token/index.js.map +1 -0
- package/dist/rules/token/line-length.d.ts +3 -0
- package/dist/rules/token/line-length.d.ts.map +1 -0
- package/dist/rules/token/line-length.js +120 -0
- package/dist/rules/token/line-length.js.map +1 -0
- package/dist/rules/token/link-fragments.d.ts +3 -0
- package/dist/rules/token/link-fragments.d.ts.map +1 -0
- package/dist/rules/token/link-fragments.js +145 -0
- package/dist/rules/token/link-fragments.js.map +1 -0
- package/dist/rules/token/link-image-reference-definitions.d.ts +3 -0
- package/dist/rules/token/link-image-reference-definitions.d.ts.map +1 -0
- package/dist/rules/token/link-image-reference-definitions.js +50 -0
- package/dist/rules/token/link-image-reference-definitions.js.map +1 -0
- package/dist/rules/token/link-image-style.d.ts +3 -0
- package/dist/rules/token/link-image-style.d.ts.map +1 -0
- package/dist/rules/token/link-image-style.js +131 -0
- package/dist/rules/token/link-image-style.js.map +1 -0
- package/dist/rules/token/list-indent.d.ts +3 -0
- package/dist/rules/token/list-indent.d.ts.map +1 -0
- package/dist/rules/token/list-indent.js +60 -0
- package/dist/rules/token/list-indent.js.map +1 -0
- package/dist/rules/token/list-length.d.ts +3 -0
- package/dist/rules/token/list-length.d.ts.map +1 -0
- package/dist/rules/token/list-length.js +55 -0
- package/dist/rules/token/list-length.js.map +1 -0
- package/dist/rules/token/list-marker-space.d.ts +3 -0
- package/dist/rules/token/list-marker-space.d.ts.map +1 -0
- package/dist/rules/token/list-marker-space.js +52 -0
- package/dist/rules/token/list-marker-space.js.map +1 -0
- package/dist/rules/token/markdoc-attributes.d.ts +3 -0
- package/dist/rules/token/markdoc-attributes.d.ts.map +1 -0
- package/dist/rules/token/markdoc-attributes.js +269 -0
- package/dist/rules/token/markdoc-attributes.js.map +1 -0
- package/dist/rules/token/markdoc-pairing.d.ts +3 -0
- package/dist/rules/token/markdoc-pairing.d.ts.map +1 -0
- package/dist/rules/token/markdoc-pairing.js +73 -0
- package/dist/rules/token/markdoc-pairing.js.map +1 -0
- package/dist/rules/token/markdoc-syntax.d.ts +3 -0
- package/dist/rules/token/markdoc-syntax.d.ts.map +1 -0
- package/dist/rules/token/markdoc-syntax.js +119 -0
- package/dist/rules/token/markdoc-syntax.js.map +1 -0
- package/dist/rules/token/markdoc-unknown-tag.d.ts +3 -0
- package/dist/rules/token/markdoc-unknown-tag.d.ts.map +1 -0
- package/dist/rules/token/markdoc-unknown-tag.js +64 -0
- package/dist/rules/token/markdoc-unknown-tag.js.map +1 -0
- package/dist/rules/token/messages.d.ts +4 -0
- package/dist/rules/token/messages.d.ts.map +1 -0
- package/dist/rules/token/messages.js +20 -0
- package/dist/rules/token/messages.js.map +1 -0
- package/dist/rules/token/no-alt-text.d.ts +3 -0
- package/dist/rules/token/no-alt-text.d.ts.map +1 -0
- package/dist/rules/token/no-alt-text.js +47 -0
- package/dist/rules/token/no-alt-text.js.map +1 -0
- package/dist/rules/token/no-bare-urls.d.ts +3 -0
- package/dist/rules/token/no-bare-urls.d.ts.map +1 -0
- package/dist/rules/token/no-bare-urls.js +88 -0
- package/dist/rules/token/no-bare-urls.js.map +1 -0
- package/dist/rules/token/no-blanks-blockquote.d.ts +3 -0
- package/dist/rules/token/no-blanks-blockquote.d.ts.map +1 -0
- package/dist/rules/token/no-blanks-blockquote.js +39 -0
- package/dist/rules/token/no-blanks-blockquote.js.map +1 -0
- package/dist/rules/token/no-duplicate-heading.d.ts +3 -0
- package/dist/rules/token/no-duplicate-heading.d.ts.map +1 -0
- package/dist/rules/token/no-duplicate-heading.js +101 -0
- package/dist/rules/token/no-duplicate-heading.js.map +1 -0
- package/dist/rules/token/no-duplicate-link-destinations.d.ts +3 -0
- package/dist/rules/token/no-duplicate-link-destinations.d.ts.map +1 -0
- package/dist/rules/token/no-duplicate-link-destinations.js +65 -0
- package/dist/rules/token/no-duplicate-link-destinations.js.map +1 -0
- package/dist/rules/token/no-emphasis-as-heading.d.ts +3 -0
- package/dist/rules/token/no-emphasis-as-heading.d.ts.map +1 -0
- package/dist/rules/token/no-emphasis-as-heading.js +44 -0
- package/dist/rules/token/no-emphasis-as-heading.js.map +1 -0
- package/dist/rules/token/no-empty-headings.d.ts +3 -0
- package/dist/rules/token/no-empty-headings.d.ts.map +1 -0
- package/dist/rules/token/no-empty-headings.js +28 -0
- package/dist/rules/token/no-empty-headings.js.map +1 -0
- package/dist/rules/token/no-empty-links.d.ts +3 -0
- package/dist/rules/token/no-empty-links.d.ts.map +1 -0
- package/dist/rules/token/no-empty-links.js +67 -0
- package/dist/rules/token/no-empty-links.js.map +1 -0
- package/dist/rules/token/no-hard-tabs.d.ts +3 -0
- package/dist/rules/token/no-hard-tabs.d.ts.map +1 -0
- package/dist/rules/token/no-hard-tabs.js +76 -0
- package/dist/rules/token/no-hard-tabs.js.map +1 -0
- package/dist/rules/token/no-inline-html.d.ts +3 -0
- package/dist/rules/token/no-inline-html.d.ts.map +1 -0
- package/dist/rules/token/no-inline-html.js +45 -0
- package/dist/rules/token/no-inline-html.js.map +1 -0
- package/dist/rules/token/no-missing-space-atx.d.ts +3 -0
- package/dist/rules/token/no-missing-space-atx.d.ts.map +1 -0
- package/dist/rules/token/no-missing-space-atx.js +36 -0
- package/dist/rules/token/no-missing-space-atx.js.map +1 -0
- package/dist/rules/token/no-missing-space-closed-atx.d.ts +3 -0
- package/dist/rules/token/no-missing-space-closed-atx.d.ts.map +1 -0
- package/dist/rules/token/no-missing-space-closed-atx.js +45 -0
- package/dist/rules/token/no-missing-space-closed-atx.js.map +1 -0
- package/dist/rules/token/no-multiple-blanks.d.ts +3 -0
- package/dist/rules/token/no-multiple-blanks.d.ts.map +1 -0
- package/dist/rules/token/no-multiple-blanks.js +35 -0
- package/dist/rules/token/no-multiple-blanks.js.map +1 -0
- package/dist/rules/token/no-multiple-space-atx.d.ts +13 -0
- package/dist/rules/token/no-multiple-space-atx.d.ts.map +1 -0
- package/dist/rules/token/no-multiple-space-atx.js +50 -0
- package/dist/rules/token/no-multiple-space-atx.js.map +1 -0
- package/dist/rules/token/no-multiple-space-blockquote.d.ts +3 -0
- package/dist/rules/token/no-multiple-space-blockquote.d.ts.map +1 -0
- package/dist/rules/token/no-multiple-space-blockquote.js +43 -0
- package/dist/rules/token/no-multiple-space-blockquote.js.map +1 -0
- package/dist/rules/token/no-multiple-space-closed-atx.d.ts +3 -0
- package/dist/rules/token/no-multiple-space-closed-atx.d.ts.map +1 -0
- package/dist/rules/token/no-multiple-space-closed-atx.js +19 -0
- package/dist/rules/token/no-multiple-space-closed-atx.js.map +1 -0
- package/dist/rules/token/no-reversed-links.d.ts +3 -0
- package/dist/rules/token/no-reversed-links.d.ts.map +1 -0
- package/dist/rules/token/no-reversed-links.js +52 -0
- package/dist/rules/token/no-reversed-links.js.map +1 -0
- package/dist/rules/token/no-space-in-code.d.ts +3 -0
- package/dist/rules/token/no-space-in-code.d.ts.map +1 -0
- package/dist/rules/token/no-space-in-code.js +75 -0
- package/dist/rules/token/no-space-in-code.js.map +1 -0
- package/dist/rules/token/no-space-in-emphasis.d.ts +3 -0
- package/dist/rules/token/no-space-in-emphasis.d.ts.map +1 -0
- package/dist/rules/token/no-space-in-emphasis.js +79 -0
- package/dist/rules/token/no-space-in-emphasis.js.map +1 -0
- package/dist/rules/token/no-space-in-links.d.ts +3 -0
- package/dist/rules/token/no-space-in-links.d.ts.map +1 -0
- package/dist/rules/token/no-space-in-links.js +48 -0
- package/dist/rules/token/no-space-in-links.js.map +1 -0
- package/dist/rules/token/no-trailing-punctuation.d.ts +3 -0
- package/dist/rules/token/no-trailing-punctuation.d.ts.map +1 -0
- package/dist/rules/token/no-trailing-punctuation.js +38 -0
- package/dist/rules/token/no-trailing-punctuation.js.map +1 -0
- package/dist/rules/token/no-trailing-spaces.d.ts +3 -0
- package/dist/rules/token/no-trailing-spaces.d.ts.map +1 -0
- package/dist/rules/token/no-trailing-spaces.js +89 -0
- package/dist/rules/token/no-trailing-spaces.js.map +1 -0
- package/dist/rules/token/ol-prefix.d.ts +3 -0
- package/dist/rules/token/ol-prefix.d.ts.map +1 -0
- package/dist/rules/token/ol-prefix.js +70 -0
- package/dist/rules/token/ol-prefix.js.map +1 -0
- package/dist/rules/token/proper-names.d.ts +3 -0
- package/dist/rules/token/proper-names.d.ts.map +1 -0
- package/dist/rules/token/proper-names.js +93 -0
- package/dist/rules/token/proper-names.js.map +1 -0
- package/dist/rules/token/reference-links-images.d.ts +3 -0
- package/dist/rules/token/reference-links-images.d.ts.map +1 -0
- package/dist/rules/token/reference-links-images.js +36 -0
- package/dist/rules/token/reference-links-images.js.map +1 -0
- package/dist/rules/token/required-headings.d.ts +3 -0
- package/dist/rules/token/required-headings.d.ts.map +1 -0
- package/dist/rules/token/required-headings.js +83 -0
- package/dist/rules/token/required-headings.js.map +1 -0
- package/dist/rules/token/single-h1.d.ts +3 -0
- package/dist/rules/token/single-h1.d.ts.map +1 -0
- package/dist/rules/token/single-h1.js +56 -0
- package/dist/rules/token/single-h1.js.map +1 -0
- package/dist/rules/token/single-trailing-newline.d.ts +3 -0
- package/dist/rules/token/single-trailing-newline.d.ts.map +1 -0
- package/dist/rules/token/single-trailing-newline.js +25 -0
- package/dist/rules/token/single-trailing-newline.js.map +1 -0
- package/dist/rules/token/strong-style.d.ts +3 -0
- package/dist/rules/token/strong-style.d.ts.map +1 -0
- package/dist/rules/token/strong-style.js +51 -0
- package/dist/rules/token/strong-style.js.map +1 -0
- package/dist/rules/token/table-column-count.d.ts +3 -0
- package/dist/rules/token/table-column-count.d.ts.map +1 -0
- package/dist/rules/token/table-column-count.js +44 -0
- package/dist/rules/token/table-column-count.js.map +1 -0
- package/dist/rules/token/table-column-style.d.ts +3 -0
- package/dist/rules/token/table-column-style.d.ts.map +1 -0
- package/dist/rules/token/table-column-style.js +179 -0
- package/dist/rules/token/table-column-style.js.map +1 -0
- package/dist/rules/token/table-pipe-style.d.ts +3 -0
- package/dist/rules/token/table-pipe-style.d.ts.map +1 -0
- package/dist/rules/token/table-pipe-style.js +54 -0
- package/dist/rules/token/table-pipe-style.js.map +1 -0
- package/dist/rules/token/ul-indent.d.ts +3 -0
- package/dist/rules/token/ul-indent.d.ts.map +1 -0
- package/dist/rules/token/ul-indent.js +72 -0
- package/dist/rules/token/ul-indent.js.map +1 -0
- package/dist/rules/token/ul-style.d.ts +3 -0
- package/dist/rules/token/ul-style.d.ts.map +1 -0
- package/dist/rules/token/ul-style.js +80 -0
- package/dist/rules/token/ul-style.js.map +1 -0
- package/dist/rules/types.d.ts +81 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/utils.d.ts +29 -0
- package/dist/rules/utils.d.ts.map +1 -0
- package/dist/{assertions → rules}/utils.js +27 -0
- package/dist/rules/utils.js.map +1 -0
- package/dist/scopes/extractor.d.ts +7 -0
- package/dist/scopes/extractor.d.ts.map +1 -0
- package/dist/scopes/extractor.js +475 -0
- package/dist/scopes/extractor.js.map +1 -0
- package/dist/scopes/selector.d.ts +51 -0
- package/dist/scopes/selector.d.ts.map +1 -0
- package/dist/scopes/selector.js +121 -0
- package/dist/scopes/selector.js.map +1 -0
- package/dist/scopes/sentences.d.ts +15 -0
- package/dist/scopes/sentences.d.ts.map +1 -0
- package/dist/scopes/sentences.js +124 -0
- package/dist/scopes/sentences.js.map +1 -0
- package/dist/scopes/types.d.ts +44 -0
- package/dist/scopes/types.d.ts.map +1 -0
- package/dist/scopes/types.js +2 -0
- package/dist/scopes/types.js.map +1 -0
- package/dist/scopes/vocabulary.d.ts +16 -0
- package/dist/scopes/vocabulary.d.ts.map +1 -0
- package/dist/scopes/vocabulary.js +70 -0
- package/dist/scopes/vocabulary.js.map +1 -0
- package/dist/types/assertions.d.ts +95 -32
- package/dist/types/assertions.d.ts.map +1 -1
- package/dist/types/problems.d.ts +4 -5
- package/dist/types/problems.d.ts.map +1 -1
- package/dist/types/rules.d.ts +4 -6
- package/dist/types/rules.d.ts.map +1 -1
- package/examples/appendices/google.appendix.yaml +91 -0
- package/examples/appendices/inclusive-language.appendix.yaml +61 -0
- package/examples/appendices/microsoft.appendix.yaml +99 -0
- package/examples/appendices/plain-language.appendix.yaml +88 -0
- package/examples/google.yaml +1525 -0
- package/examples/inclusive-language.yaml +304 -0
- package/examples/microsoft.yaml +1542 -0
- package/examples/plain-language.yaml +325 -0
- package/package.json +49 -16
- package/presets/google/PROVENANCE.md +1022 -0
- package/presets/google/sources.json +192 -0
- package/presets/inclusive-language/PROVENANCE.md +174 -0
- package/presets/inclusive-language/sources.json +107 -0
- package/presets/microsoft/PROVENANCE.md +1555 -0
- package/presets/microsoft/sources.json +494 -0
- package/presets/plain-language/PROVENANCE.md +364 -0
- package/presets/plain-language/sources.json +108 -0
- package/dist/assertions/bullet-style.d.ts +0 -3
- package/dist/assertions/bullet-style.d.ts.map +0 -1
- package/dist/assertions/bullet-style.js +0 -60
- package/dist/assertions/bullet-style.js.map +0 -1
- package/dist/assertions/index.d.ts +0 -21
- package/dist/assertions/index.d.ts.map +0 -1
- package/dist/assertions/index.js +0 -30
- package/dist/assertions/index.js.map +0 -1
- package/dist/assertions/max-image-size.d.ts +0 -3
- package/dist/assertions/max-image-size.d.ts.map +0 -1
- package/dist/assertions/max-image-size.js +0 -73
- package/dist/assertions/max-image-size.js.map +0 -1
- package/dist/assertions/max-line-length.d.ts +0 -3
- package/dist/assertions/max-line-length.d.ts.map +0 -1
- package/dist/assertions/max-line-length.js +0 -68
- package/dist/assertions/max-line-length.js.map +0 -1
- package/dist/assertions/no-broken-fragment-links.d.ts +0 -3
- package/dist/assertions/no-broken-fragment-links.d.ts.map +0 -1
- package/dist/assertions/no-broken-fragment-links.js +0 -79
- package/dist/assertions/no-broken-fragment-links.js.map +0 -1
- package/dist/assertions/no-duplicate-headings.d.ts +0 -3
- package/dist/assertions/no-duplicate-headings.d.ts.map +0 -1
- package/dist/assertions/no-duplicate-headings.js +0 -66
- package/dist/assertions/no-duplicate-headings.js.map +0 -1
- package/dist/assertions/no-hard-tabs.d.ts +0 -3
- package/dist/assertions/no-hard-tabs.d.ts.map +0 -1
- package/dist/assertions/no-hard-tabs.js +0 -63
- package/dist/assertions/no-hard-tabs.js.map +0 -1
- package/dist/assertions/no-trailing-spaces.d.ts +0 -3
- package/dist/assertions/no-trailing-spaces.d.ts.map +0 -1
- package/dist/assertions/no-trailing-spaces.js +0 -72
- package/dist/assertions/no-trailing-spaces.js.map +0 -1
- package/dist/assertions/pattern.d.ts +0 -3
- package/dist/assertions/pattern.d.ts.map +0 -1
- package/dist/assertions/pattern.js +0 -39
- package/dist/assertions/pattern.js.map +0 -1
- package/dist/assertions/semantic-line-breaks.d.ts +0 -3
- package/dist/assertions/semantic-line-breaks.d.ts.map +0 -1
- package/dist/assertions/semantic-line-breaks.js +0 -152
- package/dist/assertions/semantic-line-breaks.js.map +0 -1
- package/dist/assertions/swap.d.ts +0 -3
- package/dist/assertions/swap.d.ts.map +0 -1
- package/dist/assertions/swap.js +0 -39
- package/dist/assertions/swap.js.map +0 -1
- package/dist/assertions/utils.d.ts +0 -8
- package/dist/assertions/utils.d.ts.map +0 -1
- package/dist/assertions/utils.js.map +0 -1
- package/dist/core/scope-parser.d.ts +0 -26
- package/dist/core/scope-parser.d.ts.map +0 -1
- package/dist/core/scope-parser.js +0 -110
- package/dist/core/scope-parser.js.map +0 -1
- package/dist/files.d.ts +0 -2
- package/dist/files.d.ts.map +0 -1
- package/dist/files.js +0 -39
- package/dist/files.js.map +0 -1
- package/dist/load-config.d.ts +0 -25
- package/dist/load-config.d.ts.map +0 -1
- package/dist/load-config.js +0 -104
- package/dist/load-config.js.map +0 -1
- package/dist/load.d.ts +0 -25
- package/dist/load.d.ts.map +0 -1
- package/dist/load.js +0 -112
- package/dist/load.js.map +0 -1
- package/dist/scope.d.ts +0 -26
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -110
- package/dist/scope.js.map +0 -1
- package/dist/types.d.ts +0 -109
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts +0 -31
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -154
- package/dist/validate.js.map +0 -1
- /package/dist/{types.js → parser/types.js} +0 -0
package/dist/config/validate.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
1
3
|
import Ajv from '@redocly/ajv';
|
|
2
4
|
import addFormats from 'ajv-formats';
|
|
3
|
-
import
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
6
|
+
import { resolveMarkdocConfig } from '../parser/markdoc/schema.js';
|
|
7
|
+
import { RECHECK_CONFIG_SCHEMA, MARKDOC_TAG_SCHEMA } from './schema.js';
|
|
8
|
+
import { validateScopeSelector } from '../scopes/vocabulary.js';
|
|
9
|
+
import { tokenizeSelector, wholeDocumentKeywordProblems } from '../scopes/selector.js';
|
|
10
|
+
import { resolveExtends } from './presets/index.js';
|
|
11
|
+
import { resolveAssertion } from '../rules/registry.js';
|
|
12
|
+
import { resolveDictionaryPaths } from '../rules/scope/spelling.js';
|
|
4
13
|
const ajv = new Ajv({
|
|
5
14
|
useDefaults: true,
|
|
6
15
|
allErrors: true,
|
|
@@ -8,6 +17,10 @@ const ajv = new Ajv({
|
|
|
8
17
|
});
|
|
9
18
|
addFormats(ajv); // mismatching AJV typing due to fork
|
|
10
19
|
ajv.addSchema(RECHECK_CONFIG_SCHEMA, 'recheck-config');
|
|
20
|
+
// Compiled once so `markdoc.extend.tagsFile` entries get exactly the same
|
|
21
|
+
// per-tag shape check a config's own inline `extend.tags` gets from the
|
|
22
|
+
// schema above, without recompiling on every validate() call.
|
|
23
|
+
const validateMarkdocTagShape = ajv.compile(MARKDOC_TAG_SCHEMA);
|
|
11
24
|
/**
|
|
12
25
|
* Validates configuration structure using JSON Schema
|
|
13
26
|
*/
|
|
@@ -18,11 +31,22 @@ function validateStructure(config) {
|
|
|
18
31
|
}
|
|
19
32
|
const valid = validate(config);
|
|
20
33
|
if (!valid && validate.errors) {
|
|
21
|
-
return validate.errors.map((error) =>
|
|
22
|
-
message
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
return validate.errors.map((error) => {
|
|
35
|
+
// AJV's own `additionalProperties` message ("must NOT have additional
|
|
36
|
+
// properties") never names the offending key in `error.message`
|
|
37
|
+
// itself — it's only available on `error.params.additionalProperty`.
|
|
38
|
+
// Naming it here is what turns a schema-illegal key (e.g. the removed
|
|
39
|
+
// `autoFixable`) into an actionable, greppable error rather than a
|
|
40
|
+
// "which property?" guessing game.
|
|
41
|
+
const extra = error.keyword === 'additionalProperties' && error.params?.additionalProperty
|
|
42
|
+
? ` (unknown property "${error.params.additionalProperty}")`
|
|
43
|
+
: '';
|
|
44
|
+
return {
|
|
45
|
+
message: `${error.instancePath || '/'}: ${error.message}${extra}`,
|
|
46
|
+
path: error.instancePath,
|
|
47
|
+
value: error.data,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
26
50
|
}
|
|
27
51
|
return [];
|
|
28
52
|
}
|
|
@@ -37,61 +61,1117 @@ function validateAssertions(rule, name, errors) {
|
|
|
37
61
|
});
|
|
38
62
|
return;
|
|
39
63
|
}
|
|
40
|
-
for (const
|
|
64
|
+
for (const assertionType of Object.keys(rule.assertions)) {
|
|
65
|
+
let resolved;
|
|
41
66
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'bullet-style':
|
|
51
|
-
case 'semantic-line-breaks':
|
|
52
|
-
case 'no-hard-tabs':
|
|
53
|
-
case 'no-duplicate-headings':
|
|
54
|
-
case 'no-broken-fragment-links':
|
|
55
|
-
break;
|
|
56
|
-
default:
|
|
57
|
-
errors.push({
|
|
58
|
-
message: `Rule "${name}": unknown assertion type "${assertionType}"`,
|
|
59
|
-
path: `${name}.assertions.${assertionType}`,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
67
|
+
// Delegates to the same registry runRules() uses to dispatch
|
|
68
|
+
// assertions (rules/registry.ts resolveAssertion), rather than a
|
|
69
|
+
// hand-maintained list of known assertion ids duplicated here. Every
|
|
70
|
+
// scope AND token rule (including every markdownlint-ported rule
|
|
71
|
+
// registered via src/rules/token/index.ts) is "known" the moment
|
|
72
|
+
// it's registered, so this can't silently drift out of sync the way
|
|
73
|
+
// a hardcoded switch/case list did per rule-porting batch.
|
|
74
|
+
resolved = resolveAssertion(assertionType);
|
|
62
75
|
}
|
|
63
|
-
catch
|
|
76
|
+
catch {
|
|
64
77
|
errors.push({
|
|
65
|
-
message: `Rule "${name}":
|
|
78
|
+
message: `Rule "${name}": unknown assertion type "${assertionType}"`,
|
|
66
79
|
path: `${name}.assertions.${assertionType}`,
|
|
67
80
|
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Scope-rule assertions (pattern, occurrence, swap, ...) each have their
|
|
84
|
+
// own dedicated per-assertion validator below; token rules (the 53
|
|
85
|
+
// markdownlint-ported rules) had no option checking at all until now --
|
|
86
|
+
// see validateTokenRuleOptions.
|
|
87
|
+
if (resolved.kind === 'token') {
|
|
88
|
+
validateTokenRuleOptions(rule, name, assertionType, resolved.rule, errors);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// A misspelled option on a ported (token) rule used to validate clean and
|
|
93
|
+
// silently no-op -- invisible in a 100-rule style-guide config. Each token
|
|
94
|
+
// rule's own `defaults` object is the schema of record: it's the exact set
|
|
95
|
+
// of keys the rule reads off `ctx.config` (see e.g. rules/token/line-length.ts
|
|
96
|
+
// `defaults: { message, lineLength, codeBlocks, tables, headings, ... }`).
|
|
97
|
+
// `message` is always allowed because every token rule's `defaults` includes
|
|
98
|
+
// it (verified for all 53 ported rules) -- this is distinct from the
|
|
99
|
+
// RULE-level `message`/`severity`/`scope`/`fix`/`link`/`excludes`/
|
|
100
|
+
// `appliesTo`/`exceptions`/`assertions` keys, which are not assertion
|
|
101
|
+
// options and are validated elsewhere (schema.ts / validateAssertions).
|
|
102
|
+
function validateTokenRuleOptions(rule, name, id, tokenRule, errors) {
|
|
103
|
+
const optionsObject = requireOptionsObject(rule, name, id, errors);
|
|
104
|
+
if (!optionsObject)
|
|
105
|
+
return;
|
|
106
|
+
const allowed = new Set(Object.keys(tokenRule.defaults));
|
|
107
|
+
for (const key of Object.keys(optionsObject)) {
|
|
108
|
+
if (!allowed.has(key)) {
|
|
109
|
+
errors.push({
|
|
110
|
+
message: `Rule "${name}": unknown option "${key}" for assertion "${id}" (accepted: ${[...allowed].sort().join(', ')})`,
|
|
111
|
+
path: `${name}.assertions.${id}.${key}`,
|
|
112
|
+
value: key,
|
|
113
|
+
});
|
|
68
114
|
}
|
|
69
115
|
}
|
|
70
116
|
}
|
|
71
117
|
/**
|
|
72
|
-
*
|
|
118
|
+
* Shared guard for every per-assertion option validator below: returns the
|
|
119
|
+
* assertion's options when they are a plain object, `undefined` when the
|
|
120
|
+
* assertion isn't configured on this rule, and pushes an "options must be
|
|
121
|
+
* an object" error for anything else. The JSON schema can't catch this
|
|
122
|
+
* shape mistake (`assertions` values are `additionalProperties: true`), so
|
|
123
|
+
* without it e.g. `occurrence: "oops"` validates cleanly and misbehaves at
|
|
124
|
+
* lint time.
|
|
73
125
|
*/
|
|
74
|
-
function
|
|
126
|
+
function requireOptionsObject(rule, name, assertionId, errors) {
|
|
127
|
+
const assertions = rule.assertions;
|
|
128
|
+
if (!assertions || typeof assertions !== 'object' || !(assertionId in assertions)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const config = assertions[assertionId];
|
|
132
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
133
|
+
errors.push({
|
|
134
|
+
message: `Rule "${name}": ${assertionId} assertion options must be an object`,
|
|
135
|
+
path: `${name}.assertions.${assertionId}`,
|
|
136
|
+
});
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
// `negate` is included here (rather than left to fall through to the
|
|
142
|
+
// generic "unknown option" error below) so a config that sets it gets ONE
|
|
143
|
+
// specific, actionable message -- see the dedicated check in
|
|
144
|
+
// validatePatternOptions, not a redundant generic one alongside it.
|
|
145
|
+
const PATTERN_OPTION_KEYS = new Set(['tokens', 'ignoreCase', 'nonword', 'includeCode', 'negate']);
|
|
146
|
+
/**
|
|
147
|
+
* Rejects the removed `pattern` option `negate`. Git history shows it never
|
|
148
|
+
* functioned in ANY version of the engine — the check always sat inside the
|
|
149
|
+
* match-iteration loop, so `negate: true` reported nothing, ever, and a
|
|
150
|
+
* pattern's ABSENCE never reported either. Rather than silently ignoring a
|
|
151
|
+
* config key that reads like it inverts the rule, validation fails loudly;
|
|
152
|
+
* existence checks ("flag when a pattern is absent") are planned as a
|
|
153
|
+
* Vale-parity feature.
|
|
154
|
+
*/
|
|
155
|
+
function validatePatternOptions(rule, name, errors) {
|
|
156
|
+
const patternConfig = requireOptionsObject(rule, name, 'pattern', errors);
|
|
157
|
+
if (!patternConfig)
|
|
158
|
+
return;
|
|
159
|
+
for (const key of Object.keys(patternConfig)) {
|
|
160
|
+
if (!PATTERN_OPTION_KEYS.has(key)) {
|
|
161
|
+
errors.push({
|
|
162
|
+
message: `Rule "${name}": unknown pattern option "${key}"`,
|
|
163
|
+
path: `${name}.assertions.pattern.${key}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if ('negate' in patternConfig) {
|
|
168
|
+
errors.push({
|
|
169
|
+
message: `Rule "${name}": pattern option "negate" was removed — it never worked ` +
|
|
170
|
+
`(it never reported anything); remove it from the config`,
|
|
171
|
+
path: `${name}.assertions.pattern.negate`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// `tokens`, `ignoreCase`, and `nonword` used to have no type check at all --
|
|
175
|
+
// `tokens: "ab"` (a string, not an array) validated
|
|
176
|
+
// clean, then pattern.ts's `for (const token of tokens)` iterated the
|
|
177
|
+
// STRING CHARACTER BY CHARACTER ('a' and 'b' each compiled as their own
|
|
178
|
+
// regex), and `ignoreCase: "yes"` (any non-empty string is truthy)
|
|
179
|
+
// silently flipped case-sensitivity on a typo. `tokens` is required (not
|
|
180
|
+
// `tokens?:` -- see PatternAssertion in types/assertions.ts) and, like
|
|
181
|
+
// `swap`'s `pairs` and `consistency`'s `either`, an EMPTY tokens array can
|
|
182
|
+
// never report anything, so both shape and non-emptiness are checked here
|
|
183
|
+
// -- same reasoning as validateSwapOptions/validateConsistencyOptions.
|
|
184
|
+
const { tokens, ignoreCase, nonword } = patternConfig;
|
|
185
|
+
const isValidTokens = Array.isArray(tokens) &&
|
|
186
|
+
tokens.length > 0 &&
|
|
187
|
+
tokens.every((token) => typeof token === 'string');
|
|
188
|
+
if (!isValidTokens) {
|
|
189
|
+
errors.push({
|
|
190
|
+
message: `Rule "${name}": pattern requires "tokens" to be a non-empty array of strings`,
|
|
191
|
+
path: `${name}.assertions.pattern.tokens`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
|
|
195
|
+
errors.push({
|
|
196
|
+
message: `Rule "${name}": pattern option "ignoreCase" must be a boolean`,
|
|
197
|
+
path: `${name}.assertions.pattern.ignoreCase`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (nonword !== undefined && typeof nonword !== 'boolean') {
|
|
201
|
+
errors.push({
|
|
202
|
+
message: `Rule "${name}": pattern option "nonword" must be a boolean`,
|
|
203
|
+
path: `${name}.assertions.pattern.nonword`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Default `false`: a match inside inline code is skipped, by range, not
|
|
207
|
+
// by masking the text (see rules/scope/pattern.ts), matching swap's
|
|
208
|
+
// `includeCode` option.
|
|
209
|
+
const includeCode = patternConfig.includeCode;
|
|
210
|
+
if (includeCode !== undefined && typeof includeCode !== 'boolean') {
|
|
211
|
+
errors.push({
|
|
212
|
+
message: `Rule "${name}": pattern option "includeCode" must be a boolean`,
|
|
213
|
+
path: `${name}.assertions.pattern.includeCode`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validates the `occurrence` assertion's options. Omitting BOTH `min` and
|
|
219
|
+
* `max` is an error — an occurrence assertion with no bound can never
|
|
220
|
+
* report anything — and so is an inverted range (`min` > `max`), which no
|
|
221
|
+
* count can satisfy.
|
|
222
|
+
*/
|
|
223
|
+
const OCCURRENCE_OPTION_KEYS = new Set(['pattern', 'min', 'max', 'ignoreCase']);
|
|
224
|
+
function validateOccurrenceOptions(rule, name, errors) {
|
|
225
|
+
const occurrenceConfig = requireOptionsObject(rule, name, 'occurrence', errors);
|
|
226
|
+
if (!occurrenceConfig)
|
|
227
|
+
return;
|
|
228
|
+
for (const key of Object.keys(occurrenceConfig)) {
|
|
229
|
+
if (!OCCURRENCE_OPTION_KEYS.has(key)) {
|
|
230
|
+
errors.push({
|
|
231
|
+
message: `Rule "${name}": unknown occurrence option "${key}"`,
|
|
232
|
+
path: `${name}.assertions.occurrence.${key}`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const { min, max, pattern } = occurrenceConfig;
|
|
237
|
+
if (min === undefined && max === undefined) {
|
|
238
|
+
errors.push({
|
|
239
|
+
message: `Rule "${name}": occurrence requires at least one of "min" or "max"`,
|
|
240
|
+
path: `${name}.assertions.occurrence`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// `min`/`max` used to have no type check at all --
|
|
244
|
+
// `occurrence: { pattern: ",", max: "two" }` used to validate clean, then
|
|
245
|
+
// occurrence.ts's `count > "two"` is NaN-false (a number is never `>` a
|
|
246
|
+
// non-numeric string), so a max-bounded rule NEVER fires. Every sibling
|
|
247
|
+
// numeric validator (`metric`, `length`, `list-length`) already checks
|
|
248
|
+
// this; occurrence didn't.
|
|
249
|
+
if (min !== undefined && typeof min !== 'number') {
|
|
250
|
+
errors.push({
|
|
251
|
+
message: `Rule "${name}": occurrence option "min" must be a number`,
|
|
252
|
+
path: `${name}.assertions.occurrence.min`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (max !== undefined && typeof max !== 'number') {
|
|
256
|
+
errors.push({
|
|
257
|
+
message: `Rule "${name}": occurrence option "max" must be a number`,
|
|
258
|
+
path: `${name}.assertions.occurrence.max`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (typeof min === 'number' && typeof max === 'number' && min > max) {
|
|
262
|
+
errors.push({
|
|
263
|
+
message: `Rule "${name}": occurrence "min" (${min}) must not exceed "max" (${max})`,
|
|
264
|
+
path: `${name}.assertions.occurrence`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// A missing/empty/non-string `pattern` silently compiles to an
|
|
268
|
+
// always-matching empty pattern in occurrence.ts's execute() — a
|
|
269
|
+
// max-bounded rule then floods every segment with false positives and a
|
|
270
|
+
// min-only rule can never fire. Reject loudly instead.
|
|
271
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
272
|
+
errors.push({
|
|
273
|
+
message: `Rule "${name}": occurrence requires a non-empty string "pattern"`,
|
|
274
|
+
path: `${name}.assertions.occurrence.pattern`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validates the `repetition` assertion's options. Both are optional
|
|
280
|
+
* (defaults `\w+` / `true` -- see rules/scope/repetition.ts), but when
|
|
281
|
+
* present `pattern` must be a non-empty string (an empty one compiles to an
|
|
282
|
+
* always-matching zero-width regex) and `ignoreCase` a boolean.
|
|
283
|
+
*/
|
|
284
|
+
const REPETITION_OPTION_KEYS = new Set(['pattern', 'ignoreCase']);
|
|
285
|
+
function validateRepetitionOptions(rule, name, errors) {
|
|
286
|
+
const repetitionConfig = requireOptionsObject(rule, name, 'repetition', errors);
|
|
287
|
+
if (!repetitionConfig)
|
|
288
|
+
return;
|
|
289
|
+
for (const key of Object.keys(repetitionConfig)) {
|
|
290
|
+
if (!REPETITION_OPTION_KEYS.has(key)) {
|
|
291
|
+
errors.push({
|
|
292
|
+
message: `Rule "${name}": unknown repetition option "${key}"`,
|
|
293
|
+
path: `${name}.assertions.repetition.${key}`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const { pattern, ignoreCase } = repetitionConfig;
|
|
298
|
+
if (pattern !== undefined && (typeof pattern !== 'string' || pattern.length === 0)) {
|
|
299
|
+
errors.push({
|
|
300
|
+
message: `Rule "${name}": repetition option "pattern" must be a non-empty string`,
|
|
301
|
+
path: `${name}.assertions.repetition.pattern`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
|
|
305
|
+
errors.push({
|
|
306
|
+
message: `Rule "${name}": repetition option "ignoreCase" must be a boolean`,
|
|
307
|
+
path: `${name}.assertions.repetition.ignoreCase`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Validates the `consistency` assertion's options. `either` is required and
|
|
313
|
+
* must be a non-empty object mapping one non-empty variant string to
|
|
314
|
+
* another -- with no pairs the assertion can never report anything, and an
|
|
315
|
+
* empty-string key would reach consistency.ts's scan loop as a zero-width
|
|
316
|
+
* regex.
|
|
317
|
+
*/
|
|
318
|
+
const CONSISTENCY_OPTION_KEYS = new Set(['either', 'ignoreCase']);
|
|
319
|
+
function validateConsistencyOptions(rule, name, errors) {
|
|
320
|
+
const consistencyConfig = requireOptionsObject(rule, name, 'consistency', errors);
|
|
321
|
+
if (!consistencyConfig)
|
|
322
|
+
return;
|
|
323
|
+
for (const key of Object.keys(consistencyConfig)) {
|
|
324
|
+
if (!CONSISTENCY_OPTION_KEYS.has(key)) {
|
|
325
|
+
errors.push({
|
|
326
|
+
message: `Rule "${name}": unknown consistency option "${key}"`,
|
|
327
|
+
path: `${name}.assertions.consistency.${key}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const { either, ignoreCase } = consistencyConfig;
|
|
332
|
+
if (!either || typeof either !== 'object' || Array.isArray(either)) {
|
|
333
|
+
errors.push({
|
|
334
|
+
message: `Rule "${name}": consistency requires "either" to be an object mapping one variant to another (e.g. behavior: behaviour)`,
|
|
335
|
+
path: `${name}.assertions.consistency.either`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const entries = Object.entries(either);
|
|
340
|
+
if (entries.length === 0) {
|
|
341
|
+
errors.push({
|
|
342
|
+
message: `Rule "${name}": consistency "either" must declare at least one variant pair`,
|
|
343
|
+
path: `${name}.assertions.consistency.either`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
for (const [variant, alternative] of entries) {
|
|
347
|
+
if (variant.length === 0) {
|
|
348
|
+
errors.push({
|
|
349
|
+
message: `Rule "${name}": consistency "either" entry keys must be non-empty strings`,
|
|
350
|
+
path: `${name}.assertions.consistency.either`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (typeof alternative !== 'string' || alternative.length === 0) {
|
|
354
|
+
errors.push({
|
|
355
|
+
message: `Rule "${name}": consistency "either" entry "${variant}" must map to a non-empty string variant`,
|
|
356
|
+
path: `${name}.assertions.consistency.either.${variant}`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
|
|
362
|
+
errors.push({
|
|
363
|
+
message: `Rule "${name}": consistency option "ignoreCase" must be a boolean`,
|
|
364
|
+
path: `${name}.assertions.consistency.ignoreCase`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Validates the `conditional` assertion's options. Both `first` and
|
|
370
|
+
* `second` are required, non-empty strings. Deliberately does NOT check
|
|
371
|
+
* that they compile as regexes: like `pattern`'s `tokens`, they are raw
|
|
372
|
+
* user patterns and an invalid one silently produces zero problems at
|
|
373
|
+
* runtime (see conditional.ts).
|
|
374
|
+
*/
|
|
375
|
+
const CONDITIONAL_OPTION_KEYS = new Set(['first', 'second', 'ignoreCase']);
|
|
376
|
+
function validateConditionalOptions(rule, name, errors) {
|
|
377
|
+
const conditionalConfig = requireOptionsObject(rule, name, 'conditional', errors);
|
|
378
|
+
if (!conditionalConfig)
|
|
379
|
+
return;
|
|
380
|
+
for (const key of Object.keys(conditionalConfig)) {
|
|
381
|
+
if (!CONDITIONAL_OPTION_KEYS.has(key)) {
|
|
382
|
+
errors.push({
|
|
383
|
+
message: `Rule "${name}": unknown conditional option "${key}"`,
|
|
384
|
+
path: `${name}.assertions.conditional.${key}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const { first, second, ignoreCase } = conditionalConfig;
|
|
389
|
+
if (typeof first !== 'string' || first.length === 0) {
|
|
390
|
+
errors.push({
|
|
391
|
+
message: `Rule "${name}": conditional requires a non-empty string "first"`,
|
|
392
|
+
path: `${name}.assertions.conditional.first`,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (typeof second !== 'string' || second.length === 0) {
|
|
396
|
+
errors.push({
|
|
397
|
+
message: `Rule "${name}": conditional requires a non-empty string "second"`,
|
|
398
|
+
path: `${name}.assertions.conditional.second`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
|
|
402
|
+
errors.push({
|
|
403
|
+
message: `Rule "${name}": conditional option "ignoreCase" must be a boolean`,
|
|
404
|
+
path: `${name}.assertions.conditional.ignoreCase`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Validates the `capitalization` assertion's options. `match` is required
|
|
410
|
+
* and must be a non-empty string (a `$`-style or a custom regex — an
|
|
411
|
+
* invalid regex is deliberately NOT rejected here; it silently produces
|
|
412
|
+
* zero problems at runtime, like `pattern`'s `tokens`). `style` is accepted
|
|
413
|
+
* alongside ANY `match` value, not just `$title` (the only one it affects)
|
|
414
|
+
* — setting it elsewhere is a documented harmless no-op. `exceptions`
|
|
415
|
+
* entries must be non-empty strings or they'd silently never match in the
|
|
416
|
+
* exception lookup. `builtinVocabulary` (default `true`, see
|
|
417
|
+
* ../data/proper-nouns.ts) must be a boolean when present.
|
|
418
|
+
*/
|
|
419
|
+
const CAPITALIZATION_OPTION_KEYS = new Set(['match', 'exceptions', 'style', 'builtinVocabulary']);
|
|
420
|
+
function validateCapitalizationOptions(rule, name, errors) {
|
|
421
|
+
const capitalizationConfig = requireOptionsObject(rule, name, 'capitalization', errors);
|
|
422
|
+
if (!capitalizationConfig)
|
|
423
|
+
return;
|
|
424
|
+
for (const key of Object.keys(capitalizationConfig)) {
|
|
425
|
+
if (!CAPITALIZATION_OPTION_KEYS.has(key)) {
|
|
426
|
+
errors.push({
|
|
427
|
+
message: `Rule "${name}": unknown capitalization option "${key}"`,
|
|
428
|
+
path: `${name}.assertions.capitalization.${key}`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const { match, exceptions, style, builtinVocabulary } = capitalizationConfig;
|
|
433
|
+
if (builtinVocabulary !== undefined && typeof builtinVocabulary !== 'boolean') {
|
|
434
|
+
errors.push({
|
|
435
|
+
message: `Rule "${name}": capitalization option "builtinVocabulary" must be a boolean`,
|
|
436
|
+
path: `${name}.assertions.capitalization.builtinVocabulary`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (typeof match !== 'string' || match.length === 0) {
|
|
440
|
+
errors.push({
|
|
441
|
+
message: `Rule "${name}": capitalization requires a non-empty string "match" ` +
|
|
442
|
+
`($title, $sentence, $lower, $upper, or a regex pattern)`,
|
|
443
|
+
path: `${name}.assertions.capitalization.match`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (style !== undefined && style !== 'ap' && style !== 'chicago') {
|
|
447
|
+
errors.push({
|
|
448
|
+
message: `Rule "${name}": capitalization option "style" must be "ap" or "chicago"`,
|
|
449
|
+
path: `${name}.assertions.capitalization.style`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (exceptions !== undefined) {
|
|
453
|
+
const isValidExceptions = Array.isArray(exceptions) &&
|
|
454
|
+
exceptions.every((entry) => typeof entry === 'string' && entry.length > 0);
|
|
455
|
+
if (!isValidExceptions) {
|
|
456
|
+
errors.push({
|
|
457
|
+
message: `Rule "${name}": capitalization option "exceptions" must be an array of non-empty strings`,
|
|
458
|
+
path: `${name}.assertions.capitalization.exceptions`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Validates the `metric` assertion's options. `formula` is required and
|
|
465
|
+
* must be one of the six formulas `computeReadability` supports -- an
|
|
466
|
+
* unrecognized value would otherwise throw at lint time. At least one of
|
|
467
|
+
* `min`/`max` is required, and an inverted range (`min` > `max`) is an
|
|
468
|
+
* error -- same reasoning as `occurrence` above.
|
|
469
|
+
*/
|
|
470
|
+
const METRIC_OPTION_KEYS = new Set(['formula', 'min', 'max']);
|
|
471
|
+
const METRIC_FORMULAS = new Set([
|
|
472
|
+
'flesch-reading-ease',
|
|
473
|
+
'flesch-kincaid-grade',
|
|
474
|
+
'gunning-fog',
|
|
475
|
+
'smog',
|
|
476
|
+
'coleman-liau',
|
|
477
|
+
'automated-readability',
|
|
478
|
+
]);
|
|
479
|
+
function validateMetricOptions(rule, name, errors) {
|
|
480
|
+
const metricConfig = requireOptionsObject(rule, name, 'metric', errors);
|
|
481
|
+
if (!metricConfig)
|
|
482
|
+
return;
|
|
483
|
+
for (const key of Object.keys(metricConfig)) {
|
|
484
|
+
if (!METRIC_OPTION_KEYS.has(key)) {
|
|
485
|
+
errors.push({
|
|
486
|
+
message: `Rule "${name}": unknown metric option "${key}"`,
|
|
487
|
+
path: `${name}.assertions.metric.${key}`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const { formula, min, max } = metricConfig;
|
|
492
|
+
if (typeof formula !== 'string' || !METRIC_FORMULAS.has(formula)) {
|
|
493
|
+
errors.push({
|
|
494
|
+
message: `Rule "${name}": metric requires "formula" to be one of ${[...METRIC_FORMULAS].join(', ')}`,
|
|
495
|
+
path: `${name}.assertions.metric.formula`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (min === undefined && max === undefined) {
|
|
499
|
+
errors.push({
|
|
500
|
+
message: `Rule "${name}": metric requires at least one of "min" or "max"`,
|
|
501
|
+
path: `${name}.assertions.metric`,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (min !== undefined && typeof min !== 'number') {
|
|
505
|
+
errors.push({
|
|
506
|
+
message: `Rule "${name}": metric option "min" must be a number`,
|
|
507
|
+
path: `${name}.assertions.metric.min`,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (max !== undefined && typeof max !== 'number') {
|
|
511
|
+
errors.push({
|
|
512
|
+
message: `Rule "${name}": metric option "max" must be a number`,
|
|
513
|
+
path: `${name}.assertions.metric.max`,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (typeof min === 'number' && typeof max === 'number' && min > max) {
|
|
517
|
+
errors.push({
|
|
518
|
+
message: `Rule "${name}": metric "min" (${min}) must not exceed "max" (${max})`,
|
|
519
|
+
path: `${name}.assertions.metric`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Shared `min`/`max` integer-range check for `length` and `list-length`:
|
|
525
|
+
* both measure a COUNT
|
|
526
|
+
* that can never be negative (characters/words/sentences/list items), so a
|
|
527
|
+
* bound of `min: 0` (or any `min <= 0`) can NEVER be violated by a real
|
|
528
|
+
* count -- "must have at least 0 words" is vacuously true for every
|
|
529
|
+
* document -- and a negative `max` (e.g. `max: -1`) is violated by EVERY
|
|
530
|
+
* real count, since no count is ever less than a negative number. Neither is
|
|
531
|
+
* a meaningful bound; both are silent no-op/always-fire footguns. `min` must
|
|
532
|
+
* therefore be a positive integer and `max` a non-negative integer when
|
|
533
|
+
* present (`max: 0` is a real, meaningful "must be empty" bound, unlike a
|
|
534
|
+
* negative one). Fractional bounds (`min: 2.5`) are also rejected: both
|
|
535
|
+
* assertions always measure whole units, so a fractional bound could never
|
|
536
|
+
* be matched exactly either. Only runs when the value is ALREADY a number --
|
|
537
|
+
* a wrong-typed value is reported once by the caller's own type check, not
|
|
538
|
+
* duplicated here.
|
|
539
|
+
*/
|
|
540
|
+
function validateCountBounds(name, assertionId, min, max, errors) {
|
|
541
|
+
if (typeof min === 'number' && (!Number.isInteger(min) || min < 1)) {
|
|
542
|
+
errors.push({
|
|
543
|
+
message: `Rule "${name}": ${assertionId} option "min" must be a positive integer ` +
|
|
544
|
+
`(${min} could never be violated by a real count)`,
|
|
545
|
+
path: `${name}.assertions.${assertionId}.min`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (typeof max === 'number' && (!Number.isInteger(max) || max < 0)) {
|
|
549
|
+
errors.push({
|
|
550
|
+
message: `Rule "${name}": ${assertionId} option "max" must be a non-negative integer ` +
|
|
551
|
+
`(${max} would be violated by every real count)`,
|
|
552
|
+
path: `${name}.assertions.${assertionId}.max`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Validates the `list-length` assertion's options (rules/token/list-length.ts
|
|
558
|
+
* -- a Recheck-original TOKEN rule, not a markdownlint port). Unlike the
|
|
559
|
+
* scope-assertion validators above (occurrence, metric, ...), the generic
|
|
560
|
+
* `validateTokenRuleOptions` already rejects an unknown option name for every
|
|
561
|
+
* token rule -- derived from `Object.keys(rule.defaults)`, which declares
|
|
562
|
+
* both `min` and `max` (see list-length.ts's doc comment on its `max:
|
|
563
|
+
* undefined` default) -- so this only adds the type/range checks that
|
|
564
|
+
* mirror validateOccurrenceOptions/validateMetricOptions: `min`/`max` must be
|
|
565
|
+
* numbers when present, an inverted range (`min` > `max`) is an error -- no
|
|
566
|
+
* item count could ever satisfy it -- and `min`/`max`
|
|
567
|
+
* must additionally be a positive/non-negative INTEGER, per
|
|
568
|
+
* validateCountBounds above: unlike occurrence/metric, list-length's counts
|
|
569
|
+
* can never be negative, so `min: 0` can never fire and `max: -1` always
|
|
570
|
+
* fires, neither a meaningful bound. Omitting BOTH `min` and `max` is
|
|
571
|
+
* deliberately NOT an error here, unlike occurrence/metric: list-length's own
|
|
572
|
+
* `defaults.min` is 2, so an empty `list-length: {}` is already a complete,
|
|
573
|
+
* meaningful configuration (flag any list under 2 items), not a no-op
|
|
574
|
+
* assertion with nothing to check.
|
|
575
|
+
*/
|
|
576
|
+
function validateListLengthOptions(rule, name, errors) {
|
|
577
|
+
const listLengthConfig = requireOptionsObject(rule, name, 'list-length', errors);
|
|
578
|
+
if (!listLengthConfig)
|
|
579
|
+
return;
|
|
580
|
+
const { min, max } = listLengthConfig;
|
|
581
|
+
if (min !== undefined && typeof min !== 'number') {
|
|
582
|
+
errors.push({
|
|
583
|
+
message: `Rule "${name}": list-length option "min" must be a number`,
|
|
584
|
+
path: `${name}.assertions.list-length.min`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (max !== undefined && typeof max !== 'number') {
|
|
588
|
+
errors.push({
|
|
589
|
+
message: `Rule "${name}": list-length option "max" must be a number`,
|
|
590
|
+
path: `${name}.assertions.list-length.max`,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
if (typeof min === 'number' && typeof max === 'number' && min > max) {
|
|
594
|
+
errors.push({
|
|
595
|
+
message: `Rule "${name}": list-length "min" (${min}) must not exceed "max" (${max})`,
|
|
596
|
+
path: `${name}.assertions.list-length`,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
validateCountBounds(name, 'list-length', min, max, errors);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Validates the `spelling` assertion's options. All are optional — an
|
|
603
|
+
* empty `spelling: {}` is valid (default dictionary). When present,
|
|
604
|
+
* `dictionary` must be a non-empty string, and `vocab`/`ignore` arrays of
|
|
605
|
+
* non-empty strings — an empty-string `ignore` pattern would compile to an
|
|
606
|
+
* always-matching regex, silencing every word. `builtinVocabulary` (default
|
|
607
|
+
* `true`, see ../data/proper-nouns.ts) must be a boolean when present.
|
|
608
|
+
*/
|
|
609
|
+
const SPELLING_OPTION_KEYS = new Set(['dictionary', 'vocab', 'ignore', 'builtinVocabulary']);
|
|
610
|
+
function validateSpellingOptions(rule, name, errors) {
|
|
611
|
+
const spellingConfig = requireOptionsObject(rule, name, 'spelling', errors);
|
|
612
|
+
if (!spellingConfig)
|
|
613
|
+
return;
|
|
614
|
+
for (const key of Object.keys(spellingConfig)) {
|
|
615
|
+
if (!SPELLING_OPTION_KEYS.has(key)) {
|
|
616
|
+
errors.push({
|
|
617
|
+
message: `Rule "${name}": unknown spelling option "${key}"`,
|
|
618
|
+
path: `${name}.assertions.spelling.${key}`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const { dictionary, vocab, ignore, builtinVocabulary } = spellingConfig;
|
|
623
|
+
if (builtinVocabulary !== undefined && typeof builtinVocabulary !== 'boolean') {
|
|
624
|
+
errors.push({
|
|
625
|
+
message: `Rule "${name}": spelling option "builtinVocabulary" must be a boolean`,
|
|
626
|
+
path: `${name}.assertions.spelling.builtinVocabulary`,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (dictionary !== undefined && (typeof dictionary !== 'string' || dictionary.length === 0)) {
|
|
630
|
+
errors.push({
|
|
631
|
+
message: `Rule "${name}": spelling option "dictionary" must be a non-empty string`,
|
|
632
|
+
path: `${name}.assertions.spelling.dictionary`,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (vocab !== undefined) {
|
|
636
|
+
const isValidVocab = Array.isArray(vocab) && vocab.every((word) => typeof word === 'string' && word.length > 0);
|
|
637
|
+
if (!isValidVocab) {
|
|
638
|
+
errors.push({
|
|
639
|
+
message: `Rule "${name}": spelling option "vocab" must be an array of non-empty strings`,
|
|
640
|
+
path: `${name}.assertions.spelling.vocab`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (ignore !== undefined) {
|
|
645
|
+
const isValidIgnore = Array.isArray(ignore) && ignore.every((word) => typeof word === 'string' && word.length > 0);
|
|
646
|
+
if (!isValidIgnore) {
|
|
647
|
+
errors.push({
|
|
648
|
+
message: `Rule "${name}": spelling option "ignore" must be an array of non-empty strings`,
|
|
649
|
+
path: `${name}.assertions.spelling.ignore`,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Validates the `length` assertion's options. `unit` is required and must be
|
|
656
|
+
* one of `'characters' | 'words' | 'sentences'` -- an unrecognized value
|
|
657
|
+
* would otherwise reach `length.ts`'s `measure()` and fall through to the
|
|
658
|
+
* word-tokenizer branch silently, scoring the wrong thing with no error.
|
|
659
|
+
* At least one of `min`/`max` is required, and an inverted range (`min` >
|
|
660
|
+
* `max`) is an error -- same reasoning as `occurrence`/`metric` above.
|
|
661
|
+
*/
|
|
662
|
+
const LENGTH_OPTION_KEYS = new Set(['unit', 'min', 'max']);
|
|
663
|
+
const LENGTH_UNITS = new Set(['characters', 'words', 'sentences']);
|
|
664
|
+
function validateLengthOptions(rule, name, errors) {
|
|
665
|
+
const lengthConfig = requireOptionsObject(rule, name, 'length', errors);
|
|
666
|
+
if (!lengthConfig)
|
|
667
|
+
return;
|
|
668
|
+
for (const key of Object.keys(lengthConfig)) {
|
|
669
|
+
if (!LENGTH_OPTION_KEYS.has(key)) {
|
|
670
|
+
errors.push({
|
|
671
|
+
message: `Rule "${name}": unknown length option "${key}"`,
|
|
672
|
+
path: `${name}.assertions.length.${key}`,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const { unit, min, max } = lengthConfig;
|
|
677
|
+
if (typeof unit !== 'string' || !LENGTH_UNITS.has(unit)) {
|
|
678
|
+
errors.push({
|
|
679
|
+
message: `Rule "${name}": length requires "unit" to be one of ${[...LENGTH_UNITS].join(', ')}`,
|
|
680
|
+
path: `${name}.assertions.length.unit`,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (min === undefined && max === undefined) {
|
|
684
|
+
errors.push({
|
|
685
|
+
message: `Rule "${name}": length requires at least one of "min" or "max"`,
|
|
686
|
+
path: `${name}.assertions.length`,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (min !== undefined && typeof min !== 'number') {
|
|
690
|
+
errors.push({
|
|
691
|
+
message: `Rule "${name}": length option "min" must be a number`,
|
|
692
|
+
path: `${name}.assertions.length.min`,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (max !== undefined && typeof max !== 'number') {
|
|
696
|
+
errors.push({
|
|
697
|
+
message: `Rule "${name}": length option "max" must be a number`,
|
|
698
|
+
path: `${name}.assertions.length.max`,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (typeof min === 'number' && typeof max === 'number' && min > max) {
|
|
702
|
+
errors.push({
|
|
703
|
+
message: `Rule "${name}": length "min" (${min}) must not exceed "max" (${max})`,
|
|
704
|
+
path: `${name}.assertions.length`,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
// `min: 0` (never
|
|
708
|
+
// violated -- a segment can't have fewer than 0 characters/words/
|
|
709
|
+
// sentences) and a negative `max` (always violated) are silent no-op/
|
|
710
|
+
// always-fire footguns, same reasoning as list-length's identical check
|
|
711
|
+
// above -- see validateCountBounds's doc comment.
|
|
712
|
+
validateCountBounds(name, 'length', min, max, errors);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Validates each find -> replace entry under `swap.pairs`: the KEY must be
|
|
716
|
+
* a non-empty string (an empty one escapes to a zero-width pattern in
|
|
717
|
+
* swap.ts's findMatches, same hazard as consistency's `either` keys); the
|
|
718
|
+
* VALUE must be a string, possibly empty -- an empty replacement is a
|
|
719
|
+
* legitimate "delete this word" swap.
|
|
720
|
+
*/
|
|
721
|
+
function validateSwapPairEntries(entries, name, path, errors) {
|
|
722
|
+
for (const [key, value] of entries) {
|
|
723
|
+
const entryPath = `${path}.${key}`;
|
|
724
|
+
if (key.length === 0) {
|
|
725
|
+
errors.push({
|
|
726
|
+
message: `Rule "${name}": swap "pairs" entry keys must be non-empty strings`,
|
|
727
|
+
path: entryPath,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (typeof value !== 'string') {
|
|
731
|
+
errors.push({
|
|
732
|
+
message: `Rule "${name}": swap "pairs" entry "${key}" must map to a string replacement`,
|
|
733
|
+
path: entryPath,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Validates the `swap` assertion's options. Exactly one shape is accepted
|
|
740
|
+
* -- the only one swap.ts's findMatches actually consumes:
|
|
741
|
+
*
|
|
742
|
+
* `{ ignoreCase?, wordBoundary?, keysAreRegex?, pairs: {find: replace} }`
|
|
743
|
+
*
|
|
744
|
+
* The legacy "direct" top-level shape (`swap: { he: they }`) is rejected
|
|
745
|
+
* with a migration hint: findMatches only ever reads `options.pairs`, so
|
|
746
|
+
* direct entries validated fine but were silently inert -- rejecting them
|
|
747
|
+
* turns that no-op into an actionable config error.
|
|
748
|
+
*/
|
|
749
|
+
const SWAP_RESERVED_KEYS = new Set([
|
|
750
|
+
'ignoreCase',
|
|
751
|
+
'wordBoundary',
|
|
752
|
+
'keysAreRegex',
|
|
753
|
+
'pairs',
|
|
754
|
+
'includeCode',
|
|
755
|
+
]);
|
|
756
|
+
const SWAP_BOOLEAN_OPTION_KEYS = [
|
|
757
|
+
'ignoreCase',
|
|
758
|
+
'wordBoundary',
|
|
759
|
+
'keysAreRegex',
|
|
760
|
+
'includeCode',
|
|
761
|
+
];
|
|
762
|
+
function validateSwapOptions(rule, name, errors) {
|
|
763
|
+
const swapConfig = requireOptionsObject(rule, name, 'swap', errors);
|
|
764
|
+
if (!swapConfig)
|
|
765
|
+
return;
|
|
766
|
+
for (const key of Object.keys(swapConfig)) {
|
|
767
|
+
if (!SWAP_RESERVED_KEYS.has(key)) {
|
|
768
|
+
errors.push({
|
|
769
|
+
message: `Rule "${name}": unknown swap option "${key}" -- swap does not accept ` +
|
|
770
|
+
`find -> replace entries at the top level; move find -> replace entries under "pairs:"`,
|
|
771
|
+
path: `${name}.assertions.swap.${key}`,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
for (const key of SWAP_BOOLEAN_OPTION_KEYS) {
|
|
776
|
+
const value = swapConfig[key];
|
|
777
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
778
|
+
errors.push({
|
|
779
|
+
message: `Rule "${name}": swap option "${key}" must be a boolean`,
|
|
780
|
+
path: `${name}.assertions.swap.${key}`,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!('pairs' in swapConfig)) {
|
|
785
|
+
errors.push({
|
|
786
|
+
message: `Rule "${name}": swap requires a "pairs" object mapping find -> replace strings`,
|
|
787
|
+
path: `${name}.assertions.swap.pairs`,
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const pairs = swapConfig.pairs;
|
|
792
|
+
if (!pairs ||
|
|
793
|
+
typeof pairs !== 'object' ||
|
|
794
|
+
Array.isArray(pairs) ||
|
|
795
|
+
Object.keys(pairs).length === 0) {
|
|
796
|
+
errors.push({
|
|
797
|
+
message: `Rule "${name}": swap option "pairs" must be a non-empty object mapping find -> replace strings`,
|
|
798
|
+
path: `${name}.assertions.swap.pairs`,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
validateSwapPairEntries(Object.entries(pairs), name, `${name}.assertions.swap.pairs`, errors);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Missing-peer validation for `spelling`: `nspell` and `dictionary-en` are
|
|
807
|
+
* OPTIONAL peer dependencies, so a config that enables `spelling` without
|
|
808
|
+
* them installed must fail here with an actionable install command rather
|
|
809
|
+
* than as a bare "Cannot find module" the first time a file is linted.
|
|
810
|
+
* Runs per spelling rule so a mix of default-dictionary and
|
|
811
|
+
* custom-dictionary rules gets the right install command for each; a
|
|
812
|
+
* config with no `spelling` assertion never reaches an `import()` call at
|
|
813
|
+
* all, keeping validate() lazy about the peers.
|
|
814
|
+
*
|
|
815
|
+
* ALSO validates that a custom `dictionary`
|
|
816
|
+
* path actually names a readable `.aff`/`.dic` pair: the check above only
|
|
817
|
+
* ever checked whether `nspell` itself imports, never whether the FILES a
|
|
818
|
+
* `dictionary` option points at exist — so a missing/unreadable custom
|
|
819
|
+
* dictionary used to pass validation cleanly and only fail (silently: see
|
|
820
|
+
* spelling.ts's `loadSpeller`/`spellerCache`) the first time a file was
|
|
821
|
+
* linted, disabling spelling for the rest of the process. Resolved via
|
|
822
|
+
* `resolveDictionaryPaths`, SHARED with spelling.ts's own
|
|
823
|
+
* `readCustomDictionary`, so validate() and the runtime can never disagree
|
|
824
|
+
* about which files a `dictionary` path names.
|
|
825
|
+
*/
|
|
826
|
+
async function checkSpellingPeerDependencies(rules) {
|
|
827
|
+
const errors = [];
|
|
828
|
+
const reportedMessages = new Set();
|
|
829
|
+
const reportedDictionaryPaths = new Set();
|
|
830
|
+
for (const rule of rules) {
|
|
831
|
+
const spellingConfig = rule.assertions?.['spelling'];
|
|
832
|
+
if (!spellingConfig || typeof spellingConfig !== 'object')
|
|
833
|
+
continue;
|
|
834
|
+
const dictionaryPath = spellingConfig.dictionary;
|
|
835
|
+
const hasCustomDictionary = typeof dictionaryPath === 'string' && dictionaryPath.length > 0;
|
|
836
|
+
let missingPeer = false;
|
|
837
|
+
try {
|
|
838
|
+
await import('nspell');
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
missingPeer = true;
|
|
842
|
+
}
|
|
843
|
+
// A custom-dictionary rule never touches `dictionary-en` (see
|
|
844
|
+
// spelling.ts's loadDictionary), so its absence must not fail it.
|
|
845
|
+
if (!hasCustomDictionary) {
|
|
846
|
+
try {
|
|
847
|
+
await import('dictionary-en');
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
missingPeer = true;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (missingPeer) {
|
|
854
|
+
const installCommand = hasCustomDictionary ? 'npm i nspell' : 'npm i nspell dictionary-en';
|
|
855
|
+
const peerNames = hasCustomDictionary ? '"nspell"' : '"nspell" and "dictionary-en"';
|
|
856
|
+
const message = `The spelling assertion requires the optional peer dependenc${hasCustomDictionary ? 'y' : 'ies'} ` +
|
|
857
|
+
`${peerNames} — run \`${installCommand}\` to enable it.`;
|
|
858
|
+
// Each distinct message is reported once, at the first offending
|
|
859
|
+
// rule's path; a mixed config still reports both install commands.
|
|
860
|
+
if (!reportedMessages.has(message)) {
|
|
861
|
+
reportedMessages.add(message);
|
|
862
|
+
errors.push({ message, path: `${rule.name}.assertions.spelling` });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// Independent of the peer-import check above: even when `nspell`
|
|
866
|
+
// imports fine, a custom `dictionary` option may still name files that
|
|
867
|
+
// don't exist or aren't readable. Dedupe by the raw dictionary path
|
|
868
|
+
// string so several rules sharing one bad path only report once.
|
|
869
|
+
if (hasCustomDictionary && !reportedDictionaryPaths.has(dictionaryPath)) {
|
|
870
|
+
reportedDictionaryPaths.add(dictionaryPath);
|
|
871
|
+
const { aff, dic } = resolveDictionaryPaths(dictionaryPath);
|
|
872
|
+
const unreadable = [];
|
|
873
|
+
for (const filePath of [aff, dic]) {
|
|
874
|
+
try {
|
|
875
|
+
await fs.access(filePath, fs.constants.R_OK);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
unreadable.push(filePath);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (unreadable.length > 0) {
|
|
882
|
+
errors.push({
|
|
883
|
+
message: `Rule "${rule.name}": spelling dictionary file${unreadable.length > 1 ? 's' : ''} ` +
|
|
884
|
+
`not found or not readable: ${unreadable.join(', ')}`,
|
|
885
|
+
path: `${rule.name}.assertions.spelling.dictionary`,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return errors;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Warns about a config that extends the `recheck/markdoc` preset without
|
|
894
|
+
* turning markdoc parsing on. The preset's four rules only look at
|
|
895
|
+
* `ctx.markdoc`, which the runner populates only when parsing is enabled, so
|
|
896
|
+
* such a config ships four rule entries that can never report. That is dead
|
|
897
|
+
* weight rather than a broken config, so this goes to `console.warn` (the
|
|
898
|
+
* validation result carries only errors, no warnings) and `isValid` stays
|
|
899
|
+
* `true`.
|
|
900
|
+
*
|
|
901
|
+
* Reads the raw, pre-`resolveExtends` `extends` array rather than the merged
|
|
902
|
+
* config: post-merge, the four rule keys the preset contributes are
|
|
903
|
+
* indistinguishable from a user hand-writing the same `recheck/markdoc-*` keys
|
|
904
|
+
* directly, which is a legitimate way to opt into only some of them and is not
|
|
905
|
+
* what this warning is about. The literal `"recheck/markdoc"` entry in
|
|
906
|
+
* `extends` is the one unambiguous signal that the preset itself was requested.
|
|
907
|
+
*/
|
|
908
|
+
function warnStaleMarkdocPreset(extendsList, markdocEnabled, warnOnce) {
|
|
909
|
+
if (markdocEnabled || !Array.isArray(extendsList))
|
|
910
|
+
return;
|
|
911
|
+
if (!extendsList.includes('recheck/markdoc'))
|
|
912
|
+
return;
|
|
913
|
+
warnOnce('recheck: config extends "recheck/markdoc" but "markdoc" parsing is off — its four rules ' +
|
|
914
|
+
'can never fire; set "markdoc: true" (or an object form) to enable them.');
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Reads, parses, and shape-checks `markdoc.extend.tagsFile`, resolved
|
|
918
|
+
* relative to `configDir`. Returns the file's tags (already validated per
|
|
919
|
+
* entry against the same `MARKDOC_TAG_SCHEMA` inline `extend.tags` uses) plus
|
|
920
|
+
* any config errors; a non-empty error list means the caller must treat
|
|
921
|
+
* markdoc as disabled for this call, the same as any other structurally
|
|
922
|
+
* invalid markdoc shape.
|
|
923
|
+
*
|
|
924
|
+
* Deliberately does not touch the filesystem unless `raw` actually names a
|
|
925
|
+
* `tagsFile` -- a config with markdoc off, or with only inline `extend.tags`,
|
|
926
|
+
* must never probe for a file it never referenced.
|
|
927
|
+
*/
|
|
928
|
+
async function loadMarkdocTagsFile(raw, configDir) {
|
|
929
|
+
const tagsFile = raw !== null && typeof raw === 'object' ? raw.extend?.tagsFile : undefined;
|
|
930
|
+
if (!tagsFile)
|
|
931
|
+
return { errors: [] };
|
|
932
|
+
const resolvedPath = path.resolve(configDir, tagsFile);
|
|
933
|
+
const errors = [];
|
|
934
|
+
let content;
|
|
935
|
+
try {
|
|
936
|
+
content = await fs.readFile(resolvedPath, 'utf8');
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
errors.push({
|
|
940
|
+
path: '/markdoc/extend/tagsFile',
|
|
941
|
+
message: `markdoc.extend.tagsFile: could not read "${resolvedPath}": ${error.message}`,
|
|
942
|
+
});
|
|
943
|
+
return { errors };
|
|
944
|
+
}
|
|
945
|
+
let parsed;
|
|
946
|
+
try {
|
|
947
|
+
parsed = yaml.load(content);
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
errors.push({
|
|
951
|
+
path: '/markdoc/extend/tagsFile',
|
|
952
|
+
message: `markdoc.extend.tagsFile: could not parse "${resolvedPath}" as YAML: ${error.message}`,
|
|
953
|
+
});
|
|
954
|
+
return { errors };
|
|
955
|
+
}
|
|
956
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
957
|
+
errors.push({
|
|
958
|
+
path: '/markdoc/extend/tagsFile',
|
|
959
|
+
message: `markdoc.extend.tagsFile: "${resolvedPath}" must be a YAML map of tag name to tag schema, got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'an array' : typeof parsed}`,
|
|
960
|
+
});
|
|
961
|
+
return { errors };
|
|
962
|
+
}
|
|
963
|
+
const fileTags = {};
|
|
964
|
+
for (const [tagName, tagValue] of Object.entries(parsed)) {
|
|
965
|
+
if (!validateMarkdocTagShape(tagValue)) {
|
|
966
|
+
const detail = ajv.errorsText(validateMarkdocTagShape.errors, { separator: '; ' });
|
|
967
|
+
errors.push({
|
|
968
|
+
path: `/markdoc/extend/tagsFile/${tagName}`,
|
|
969
|
+
message: `markdoc.extend.tagsFile: "${resolvedPath}" tag "${tagName}" is invalid: ${detail}`,
|
|
970
|
+
});
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
fileTags[tagName] = tagValue;
|
|
974
|
+
}
|
|
975
|
+
// Any per-tag shape failure invalidates the whole file's contribution --
|
|
976
|
+
// partially trusting a file that failed its own shape check would silently
|
|
977
|
+
// merge an unvalidated tag into the resolved schema.
|
|
978
|
+
if (errors.length > 0)
|
|
979
|
+
return { errors };
|
|
980
|
+
return { fileTags, errors: [] };
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Stale-pattern warning: a `pattern` assertion token that starts
|
|
984
|
+
* with the literal characters `^#` almost always indicates a config
|
|
985
|
+
* written for the pre-AST line-based scope extractor, where segment
|
|
986
|
+
* content still included the raw `#` heading marker. After the AST
|
|
987
|
+
* migration, non-`raw`/non-`all` scopes (e.g. `heading`, `sentence`,
|
|
988
|
+
* `paragraph`) hand `pattern` only the semantic TEXT of the segment — the
|
|
989
|
+
* literal markup is already stripped — so a token anchored on `^#` can
|
|
990
|
+
* never match and the rule silently does nothing. `scope: raw` (and the
|
|
991
|
+
* default `scope: all`, which also sees full raw file content) are exempt:
|
|
992
|
+
* both still see literal markup, so `^#` is a legitimate anchor there.
|
|
993
|
+
* This exact silent-death case was found in the repo's own recheck.yaml
|
|
994
|
+
* after the AST migration.
|
|
995
|
+
*/
|
|
996
|
+
function warnStalePatternPrefix(rule, name, warnOnce) {
|
|
997
|
+
const patternConfig = rule.assertions?.['pattern'];
|
|
998
|
+
if (!patternConfig || !Array.isArray(patternConfig.tokens))
|
|
999
|
+
return;
|
|
1000
|
+
const scopeEntries = rule.scope === undefined ? [] : Array.isArray(rule.scope) ? rule.scope : [rule.scope];
|
|
1001
|
+
// Each scope entry may itself be a `&`-joined selector clause (e.g.
|
|
1002
|
+
// '~blockquote & ~heading') — split and check every term, since ANY
|
|
1003
|
+
// non-raw/non-all term in the selector means some matched segments will
|
|
1004
|
+
// be semantic-text-only. Parsed via the selector module's own tokenizer
|
|
1005
|
+
// so this check can't drift from how compileSelector reads the entry.
|
|
1006
|
+
const scopeTerms = scopeEntries.flatMap((entry) => typeof entry === 'string' ? tokenizeSelector(entry).map(({ term }) => term) : []);
|
|
1007
|
+
const hasRawOrAllScope = scopeEntries.length === 0 || scopeTerms.some((term) => term === 'raw' || term === 'all');
|
|
1008
|
+
if (hasRawOrAllScope)
|
|
1009
|
+
return;
|
|
1010
|
+
for (const token of patternConfig.tokens) {
|
|
1011
|
+
if (typeof token === 'string' && token.startsWith('^#')) {
|
|
1012
|
+
const scopeDisplay = Array.isArray(rule.scope) ? rule.scope.join(', ') : String(rule.scope);
|
|
1013
|
+
warnOnce(`recheck: Rule "${name}": pattern "${token}" starts with '^#' but scope "${scopeDisplay}" matches semantic text without markup — drop the '#' prefix or use scope: raw`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Vale parity: `metric` rules are ALWAYS summary-scoped — readability is a
|
|
1019
|
+
* whole-document score over the document's prose, which is exactly what the
|
|
1020
|
+
* `summary` scope segments carry (see scopes/extractor.ts). Any rule whose
|
|
1021
|
+
* assertions include `metric` gets `scope: 'summary'` forced here, so the
|
|
1022
|
+
* runner hands metric.ts the summary segments and the rule never re-extracts
|
|
1023
|
+
* scopes itself. A config that EXPLICITLY set some other scope gets a
|
|
1024
|
+
* warning that the scope is ignored — a warning, not an error, because the
|
|
1025
|
+
* rule still behaves correctly; ValidationResult has no warning channel
|
|
1026
|
+
* (only errors), so this uses console.warn via `warnOnce`, the
|
|
1027
|
+
* `warnStalePatternPrefix` precedent above. Explicitness comes from
|
|
1028
|
+
* `hasExplicitScope` (captured BEFORE schema validation): AJV `useDefaults`
|
|
1029
|
+
* injects `scope: 'all'` onto every rule that omitted it, so post-schema the
|
|
1030
|
+
* two cases are indistinguishable.
|
|
1031
|
+
*/
|
|
1032
|
+
function normalizeMetricScope(rule, name, hasExplicitScope, warnOnce) {
|
|
1033
|
+
if (!rule.assertions || typeof rule.assertions !== 'object' || !('metric' in rule.assertions)) {
|
|
1034
|
+
return rule.scope;
|
|
1035
|
+
}
|
|
1036
|
+
if (hasExplicitScope) {
|
|
1037
|
+
const entries = Array.isArray(rule.scope) ? rule.scope : [rule.scope];
|
|
1038
|
+
const isSummary = entries.length === 1 && (entries[0] === 'summary' || entries[0] === 'default');
|
|
1039
|
+
if (!isSummary) {
|
|
1040
|
+
const scopeDisplay = Array.isArray(rule.scope) ? rule.scope.join(', ') : String(rule.scope);
|
|
1041
|
+
warnOnce(`recheck: Rule "${name}": metric is always summary-scoped; ignoring configured scope "${scopeDisplay}"`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return 'summary';
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Validates a rule's `scope` field against the full scope vocabulary and
|
|
1048
|
+
* selector syntax (optional `~` negation, `&`-joined terms). AJV only
|
|
1049
|
+
* checks the structural shape (string, or array of strings); this is the
|
|
1050
|
+
* term-level check that gives a helpful message naming the bad term.
|
|
1051
|
+
*/
|
|
1052
|
+
function validateScope(rule, name, errors) {
|
|
1053
|
+
if (rule.scope === undefined)
|
|
1054
|
+
return;
|
|
1055
|
+
const entries = Array.isArray(rule.scope) ? rule.scope : [rule.scope];
|
|
1056
|
+
for (const entry of entries) {
|
|
1057
|
+
if (typeof entry !== 'string')
|
|
1058
|
+
continue; // caught by schema
|
|
1059
|
+
for (const problem of validateScopeSelector(entry)) {
|
|
1060
|
+
errors.push({
|
|
1061
|
+
message: `Rule "${name}": invalid scope — ${problem}`,
|
|
1062
|
+
path: `${name}.scope`,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
// `all`/`raw` as a TERM inside a compound or negated selector expression
|
|
1066
|
+
// (`heading & all`, `~all`, `~code & ~raw`) is the within-entry variant
|
|
1067
|
+
// of the array-mixing mistake rejected below: the conjunction form
|
|
1068
|
+
// compiles to a predicate that can never match (silently reporting
|
|
1069
|
+
// nothing), the negated form to one that matches every segment. The
|
|
1070
|
+
// shared helper (scopes/selector.ts) is also what makes compileSelector
|
|
1071
|
+
// throw on these shapes, so validation and compilation reject exactly
|
|
1072
|
+
// the same inputs.
|
|
1073
|
+
for (const problem of wholeDocumentKeywordProblems(entry)) {
|
|
1074
|
+
errors.push({
|
|
1075
|
+
message: `Rule "${name}": invalid scope — ${problem}`,
|
|
1076
|
+
path: `${name}.scope`,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// `all`/`raw` are whole-document keywords, not segment names — the
|
|
1081
|
+
// extractor never emits segments with those scopes, so combining either
|
|
1082
|
+
// with any other array entry (e.g. `scope: [all, code]`) can only ever
|
|
1083
|
+
// silently match nothing for the `all`/`raw` part. A single-element array
|
|
1084
|
+
// (`scope: ['all']`) is fine — compileSelector normalizes it to the bare
|
|
1085
|
+
// string's whole-document semantics — but a mix is a config mistake that
|
|
1086
|
+
// must fail loudly rather than validate and then report zero findings.
|
|
1087
|
+
if (entries.length > 1) {
|
|
1088
|
+
for (const entry of entries) {
|
|
1089
|
+
if (typeof entry !== 'string')
|
|
1090
|
+
continue; // caught by schema
|
|
1091
|
+
const term = entry.trim();
|
|
1092
|
+
if (term === 'all' || term === 'raw') {
|
|
1093
|
+
errors.push({
|
|
1094
|
+
message: `Rule "${name}": scope "${term}" covers the whole document and cannot be ` +
|
|
1095
|
+
`combined with other scopes — use \`scope: ${term}\` alone`,
|
|
1096
|
+
path: `${name}.scope`,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Per-assertion `%s` message-placeholder caps. `metric` passes four values
|
|
1104
|
+
* (formula, score, min, max); `length` passes three (size, unit, bound --
|
|
1105
|
+
* see rules/scope/length.ts's FALLBACK_MAX/FALLBACK_MIN); every other
|
|
1106
|
+
* assertion passes at most two. A rule's cap is the largest among its
|
|
1107
|
+
* configured assertions, so a message can never declare more slots than its
|
|
1108
|
+
* assertion will ever fill.
|
|
1109
|
+
*/
|
|
1110
|
+
const MESSAGE_PLACEHOLDER_CAPS = {
|
|
1111
|
+
metric: 4,
|
|
1112
|
+
length: 3,
|
|
1113
|
+
};
|
|
1114
|
+
const DEFAULT_MESSAGE_PLACEHOLDER_CAP = 2;
|
|
1115
|
+
function messagePlaceholderCap(rule) {
|
|
1116
|
+
const assertions = rule.assertions;
|
|
1117
|
+
const assertionIds = assertions && typeof assertions === 'object' ? Object.keys(assertions) : [];
|
|
1118
|
+
return assertionIds.reduce((cap, id) => Math.max(cap, MESSAGE_PLACEHOLDER_CAPS[id] ?? DEFAULT_MESSAGE_PLACEHOLDER_CAP), DEFAULT_MESSAGE_PLACEHOLDER_CAP);
|
|
1119
|
+
}
|
|
1120
|
+
function validateSemantics(config, rulesWithExplicitScope = new Set()) {
|
|
75
1121
|
const errors = [];
|
|
76
1122
|
const rules = [];
|
|
1123
|
+
// Dedupes the stale-pattern warning below to once per distinct message
|
|
1124
|
+
// for this whole validate() call — a config with the same stale pattern
|
|
1125
|
+
// shape on more than one rule only warns once per load.
|
|
1126
|
+
const warnedMessages = new Set();
|
|
1127
|
+
const warnOnce = (message) => {
|
|
1128
|
+
if (warnedMessages.has(message))
|
|
1129
|
+
return;
|
|
1130
|
+
warnedMessages.add(message);
|
|
1131
|
+
console.warn(message);
|
|
1132
|
+
};
|
|
77
1133
|
for (const [key, rule] of Object.entries(config)) {
|
|
78
1134
|
try {
|
|
79
1135
|
// Derive name and shortName
|
|
80
1136
|
const name = key;
|
|
81
1137
|
const shortName = key.replace(/^recheck\//, '');
|
|
82
|
-
// Validate message placeholder count
|
|
83
|
-
|
|
84
|
-
|
|
1138
|
+
// Validate message placeholder count against the rule's own
|
|
1139
|
+
// per-assertion cap (see MESSAGE_PLACEHOLDER_CAPS above). `rule.message`
|
|
1140
|
+
// is required by the JSON schema (see schema.ts `required`) so it is
|
|
1141
|
+
// always a string by the time a config passes AJV structural
|
|
1142
|
+
// validation; the `?? ''` only satisfies the now-optional
|
|
1143
|
+
// NormalizedRule/BaseRule type.
|
|
1144
|
+
const placeholderCount = ((rule.message ?? '').match(/%s/g) || []).length;
|
|
1145
|
+
const placeholderCap = messagePlaceholderCap(rule);
|
|
1146
|
+
if (placeholderCount > placeholderCap) {
|
|
85
1147
|
errors.push({
|
|
86
|
-
message: `Rule "${name}": message can have at most
|
|
1148
|
+
message: `Rule "${name}": message can have at most ${placeholderCap} %s placeholders, found ${placeholderCount}`,
|
|
87
1149
|
path: `${name}.message`,
|
|
88
1150
|
});
|
|
89
1151
|
}
|
|
90
1152
|
// Assertions validation
|
|
91
1153
|
validateAssertions(rule, name, errors);
|
|
92
|
-
//
|
|
1154
|
+
// Removed `pattern` options (negate) must fail loudly, not no-op
|
|
1155
|
+
validatePatternOptions(rule, name, errors);
|
|
1156
|
+
validateOccurrenceOptions(rule, name, errors);
|
|
1157
|
+
validateRepetitionOptions(rule, name, errors);
|
|
1158
|
+
validateConsistencyOptions(rule, name, errors);
|
|
1159
|
+
validateConditionalOptions(rule, name, errors);
|
|
1160
|
+
validateCapitalizationOptions(rule, name, errors);
|
|
1161
|
+
validateMetricOptions(rule, name, errors);
|
|
1162
|
+
validateListLengthOptions(rule, name, errors);
|
|
1163
|
+
validateSpellingOptions(rule, name, errors);
|
|
1164
|
+
validateSwapOptions(rule, name, errors);
|
|
1165
|
+
validateLengthOptions(rule, name, errors);
|
|
1166
|
+
// Scope vocabulary/selector-syntax validation
|
|
1167
|
+
validateScope(rule, name, errors);
|
|
1168
|
+
// Stale `^#`-prefixed pattern token vs. non-raw/non-all scope
|
|
1169
|
+
warnStalePatternPrefix(rule, name, warnOnce);
|
|
1170
|
+
// Create normalized rule. `metric` rules are forced to
|
|
1171
|
+
// `scope: summary` (see normalizeMetricScope above).
|
|
93
1172
|
const normalizedRule = {
|
|
94
1173
|
...rule,
|
|
1174
|
+
scope: normalizeMetricScope(rule, name, rulesWithExplicitScope.has(name), warnOnce),
|
|
95
1175
|
name,
|
|
96
1176
|
shortName,
|
|
97
1177
|
};
|
|
@@ -107,23 +1187,99 @@ function validateSemantics(config) {
|
|
|
107
1187
|
return { errors, rules };
|
|
108
1188
|
}
|
|
109
1189
|
/**
|
|
110
|
-
* Full validation pipeline
|
|
1190
|
+
* Full validation pipeline. `options.configDir` (default `process.cwd()`) is
|
|
1191
|
+
* where a relative `markdoc.extend.tagsFile` resolves from -- the directory
|
|
1192
|
+
* containing the config file, so a project's `tagsFile: ./tags.yaml` behaves
|
|
1193
|
+
* the same regardless of the caller's own working directory.
|
|
111
1194
|
*/
|
|
112
|
-
export async function validate(config) {
|
|
113
|
-
|
|
1195
|
+
export async function validate(config, options) {
|
|
1196
|
+
// Resolve `extends` presets before schema validation of rules: the
|
|
1197
|
+
// merged (preset + user) config is what gets schema/semantic-validated,
|
|
1198
|
+
// so patternProperties only ever sees real `<namespace>/<rule>` rule keys
|
|
1199
|
+
// (`recheck/*` and, since the style-guide presets were added, `google/*`,
|
|
1200
|
+
// `microsoft/*`, and other preset-namespaced ids -- see schema.ts).
|
|
1201
|
+
// `extends` itself is schema-legal at the top level (see schema.ts) but
|
|
1202
|
+
// is stripped here — it is not a rule and must not reach rule iteration.
|
|
1203
|
+
// `resolveExtends` only fails to merge the UNRESOLVABLE preset name(s) it
|
|
1204
|
+
// reports in `extendsErrors` — every other preset and all of the user's
|
|
1205
|
+
// own top-level rule keys still land in `resolvedConfig` — so structure
|
|
1206
|
+
// and semantic validation below still run against everything that DID
|
|
1207
|
+
// resolve, instead of being skipped just because one `extends` entry
|
|
1208
|
+
// named an unknown preset. An unknown preset used to short-circuit semantic
|
|
1209
|
+
// validation entirely, hiding e.g. an unknown assertion id elsewhere in the
|
|
1210
|
+
// same config.
|
|
1211
|
+
const hasExtends = config && typeof config === 'object' && 'extends' in config;
|
|
1212
|
+
const { config: resolvedConfig, errors: extendsErrors } = hasExtends
|
|
1213
|
+
? resolveExtends(config)
|
|
1214
|
+
: { config: config, errors: [] };
|
|
1215
|
+
// Which rules carry an EXPLICIT `scope`, recorded before validateStructure
|
|
1216
|
+
// runs: AJV `useDefaults` mutates the config in place, injecting
|
|
1217
|
+
// `scope: 'all'` onto every rule that omitted it, so this is the only
|
|
1218
|
+
// point where "configured" and "defaulted" scopes are distinguishable —
|
|
1219
|
+
// normalizeMetricScope needs the distinction to warn only about scopes a
|
|
1220
|
+
// user actually wrote.
|
|
1221
|
+
const rulesWithExplicitScope = new Set(resolvedConfig && typeof resolvedConfig === 'object'
|
|
1222
|
+
? Object.entries(resolvedConfig)
|
|
1223
|
+
.filter(([key, rule]) => key !== 'extends' &&
|
|
1224
|
+
key !== 'markdoc' &&
|
|
1225
|
+
rule !== null &&
|
|
1226
|
+
typeof rule === 'object' &&
|
|
1227
|
+
'scope' in rule)
|
|
1228
|
+
.map(([key]) => key)
|
|
1229
|
+
: []);
|
|
1230
|
+
const structureErrors = validateStructure(resolvedConfig);
|
|
1231
|
+
// `markdoc`, like `extends`, is an engine-level flag rather than a rule, so
|
|
1232
|
+
// it is read here before being stripped from rule iteration below.
|
|
1233
|
+
// `resolveMarkdocConfig` is defensive about the shape it is handed, so this
|
|
1234
|
+
// is safe to call even when `structureErrors` is about to report the same
|
|
1235
|
+
// value as invalid (e.g. `{ schema: 'bogus' }`).
|
|
1236
|
+
const rawMarkdoc = resolvedConfig?.markdoc;
|
|
1237
|
+
let { enabled: markdocEnabled, schema: markdocSchema } = resolveMarkdocConfig(rawMarkdoc);
|
|
1238
|
+
// The stale-preset warning is independent of structure and semantic
|
|
1239
|
+
// validity, so it runs here rather than after an error-return path below
|
|
1240
|
+
// could short-circuit it. A bare `console.warn` is enough: unlike
|
|
1241
|
+
// `warnStalePatternPrefix`, which runs once per rule and needs the deduping
|
|
1242
|
+
// `warnOnce`, this fires at most once per `validate()` call.
|
|
1243
|
+
warnStaleMarkdocPreset(hasExtends ? config.extends : undefined, markdocEnabled, (message) => console.warn(message));
|
|
114
1244
|
if (structureErrors.length > 0) {
|
|
115
1245
|
return {
|
|
116
1246
|
isValid: false,
|
|
117
|
-
errors: structureErrors,
|
|
1247
|
+
errors: [...extendsErrors, ...structureErrors],
|
|
118
1248
|
rules: [],
|
|
1249
|
+
markdoc: { enabled: markdocEnabled, schema: markdocSchema },
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
// Only reached once structural validation passed, so `rawMarkdoc`'s shape
|
|
1253
|
+
// (including `extend.tagsFile`, when present) is already known-good --
|
|
1254
|
+
// safe to resolve and read the file now. `loadMarkdocTagsFile` itself
|
|
1255
|
+
// never touches the filesystem when there's no `tagsFile` to load.
|
|
1256
|
+
const { fileTags, errors: tagsFileErrors } = await loadMarkdocTagsFile(rawMarkdoc, options?.configDir ?? process.cwd());
|
|
1257
|
+
if (tagsFileErrors.length > 0) {
|
|
1258
|
+
// Degrade exactly like any other invalid markdoc shape: a broken
|
|
1259
|
+
// tagsFile leaves no trustworthy schema for markdoc rules to run
|
|
1260
|
+
// against for this call.
|
|
1261
|
+
markdocEnabled = false;
|
|
1262
|
+
markdocSchema = null;
|
|
1263
|
+
}
|
|
1264
|
+
else if (fileTags) {
|
|
1265
|
+
const resolvedExtend = {
|
|
1266
|
+
fileTags,
|
|
1267
|
+
tags: rawMarkdoc && typeof rawMarkdoc === 'object' ? rawMarkdoc.extend?.tags : undefined,
|
|
119
1268
|
};
|
|
1269
|
+
({ enabled: markdocEnabled, schema: markdocSchema } = resolveMarkdocConfig(rawMarkdoc, resolvedExtend));
|
|
120
1270
|
}
|
|
121
|
-
// Then validate semantics
|
|
122
|
-
|
|
1271
|
+
// Then validate semantics of everything that resolved successfully.
|
|
1272
|
+
// `markdoc` is stripped first, exactly as `extends` is stripped in
|
|
1273
|
+
// resolveExtends, so rule iteration in validateSemantics never sees it.
|
|
1274
|
+
const { markdoc: _markdoc, ...rulesOnlyConfig } = resolvedConfig;
|
|
1275
|
+
const { errors: semanticErrors, rules } = validateSemantics(rulesOnlyConfig, rulesWithExplicitScope);
|
|
1276
|
+
const peerErrors = await checkSpellingPeerDependencies(rules);
|
|
1277
|
+
const errors = [...extendsErrors, ...semanticErrors, ...peerErrors, ...tagsFileErrors];
|
|
123
1278
|
return {
|
|
124
|
-
isValid:
|
|
125
|
-
errors
|
|
1279
|
+
isValid: errors.length === 0,
|
|
1280
|
+
errors,
|
|
126
1281
|
rules,
|
|
1282
|
+
markdoc: { enabled: markdocEnabled, schema: markdocSchema },
|
|
127
1283
|
};
|
|
128
1284
|
}
|
|
129
1285
|
//# sourceMappingURL=validate.js.map
|