diff --git a/.github/workflows/js-lint.yml b/.github/workflows/js-lint.yml index 3243361..2f1b424 100644 --- a/.github/workflows/js-lint.yml +++ b/.github/workflows/js-lint.yml @@ -8,7 +8,7 @@ name: JS Lint on: push: paths: - - '.markdownlint-rules/**/*.js' + - 'markdownlint-rules/**/*.js' - 'eslint.config.cjs' - 'package.json' - 'package-lock.json' diff --git a/.github/workflows/lint-readmes.yml b/.github/workflows/lint-readmes.yml index 014b319..0377579 100644 --- a/.github/workflows/lint-readmes.yml +++ b/.github/workflows/lint-readmes.yml @@ -11,14 +11,14 @@ on: - '**/README.md' - '.markdownlint-cli2.jsonc' - '.markdownlint.yml' - - '.markdownlint-rules/**' + - 'markdownlint-rules/**' - 'Makefile' pull_request: paths: - '**/README.md' - '.markdownlint-cli2.jsonc' - '.markdownlint.yml' - - '.markdownlint-rules/**' + - 'markdownlint-rules/**' - 'Makefile' jobs: diff --git a/.github/workflows/markdownlint-tests.yml b/.github/workflows/markdownlint-tests.yml index 7e7b78e..c59a32b 100644 --- a/.github/workflows/markdownlint-tests.yml +++ b/.github/workflows/markdownlint-tests.yml @@ -8,13 +8,13 @@ on: paths: - '.markdownlint-cli2.jsonc' - '.markdownlint.yml' - - '.markdownlint-rules/**' + - 'markdownlint-rules/**' - 'md_test_files/**' pull_request: paths: - '.markdownlint-cli2.jsonc' - '.markdownlint.yml' - - '.markdownlint-rules/**' + - 'markdownlint-rules/**' - 'md_test_files/**' jobs: diff --git a/.github/workflows/rule-unit-tests.yml b/.github/workflows/rule-unit-tests.yml index 35fa349..5232f11 100644 --- a/.github/workflows/rule-unit-tests.yml +++ b/.github/workflows/rule-unit-tests.yml @@ -1,12 +1,12 @@ name: Rule unit tests -# Unit tests for .markdownlint-rules/*.js (Node test runner) with coverage (fails if any file < 90%). +# Unit tests for markdownlint-rules/*.js (Node test runner) with coverage (fails if any file < 90%). # NOTE: Keep in sync with the 'test-rules' and 'test-rules-coverage' targets in the root Makefile. on: push: paths: - - '.markdownlint-rules/**/*.js' + - 'markdownlint-rules/**/*.js' - 'test/markdownlint-rules/**' - 'package.json' - 'package-lock.json' diff --git a/.gitignore b/.gitignore index 5e3c5ce..2daf852 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ coverage.* # Ignore Python cache files **/__pycache__/ .pytest_cache/ + +# Ignore test files +md_test_files/negative_document_length.md diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 4541cb4..299e45c 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -10,11 +10,12 @@ "**/*.plan.md" ], "customRules": [ - ".markdownlint-rules/no-heading-like-lines.js", - ".markdownlint-rules/allow-custom-anchors.js", - ".markdownlint-rules/no-duplicate-headings-normalized.js", - ".markdownlint-rules/heading-numbering.js", - ".markdownlint-rules/heading-title-case.js", - ".markdownlint-rules/ascii-only.js" + "markdownlint-rules/no-heading-like-lines.js", + "markdownlint-rules/allow-custom-anchors.js", + "markdownlint-rules/no-duplicate-headings-normalized.js", + "markdownlint-rules/heading-numbering.js", + "markdownlint-rules/heading-title-case.js", + "markdownlint-rules/ascii-only.js", + "markdownlint-rules/document-length.js" ] } diff --git a/.markdownlint.yml b/.markdownlint.yml index 2436045..232baa0 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,254 +1,61 @@ --- -# Example markdownlint YAML configuration with all properties set to their default value +# markdownlint configuration: default built-in rules plus custom rule overrides -# Default state for all rules default: true -# Path to configuration file to extend -# extends: null - -# MD001/heading-increment/header-increment - Heading levels should only increment by one level at a time -MD001: true - -# MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading -MD002: - # Heading level - level: 1 - -# MD003/heading-style/header-style - Heading style -MD003: - # Heading style - style: "consistent" - -# MD004/ul-style - Unordered list style -MD004: - # List style - style: "consistent" - -# MD005/list-indent - Inconsistent indentation for list items at the same level -MD005: true - -# MD006/ul-start-left - Consider starting bulleted lists at the beginning of the line -MD006: true - -# MD007/ul-indent - Unordered list indentation -MD007: - # Spaces for indent - indent: 2 - # Whether to indent the first level of the list - start_indented: false - -# MD009/no-trailing-spaces - Trailing spaces -MD009: - # Spaces for line break - br_spaces: 2 - # Allow spaces for empty lines in list items - list_item_empty_lines: false - # Include unnecessary breaks - strict: false - -# MD010/no-hard-tabs - Hard tabs -MD010: - # Include code blocks - code_blocks: true - # Number of spaces for each hard tab - # spaces_per_tab: 1 - -# MD011/no-reversed-links - Reversed link syntax -MD011: true - -# MD012/no-multiple-blanks - Multiple consecutive blank lines -MD012: - # Consecutive blank lines - maximum: 1 - -# MD013/line-length - Line length +# MD013/line-length - line length limits (non-default) MD013: - # Number of characters line_length: 500 - # Number of characters for headings heading_line_length: 500 - # Number of characters for code blocks code_block_line_length: 500 - # Include code blocks code_blocks: true - # Include tables tables: true - # Include headings headings: true - # Strict length checking strict: false - # Stern length checking stern: false -# MD014/commands-show-output - Dollar signs used before commands without showing output -MD014: true - -# MD018/no-missing-space-atx - No space after hash on atx style heading -MD018: true - -# MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading -MD019: true - -# MD020/no-missing-space-closed-atx - No space inside hashes on closed atx style heading -MD020: true - -# MD021/no-multiple-space-closed-atx - Multiple spaces inside hashes on closed atx style heading -MD021: true - -# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines -MD022: - # Blank lines above heading - lines_above: 1 - # Blank lines below heading - lines_below: 1 - -# MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line -MD023: true - -# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content -MD024: - # Only check sibling headings - siblings_only: false - -# MD025/single-title/single-h1 - Multiple top-level headings in the same document -MD025: - # Heading level - level: 1 - # RegExp for matching title in front matter - front_matter_title: "^\\s*title\\s*[:=]" - -# MD026/no-trailing-punctuation - Trailing punctuation in heading -MD026: - # Punctuation characters - punctuation: ".,;:!。,;:!" - -# MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol -MD027: true - -# MD028/no-blanks-blockquote - Blank line inside blockquote -MD028: true - -# MD029/ol-prefix - Ordered list item prefix -MD029: - # List style - style: "one_or_ordered" - -# MD030/list-marker-space - Spaces after list markers -MD030: - # Spaces for single-line unordered list items - ul_single: 1 - # Spaces for single-line ordered list items - ol_single: 1 - # Spaces for multi-line unordered list items - ul_multi: 1 - # Spaces for multi-line ordered list items - ol_multi: 1 - -# MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines -MD031: - # Include list items - list_items: true - -# MD032/blanks-around-lists - Lists should be surrounded by blank lines -MD032: true - -# MD033/no-inline-html - Inline HTML +# MD033/no-inline-html - allow anchor elements for custom anchors MD033: - # Allowed elements allowed_elements: ["a"] -# MD034/no-bare-urls - Bare URL used -MD034: true - -# MD035/hr-style - Horizontal rule style -MD035: - # Horizontal rule style - style: "consistent" - -# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading -MD036: - # Punctuation characters - punctuation: ".,;:!?。,;:!?" - -# MD037/no-space-in-emphasis - Spaces inside emphasis markers -MD037: true - -# MD038/no-space-in-code - Spaces inside code span elements -MD038: true - -# MD039/no-space-in-links - Spaces inside link text -MD039: true - -# MD040/fenced-code-language - Fenced code blocks should have a language specified -MD040: true - -# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading -MD041: - # Heading level - level: 1 - # RegExp for matching title in front matter - front_matter_title: "^\\s*title\\s*[:=]" - -# MD042/no-empty-links - No empty links -MD042: true - -# MD043/required-headings/required-headers - Required heading structure -# MD043: - # List of headings - # headings: ["*"] - # List of headings - # headers: [] - -# MD044/proper-names - Proper names should have the correct capitalization -MD044: - # List of proper names - names: [] - # Include code blocks - code_blocks: true - -# MD045/no-alt-text - Images should have alternate text (alt text) -MD045: true - -# MD046/code-block-style - Code block style -MD046: - # Block style - style: "consistent" - -# MD047/single-trailing-newline - Files should end with a single newline character -MD047: true - -# MD048/code-fence-style - Code fence style -MD048: - # Code fence style - style: "consistent" - -# MD060/table-column-style - Table column style -MD060: - # Table column style - style: "aligned" - -# ascii-only: no built-in path/emoji defaults; set paths and emoji in config -# - allowedUnicode: optional; single chars allowed in all files (no default) -# - unicodeReplacements: optional; if omitted, rule uses built-in defaults (arrows, quotes, <=>=*) +# ascii-only: disallow non-ASCII except in configured paths; inline code (backticks) always stripped +# No built-in path/emoji defaults. Options: +# - allowedPathPatternsUnicode: glob list; files where any non-ASCII is allowed +# - allowedPathPatternsEmoji: glob list; files where only allowedEmoji chars are allowed +# - allowedEmoji: emoji/chars allowed in paths matching allowedPathPatternsEmoji (multi-codepoint OK) +# - allowedUnicode: chars allowed in all files; extends default set (é, ï, ñ, ç, etc.) unless replace below +# - allowedUnicodeReplaceDefault: true = use only allowedUnicode list (no default set) +# - allowUnicodeInCodeBlocks: true (default) = skip fenced blocks; false = check them for unicode +# - disallowUnicodeInCodeBlockTypes: when allowUnicodeInCodeBlocks false, only these block types checked +# (block type = first word after opening fence, e.g. "text" from ```text); empty = check all blocks +# - unicodeReplacements: object or [char, replacement] array; built-in defaults (arrows, quotes, <=>=*) if omitted ascii-only: allowedPathPatternsUnicode: # - "**/README.md" allowedPathPatternsEmoji: - - ".markdownlint-rules/README.md" + - "markdownlint-rules/README.md" + # allowedUnicode: + # - "ń" # additions on top of default (fixtures e.g. positive.md) + # allowedUnicodeReplaceDefault: false # true = use only allowedUnicode list, no default set allowedEmoji: - "✅" - "❌" - "📊" - "⚠️" -# heading-title-case: enforce title case for headings; words in backticks ignored; configurable lowercase words +# document-length: disallow documents longer than maximum lines (default 1500) +# - maximum: positive integer; default 1500 +document-length: + maximum: 1500 + +# heading-title-case: AP-style headline capitalization; words in backticks ignored # heading-title-case: -# lowercaseWords: ["a", "an", "the", "vs", ...] # optional override; default list used if omitted +# lowercaseWords: ["through", ...] # optional; extends default list (add words) +# lowercaseWordsReplaceDefault: true # optional; true = use only lowercaseWords list, no default set -# allow-custom-anchors: anchor id patterns and optional placement per pattern -# - allowedIdPatterns: array of regex strings or { pattern, placement? }; placement is per anchor regex -# - strictPlacement: if true (default), enforce placement when placement is set for that pattern +# allow-custom-anchors: require anchor ids to match patterns; optional placement (line/heading) per pattern +# - allowedIdPatterns: list of regex strings or { pattern, placement? }; placement applies per pattern +# - strictPlacement: true (default) = enforce placement when a pattern has placement set allow-custom-anchors: allowedIdPatterns: - pattern: "^spec-[a-z0-9-]+$" diff --git a/.vscode/settings.json b/.vscode/settings.json index b759ec9..96ec4cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,11 @@ { "markdownlint.customRules": [ - "./.markdownlint-rules/allow-custom-anchors.js", - "./.markdownlint-rules/ascii-only.js", - "./.markdownlint-rules/heading-numbering.js", - "./.markdownlint-rules/heading-title-case.js", - "./.markdownlint-rules/no-duplicate-headings-normalized.js", - "./.markdownlint-rules/no-heading-like-lines.js" + "./markdownlint-rules/allow-custom-anchors.js", + "./markdownlint-rules/ascii-only.js", + "./markdownlint-rules/heading-numbering.js", + "./markdownlint-rules/heading-title-case.js", + "./markdownlint-rules/no-duplicate-headings-normalized.js", + "./markdownlint-rules/no-heading-like-lines.js", + "./markdownlint-rules/document-length.js" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aca4930..c1e9ea1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi ### Lint Rule JavaScript (`make lint-js`) -- Lints `.markdownlint-rules/*.js` with ESLint (recommended + complexity/max-lines). +- Lints `markdownlint-rules/*.js` with ESLint (recommended + complexity/max-lines). - Same as the [JS Lint](.github/workflows/js-lint.yml) workflow. ```bash @@ -30,7 +30,7 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi Optional: limit to specific paths: ```bash - make lint-js PATHS=".markdownlint-rules/heading-title-case.js,.markdownlint-rules/utils.js" + make lint-js PATHS="markdownlint-rules/heading-title-case.js,markdownlint-rules/utils.js" ``` ### Markdownlint Tests (`make test-markdownlint`) @@ -58,7 +58,7 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi - **Security tests** (`test/markdownlint-rules/security.test.js`): assert that invalid or malformed regex in rule config does not throw (defensive parsing). A skipped test documents ReDoS risk when rules use user-controlled regex; enable it after adding mitigation (e.g. safe-regex or timeout). -- **Security lint**: `eslint-plugin-security` is enabled for `.markdownlint-rules` (via `make lint-js`) and forbids `eval`, `new Function`, `child_process`, non-literal `require`/`fs`/`Buffer`, and similar. +- **Security lint**: `eslint-plugin-security` is enabled for `markdownlint-rules` (via `make lint-js`) and forbids `eval`, `new Function`, `child_process`, non-literal `require`/`fs`/`Buffer`, and similar. These execute as part of `make test-rules` and `make lint-js` respectively. `make ci` includes these in all the checks it executes. @@ -95,8 +95,8 @@ Or run individual targets: `make lint-js && make test-rules && make test-markdow ## Custom Rules -- Rule code lives in [.markdownlint-rules/](.markdownlint-rules/). Do not register `utils.js` as a rule; it is a shared helper. -- Config for custom rules is in [.markdownlint.yml](.markdownlint.yml). Rule docs and reuse instructions are in [.markdownlint-rules/README.md](.markdownlint-rules/README.md). +- Rule code lives in [markdownlint-rules/](markdownlint-rules/). Do not register `utils.js` as a rule; it is a shared helper. +- Config for custom rules is in [.markdownlint.yml](.markdownlint.yml). Rule docs and reuse instructions are in [markdownlint-rules/README.md](markdownlint-rules/README.md). ## Sync Notes diff --git a/Makefile b/Makefile index be59f66..8ed6fd2 100644 --- a/Makefile +++ b/Makefile @@ -20,16 +20,16 @@ lint-readmes: MDL="npx markdownlint-cli2"; \ fi; \ echo "Linting READMEs..."; \ - $$MDL README.md **/README.md .markdownlint-rules/README.md CONTRIBUTING.md + $$MDL README.md **/README.md markdownlint-rules/README.md CONTRIBUTING.md # JavaScript linting - performs same checks as GitHub Actions workflow # NOTE: This target must be kept in sync with .github/workflows/js-lint.yml. # When adding or modifying JS linting, update both this Makefile and # the workflow file to ensure local 'make lint-js' matches CI behavior. # Requires: Node.js and npm; run 'npm install' (or npm ci) once for node_modules. -# Lints .markdownlint-rules/*.js with ESLint (recommended + complexity/max-lines + eslint-plugin-security). +# Lints markdownlint-rules/*.js with ESLint (recommended + complexity/max-lines + eslint-plugin-security). # Usage: make lint-js [PATHS="path1,path2"] -# - PATHS: Comma-separated list of files/directories (default: .markdownlint-rules) +# - PATHS: Comma-separated list of files/directories (default: markdownlint-rules) lint-js: @command -v node >/dev/null 2>&1 || { \ echo "Error: node not found. Install Node.js to run JS linting."; \ @@ -46,7 +46,7 @@ lint-js: if [ -n "$(PATHS)" ]; then \ LINT_PATHS=$$(echo "$(PATHS)" | tr ',' ' '); \ else \ - LINT_PATHS=".markdownlint-rules"; \ + LINT_PATHS="markdownlint-rules"; \ fi; \ echo "Running eslint on $$LINT_PATHS..."; \ $$ESLINT $$LINT_PATHS --ext .js; \ @@ -120,7 +120,7 @@ lint-python: echo ""; echo "Lint exit codes: flake8=$$FLAKE8_RESULT pylint=$$PYLINT_RESULT xenon=$$XENON_RESULT radon_mi=$$MI_RESULT bandit=$$BANDIT_RESULT"; \ [ $$FLAKE8_RESULT -ne 0 ] || [ $$PYLINT_RESULT -ne 0 ] || [ $$XENON_RESULT -ne 0 ] || [ $$MI_RESULT -ne 0 ] || [ $$BANDIT_RESULT -ne 0 ] && exit 1; exit 0 -# Unit tests for .markdownlint-rules/*.js - same as .github/workflows/rule-unit-tests.yml +# Unit tests for markdownlint-rules/*.js - same as .github/workflows/rule-unit-tests.yml # NOTE: Keep in sync with that workflow. Requires: Node.js, npm; run 'npm install' first. test-rules: @command -v node >/dev/null 2>&1 || { \ @@ -129,7 +129,7 @@ test-rules: } @node --test test/markdownlint-rules/*.test.js -# Unit test coverage for .markdownlint-rules/*.js (fails if any file < 90% lines/statements). +# Unit test coverage for markdownlint-rules/*.js (fails if any file < 90% lines/statements). # Requires: Node.js, npm; run 'npm install' first. test-rules-coverage: @command -v node >/dev/null 2>&1 || { \ diff --git a/README.md b/README.md index f7e2183..3f801d6 100644 --- a/README.md +++ b/README.md @@ -12,26 +12,29 @@ Lint and docs-as-code tooling: custom [markdownlint](https://github.com/DavidAns ## Features -- **Custom markdownlint rules** in [.markdownlint-rules/](.markdownlint-rules/README.md) (intended to be **copied directly** into whatever repo wishes to use them; no need to depend on this repo): - - [allow-custom-anchors.js](.markdownlint-rules/allow-custom-anchors.js) - Custom anchor validation. +- **Custom markdownlint rules** in [markdownlint-rules/](markdownlint-rules/README.md) (intended to be **copied directly** into whatever repo wishes to use them; no need to depend on this repo): + - [allow-custom-anchors.js](markdownlint-rules/allow-custom-anchors.js) - Custom anchor validation. - Only allow `` whose ids match configured regex patterns; optional placement (heading match, line match, require-after, max per section). - Use when: enforcing stable fragment links (e.g. spec/algo docs) and consistent anchor placement. - - [ascii-only.js](.markdownlint-rules/ascii-only.js) - ASCII-only with path/emoji allowlists. + - [ascii-only.js](markdownlint-rules/ascii-only.js) - ASCII-only with path/emoji allowlists. - Disallow non-ASCII except in paths matching globs; allow Unicode or emoji-only in specific paths; optional replacement suggestions in errors. - Use when: keeping most docs ASCII while allowing Unicode/emoji only in chosen files (e.g. i18n or release notes). - - [heading-numbering.js](.markdownlint-rules/heading-numbering.js) - heading numbering. + - [heading-numbering.js](markdownlint-rules/heading-numbering.js) - heading numbering. - Enforce segment count by numbering root, sequential numbering per section, and consistent period style (e.g. `1. Title` vs `1 Title`). - Use when: docs use numbered headings (e.g. `### 1.2.3 Title`) and you want structure and style consistent. - - [heading-title-case.js](.markdownlint-rules/heading-title-case.js) - heading title case. + - [heading-title-case.js](markdownlint-rules/heading-title-case.js) - heading title case. - Enforce title case for headings; words in backticks ignored; configurable lowercase words (e.g. vs, and, the). - Use when: you want consistent capitalization of headings (first/last and major words capped; small words lowercase in the middle). - - [no-duplicate-headings-normalized.js](.markdownlint-rules/no-duplicate-headings-normalized.js) - duplicate-heading checks. + - [no-duplicate-headings-normalized.js](markdownlint-rules/no-duplicate-headings-normalized.js) - duplicate-heading checks. - Disallow duplicate heading titles after stripping numeric prefixes and normalizing case/whitespace; first occurrence is reference. - Use when: avoiding duplicate section titles that differ only by number or formatting. - - [no-heading-like-lines.js](.markdownlint-rules/no-heading-like-lines.js) - no heading-like lines. + - [no-heading-like-lines.js](markdownlint-rules/no-heading-like-lines.js) - no heading-like lines. - Report lines that look like headings but are not (e.g. `**Text:**`, `1. **Text**`); prompt use of real `#` headings. - Use when: ensuring real Markdown headings instead of bold/italic that look like headings. - - [utils.js](.markdownlint-rules/utils.js) - shared utilities + - [document-length.js](markdownlint-rules/document-length.js) - maximum document length. + - Disallow documents longer than a configured number of lines (default 1500); reports on line 1 when over the limit. + - Use when: keeping individual docs under a line cap to encourage splitting. + - [utils.js](markdownlint-rules/utils.js) - shared utilities - Heading/content helpers and path/glob matching; used by other rules. Do not register as a rule in markdownlint. - Use when: copying the rule files; required by several of the rules above. @@ -43,7 +46,7 @@ Lint and docs-as-code tooling: custom [markdownlint](https://github.com/DavidAns - **Python unit tests**: `unittest` tests for [test-scripts/](test-scripts/README.md) in `test-scripts/test_*.py`; run with `make test-python`. - **Python linting** for repo tooling scripts: `make lint-python` (flake8, pylint, xenon/radon, vulture, bandit). -See **[.markdownlint-rules/README.md](.markdownlint-rules/README.md)** for rule docs and configuration. +See **[markdownlint-rules/README.md](markdownlint-rules/README.md)** for rule docs and configuration. ## Requirements @@ -51,7 +54,7 @@ See **[.markdownlint-rules/README.md](.markdownlint-rules/README.md)** for rule - Python 3 (for repo test scripts and `make lint-python`) - [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) and config (`.markdownlint-cli2.jsonc`, `.markdownlint.yml`) when using the custom rules in another repo -## Install +## Install Testing Dependencies ```bash npm install @@ -65,7 +68,7 @@ npm install make lint-js ``` - Optional: limit paths: `make lint-js PATHS=".markdownlint-rules/allow-custom-anchors.js,.markdownlint-rules/utils.js"` + Optional: limit paths: `make lint-js PATHS="markdownlint-rules/allow-custom-anchors.js,markdownlint-rules/utils.js"` - **Run markdownlint fixture tests**: @@ -104,8 +107,8 @@ npm install make lint-python ``` -- **Use the custom rules**: Copy the `.markdownlint-rules/*.js` files (and optionally the rule [README](.markdownlint-rules/README.md) and config) into your docs repo, then point markdownlint-cli2 at that directory and your `.markdownlint.yml` / `.markdownlint-cli2.jsonc`. - For VS Code (and forks like Cursor), see [.markdownlint-rules/README.md](.markdownlint-rules/README.md#using-in-vs-code-and-its-forks). +- **Use the custom rules**: Copy the `markdownlint-rules/*.js` files (and optionally the rule [README](markdownlint-rules/README.md) and config) into your docs repo, then point markdownlint-cli2 at that directory and your `.markdownlint.yml` / `.markdownlint-cli2.jsonc`. + For VS Code (and forks like Cursor), see [markdownlint-rules/README.md](markdownlint-rules/README.md#using-in-vs-code-and-its-forks). ## Repository Layout @@ -117,13 +120,14 @@ npm install - [rule-unit-tests.yml](.github/workflows/rule-unit-tests.yml) - [python-lint.yml](.github/workflows/python-lint.yml) - [python-tests.yml](.github/workflows/python-tests.yml) -- **`.vscode/settings.json`** - Editor settings so the markdownlint extension uses this repo's custom rules in VS Code and compatible editors (see [.markdownlint-rules/README.md](.markdownlint-rules/README.md#using-in-vs-code-and-its-forks)). +- **`.vscode/settings.json`** - Editor settings so the markdownlint extension uses this repo's custom rules in VS Code and compatible editors (see [markdownlint-rules/README.md](markdownlint-rules/README.md#using-in-vs-code-and-its-forks)). - **`.markdownlint-cli2.jsonc`** - markdownlint-cli2 config: custom rule paths, extends `.markdownlint.yml`, ignores. -- **`.markdownlint-rules/`** - Custom rule modules (`*.js`) and [README](.markdownlint-rules/README.md). Copy into other repos; do not register `utils.js`. - **`.markdownlint.yml`** - markdownlint and custom-rule options (e.g. ascii-only, allow-custom-anchors). - **`CONTRIBUTING.md`** - How to contribute and run tests/linting locally. -- **`eslint.config.cjs`** - ESLint config for `.markdownlint-rules/*.js`. +- **`eslint.config.cjs`** - ESLint config for `markdownlint-rules/*.js`. - **`Makefile`** - Local targets (kept in sync with the workflows in `.github/workflows/`). +- **`markdownlint-rules/`** - Custom rule modules (`*.js`) and [README](markdownlint-rules/README.md). + Copy into other repos as `.markdownlint-rules`; do not register `utils.js`. - **`md_test_files/`** - Test fixtures: `positive.md` (must pass), `negative_*.md` (must fail). See [md_test_files/README.md](md_test_files/README.md). - **`package.json`** - npm dependencies (ESLint, markdownlint-cli2, etc.). diff --git a/eslint.config.cjs b/eslint.config.cjs index 11d686d..5d6f272 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -8,7 +8,7 @@ const eslintPluginSecurity = require("eslint-plugin-security"); module.exports = defineConfig([ { name: "docs-as-code-tools/markdownlint-rules", - files: [".markdownlint-rules/**/*.js"], + files: ["markdownlint-rules/**/*.js"], plugins: { js, security: eslintPluginSecurity }, extends: ["js/recommended"], languageOptions: { diff --git a/.markdownlint-rules/README.md b/markdownlint-rules/README.md similarity index 76% rename from .markdownlint-rules/README.md rename to markdownlint-rules/README.md index ed20dc5..82a9358 100644 --- a/.markdownlint-rules/README.md +++ b/markdownlint-rules/README.md @@ -131,6 +131,23 @@ Order of entries matters: the first pattern that matches the anchor id is used. **Behavior:** Reports lines that look like headings but are not (e.g. `**Text:**`, `**Text**:`, `1. **Text**`, and italic variants). Prompts use of real `#` headings instead. +### `document-length` + +**File:** `document-length.js` + +**Description:** Disallow documents longer than a configured number of lines. + +**Configuration:** In `.markdownlint.yml` (or `.markdownlint.json`) under `document-length`: + +```yaml +document-length: + maximum: 1500 # optional; default 1500 +``` + +- **`maximum`** (number, default `1500`): Maximum allowed line count. Must be a positive integer. + +**Behavior:** When the file has more than `maximum` lines, the rule reports a single error on line 1. The message includes the actual line count and the maximum and suggests splitting into smaller files. + ### `ascii-only` **File:** `ascii-only.js` @@ -139,23 +156,94 @@ Order of entries matters: the first pattern that matches the anchor id is used. **Configuration:** In `.markdownlint.yml` (or `.markdownlint.json`) under `ascii-only`: +Example: minimal (default letters plus path/emoji) + +```yaml +ascii-only: + allowedPathPatternsUnicode: + - "**/README.md" # any non-ASCII allowed in READMEs + allowedPathPatternsEmoji: + - "docs/**" # only allowedEmoji in docs/ + allowedEmoji: + - "✅" + - "⚠️" +``` + +Example: extend default allowed characters (e.g. degree sign, or `ń` for Polish) + +```yaml +ascii-only: + allowedUnicode: + - "°" + - "ń" # merged with default (é, ï, ñ, ç, etc.) + # allowedUnicodeReplaceDefault: false # default; true = use only the list above +``` + +Example: override default (strict allowlist only) + +```yaml +ascii-only: + allowedUnicode: + - "°" + - "→" # only these two allowed in prose + allowedUnicodeReplaceDefault: true +``` + +Example: check unicode inside code blocks (e.g. only in `text` and `bash` blocks) + +```yaml +ascii-only: + allowUnicodeInCodeBlocks: false + disallowUnicodeInCodeBlockTypes: + - "text" + - "bash" # ```text and ```bash checked; ```go skipped +``` + +Example: custom replacement suggestions in error messages + +```yaml +ascii-only: + unicodeReplacements: # object form + "→": "->" + "←": "<-" + "°": " deg" + # or array form: + # unicodeReplacements: [["→", "->"], ["←", "<-"]] +``` + +Example: full configuration combining options + ```yaml ascii-only: allowedPathPatternsUnicode: - "**/README.md" + - "**/CHANGELOG*.md" allowedPathPatternsEmoji: - "docs/**" allowedEmoji: - "✅" + - "❌" - "⚠️" allowedUnicode: - - "°" # optional: allow in all files + - "°" + - "ń" + allowUnicodeInCodeBlocks: true # default; set false to check fenced blocks + # disallowUnicodeInCodeBlockTypes: ["text", "bash"] # when allowUnicodeInCodeBlocks false + unicodeReplacements: + "→": "->" + "—": "--" ``` - **`allowedPathPatternsUnicode`** (list of strings, default none): Glob patterns for files where any non-ASCII is allowed. - **`allowedPathPatternsEmoji`** (list of strings, default none): Glob patterns for files where only `allowedEmoji` characters are allowed. - **`allowedEmoji`** (list of strings, default none): Emoji (or other chars) allowed in paths matching `allowedPathPatternsEmoji`; each entry may be multi-codepoint (e.g. ⚠️); all code points are allowed. -- **`allowedUnicode`** (list of single-character strings, default none): Optional. Characters allowed in all files (global allowlist). +- **`allowedUnicode`** (list of single-character strings, optional): Characters allowed in all files (global allowlist). By default these **extend** the built-in set of common non-English letters (e.g. é, ï, è, ñ, ç). Set **`allowedUnicodeReplaceDefault: true`** to **override** and use only your list (no default set). +- **`allowedUnicodeReplaceDefault`** (boolean, default false): When true, only `allowedUnicode` is used (no built-in default set). +- **`allowUnicodeInCodeBlocks`** (boolean, default true): When true, lines inside fenced code blocks are not checked. + When false, code blocks are checked (or only those in `disallowUnicodeInCodeBlockTypes` if that list is non-empty). +- **`disallowUnicodeInCodeBlockTypes`** (list of strings, default empty): + When `allowUnicodeInCodeBlocks` is false, only fenced blocks whose info string (e.g. `text`, `bash`) is in this list are checked; block type is the first word after the opening fence. + When empty, all code blocks are checked. - **`unicodeReplacements`** (object or array of [char, replacement], default built-in): Map of single Unicode character to suggested ASCII replacement in error messages. When omitted, rule uses built-in defaults (arrows, quotes, <=, >=, \*). Glob matching supports `**` (any path) and `*` (within a segment). @@ -167,10 +255,10 @@ Relative patterns (no leading `/` or `*`) match both path-prefix (e.g. `dev_docs - No built-in path or emoji defaults; configure `allowedPathPatternsUnicode`, `allowedPathPatternsEmoji`, and `allowedEmoji` as needed. - If the file path matches `allowedPathPatternsUnicode`, any non-ASCII is allowed in that file. - If the file path matches `allowedPathPatternsEmoji`, only characters in `allowedEmoji` (and Unicode variation selectors U+FE00-U+FE0F) are allowed; other non-ASCII is reported per occurrence. -- Characters in `allowedUnicode` (when configured) are allowed in all files. +- Characters allowed in all files: the default set (e.g. é, ï, ñ, ç) plus `allowedUnicode` when **extend** (default), or only `allowedUnicode` when `allowedUnicodeReplaceDefault: true`. - Non-ASCII is detected by code-point iteration (surrogate pairs treated as one character) and compared after NFC normalization. - **One error per disallowed character:** each violation highlights only that character (range) on the line. The detail names the character, its code point (e.g. U+2192), and the suggested replacement when present in `unicodeReplacements`. -- Inline code (backticks) is stripped before scanning. +- Inline code (backticks) is stripped before scanning. Fenced code blocks are skipped by default; set `allowUnicodeInCodeBlocks: false` to check them, and optionally `disallowUnicodeInCodeBlockTypes` to restrict which block types (e.g. `text`, `bash`) are checked. ### `heading-title-case` @@ -183,17 +271,17 @@ Relative patterns (no leading `/` or `*`) match both path-prefix (e.g. `dev_docs ```yaml heading-title-case: - lowercaseWords: # optional; default list if omitted - - "a" - - "an" - - "the" - - "vs" - - "and" - - "or" + lowercaseWords: # optional; extends default list (add words) + - "through" + # lowercaseWordsReplaceDefault: true # optional; true = use only lowercaseWords list, no default ``` - **`lowercaseWords`** (array of strings, optional): Words that must be lowercase in the middle of a heading. - If omitted, a default list aligned with AP headline style is used (articles, coordinating conjunctions, short prepositions, and short verb/pronoun): + By default these **extend** the built-in list (articles, conjunctions, short prepositions, etc.). + Set **`lowercaseWordsReplaceDefault: true`** to **override** and use only your list. +- **`lowercaseWordsReplaceDefault`** (boolean, default false): When true, only `lowercaseWords` is used (no built-in default list). + +Built-in default list (when not replaced): ```text a, an, the, diff --git a/.markdownlint-rules/allow-custom-anchors.js b/markdownlint-rules/allow-custom-anchors.js similarity index 100% rename from .markdownlint-rules/allow-custom-anchors.js rename to markdownlint-rules/allow-custom-anchors.js diff --git a/.markdownlint-rules/ascii-only.js b/markdownlint-rules/ascii-only.js similarity index 63% rename from .markdownlint-rules/ascii-only.js rename to markdownlint-rules/ascii-only.js index aa1bc68..67fff97 100644 --- a/.markdownlint-rules/ascii-only.js +++ b/markdownlint-rules/ascii-only.js @@ -1,6 +1,7 @@ "use strict"; const { + iterateLinesWithFenceInfo, iterateNonFencedLines, pathMatchesAny, stripInlineCode, @@ -24,6 +25,27 @@ const DEFAULT_UNICODE_REPLACEMENTS = { "\u2018": "'", }; +/** + * Common non-English letters allowed by default (config allowedUnicode extends this). + * Escape → character: + * 00E9→é 00EF→ï 00E8→è 00EA→ê 00EB→ë 00E0→à 00E2→â 00E4→ä + * 00F9→ù 00FB→û 00FC→ü 00F4→ô 00F6→ö 00EE→î 00FF→ÿ 00F1→ñ + * 00E7→ç 00E1→á 00ED→í 00F3→ó 00FA→ú 00E3→ã 00F5→õ 00E6→æ + * 0153→œ 00F0→ð 00FE→þ 00F8→ø 00E5→å + * 00C9→É 00CF→Ï 00C8→È 00CA→Ê 00CB→Ë 00C0→À 00C2→ 00C4→Ä + * 00D9→Ù 00DB→Û 00DC→Ü 00D4→Ô 00D6→Ö 00CE→Î 00D1→Ñ + * 00C7→Ç 00C1→Á 00CD→Í 00D3→Ó 00DA→Ú 00C3→à 00D5→Õ 00C6→Æ 0152→Œ + */ +const DEFAULT_ALLOWED_UNICODE = [ + "\u00E9", "\u00EF", "\u00E8", "\u00EA", "\u00EB", "\u00E0", "\u00E2", "\u00E4", + "\u00F9", "\u00FB", "\u00FC", "\u00F4", "\u00F6", "\u00EE", "\u00FF", "\u00F1", + "\u00E7", "\u00E1", "\u00ED", "\u00F3", "\u00FA", "\u00E3", "\u00F5", "\u00E6", + "\u0153", "\u00F0", "\u00FE", "\u00F8", "\u00E5", "\u00C9", "\u00CF", "\u00C8", + "\u00CA", "\u00CB", "\u00C0", "\u00C2", "\u00C4", "\u00D9", "\u00DB", "\u00DC", + "\u00D4", "\u00D6", "\u00CE", "\u00D1", "\u00C7", "\u00C1", "\u00CD", "\u00D3", + "\u00DA", "\u00C3", "\u00D5", "\u00C6", "\u0152", +]; + /** * Return true if the string contains any non-ASCII character (code point > 0x7F). * @param {string} str - Input string @@ -150,6 +172,18 @@ function toCharSet(arr) { */ function getConfig(params) { const c = params.config || {}; + const defaultSet = toCharSet(DEFAULT_ALLOWED_UNICODE); + const configSet = toCharSet(c.allowedUnicode); + const replaceDefault = c.allowedUnicodeReplaceDefault === true; + const allowedUnicodeSet = replaceDefault + ? configSet + : new Set([...defaultSet, ...configSet]); + const disallowTypesRaw = Array.isArray(c.disallowUnicodeInCodeBlockTypes) + ? c.disallowUnicodeInCodeBlockTypes + : []; + const disallowUnicodeInCodeBlockTypesSet = new Set( + disallowTypesRaw.filter((t) => typeof t === "string").map((t) => String(t).trim().toLowerCase()), + ); return { allowedPathPatternsUnicode: Array.isArray(c.allowedPathPatternsUnicode) ? c.allowedPathPatternsUnicode @@ -158,7 +192,9 @@ function getConfig(params) { ? c.allowedPathPatternsEmoji : [], allowedEmoji: Array.isArray(c.allowedEmoji) ? c.allowedEmoji : [], - allowedUnicode: toCharSet(c.allowedUnicode), + allowedUnicode: allowedUnicodeSet, + allowUnicodeInCodeBlocks: c.allowUnicodeInCodeBlocks !== false, + disallowUnicodeInCodeBlockTypes: disallowUnicodeInCodeBlockTypesSet, unicodeReplacements: buildReplacementsMap( c.unicodeReplacements ?? DEFAULT_UNICODE_REPLACEMENTS, ), @@ -218,42 +254,69 @@ function buildOccurrenceDetail(ch, replacement, allowEmojiOnly, config) { return `Character '${ch}' (${cpStr}) not allowed; use ASCII only`; } +/** + * Whether to check this line when it is inside a fenced code block (call only when allowUnicodeInCodeBlocks is false). + * @param {boolean} inFencedBlock - Line is inside a fence + * @param {string} blockType - Info string of the block (e.g. "text", "bash") + * @param {Set} disallowTypes - Block types to check (empty = check all) + * @returns {boolean} + */ +function shouldCheckFencedLine(inFencedBlock, blockType, disallowTypes) { + if (!inFencedBlock) return true; + if (disallowTypes.size === 0) return true; + return disallowTypes.has(blockType); +} + /** * markdownlint rule: disallow non-ASCII except in configured paths; optional * replacement suggestions via unicodeReplacements. Paths can allow full Unicode - * or emoji-only. + * or emoji-only. Unicode and emoji inside fenced code blocks or between + * backticks (inline code) are ignored by default; use allowUnicodeInCodeBlocks + * and disallowUnicodeInCodeBlockTypes to check inside code blocks. * * @param {object} params - markdownlint params (lines, name, config) * @param {function(object): void} onError - Callback to report an error */ function ruleFunction(params, onError) { - const filePath = params.name || ""; - const config = getConfig(params); - const allowUnicode = pathMatchesAny(filePath, config.allowedPathPatternsUnicode); - const allowEmojiOnly = pathMatchesAny(filePath, config.allowedPathPatternsEmoji); - const allowedEmojiSet = toCharSet(config.allowedEmoji); - const allowedUnicodeSet = config.allowedUnicode; + const filePath = params.name || ""; + const config = getConfig(params); + const allowUnicode = pathMatchesAny(filePath, config.allowedPathPatternsUnicode); + const allowEmojiOnly = pathMatchesAny(filePath, config.allowedPathPatternsEmoji); + const allowedEmojiSet = toCharSet(config.allowedEmoji); + const allowedUnicodeSet = config.allowedUnicode; - for (const { lineNumber, line } of iterateNonFencedLines(params.lines)) { - const scan = stripInlineCode(line); - if (!hasNonAscii(scan)) continue; - if (allowUnicode) continue; - if (allowEmojiOnly && onlyAllowedEmoji(scan, allowedEmojiSet)) continue; + const checkLine = (lineNumber, line) => { + const scan = stripInlineCode(line); + if (!hasNonAscii(scan)) return; + if (allowUnicode) return; + if (allowEmojiOnly && onlyAllowedEmoji(scan, allowedEmojiSet)) return; - for (const { startIndex, char, length } of getDisallowedOccurrences( - scan, allowEmojiOnly, allowedUnicodeSet, allowedEmojiSet, - )) { - const column = startIndex + 1; - const replacement = config.unicodeReplacements.get(char); - onError({ - lineNumber, - detail: buildOccurrenceDetail(char, replacement, allowEmojiOnly, config), - context: line, - range: [column, length], - }); - } + for (const { startIndex, char, length } of getDisallowedOccurrences( + scan, allowEmojiOnly, allowedUnicodeSet, allowedEmojiSet, + )) { + const column = startIndex + 1; + const replacement = config.unicodeReplacements.get(char); + onError({ + lineNumber, + detail: buildOccurrenceDetail(char, replacement, allowEmojiOnly, config), + context: line, + range: [column, length], + }); + } + }; + + if (config.allowUnicodeInCodeBlocks) { + for (const { lineNumber, line } of iterateNonFencedLines(params.lines)) { + checkLine(lineNumber, line); } + return; + } + + for (const { lineNumber, line, inFencedBlock, blockType } of iterateLinesWithFenceInfo(params.lines)) { + if (!shouldCheckFencedLine(inFencedBlock, blockType, config.disallowUnicodeInCodeBlockTypes)) continue; + checkLine(lineNumber, line); } +} module.exports = { names: ["ascii-only"], diff --git a/markdownlint-rules/document-length.js b/markdownlint-rules/document-length.js new file mode 100644 index 0000000..cf8b6d0 --- /dev/null +++ b/markdownlint-rules/document-length.js @@ -0,0 +1,32 @@ +"use strict"; + +/** + * markdownlint rule: disallow documents longer than a configured number of lines. + * Reports a single error on line 1 when the file exceeds the maximum. + * + * @param {object} params - markdownlint params (lines, name, config) + * @param {function(object): void} onError - Callback to report an error + */ +function ruleFunction(params, onError) { + const lines = params.lines; + const raw = params.config?.maximum; + const maximum = + typeof raw === "number" && Number.isInteger(raw) && raw >= 1 ? raw : 1500; + + if (lines.length <= maximum) { + return; + } + + onError({ + lineNumber: 1, + detail: `Document has ${lines.length} lines (maximum ${maximum}). Consider splitting into smaller files.`, + context: lines[0] ?? "", + }); +} + +module.exports = { + names: ["document-length"], + description: "Disallow documents longer than a configured number of lines", + tags: ["length"], + function: ruleFunction, +}; diff --git a/.markdownlint-rules/heading-numbering.js b/markdownlint-rules/heading-numbering.js similarity index 100% rename from .markdownlint-rules/heading-numbering.js rename to markdownlint-rules/heading-numbering.js diff --git a/.markdownlint-rules/heading-title-case.js b/markdownlint-rules/heading-title-case.js similarity index 94% rename from .markdownlint-rules/heading-title-case.js rename to markdownlint-rules/heading-title-case.js index e5ee718..3c4c656 100644 --- a/.markdownlint-rules/heading-title-case.js +++ b/markdownlint-rules/heading-title-case.js @@ -196,11 +196,15 @@ function getWordRangeInLine(line, rawText, titleText, opts) { * @param {function(object): void} onError - Callback to report an error */ function ruleFunction(params, onError) { - const options = params.config?.["heading-title-case"] ?? {}; - const customLower = options.lowercaseWords; - const lowercaseWords = Array.isArray(customLower) && customLower.length > 0 - ? new Set(customLower.map((w) => String(w).toLowerCase().trim()).filter(Boolean)) - : DEFAULT_LOWERCASE_WORDS; + const options = params.config?.["heading-title-case"] ?? {}; + const customLower = options.lowercaseWords; + const configSet = Array.isArray(customLower) + ? new Set(customLower.map((w) => String(w).toLowerCase().trim()).filter(Boolean)) + : new Set(); + const replaceDefault = options.lowercaseWordsReplaceDefault === true; + const lowercaseWords = replaceDefault + ? configSet + : new Set([...DEFAULT_LOWERCASE_WORDS, ...configSet]); const headings = extractHeadings(params.lines); for (const h of headings) { diff --git a/.markdownlint-rules/no-duplicate-headings-normalized.js b/markdownlint-rules/no-duplicate-headings-normalized.js similarity index 100% rename from .markdownlint-rules/no-duplicate-headings-normalized.js rename to markdownlint-rules/no-duplicate-headings-normalized.js diff --git a/.markdownlint-rules/no-heading-like-lines.js b/markdownlint-rules/no-heading-like-lines.js similarity index 100% rename from .markdownlint-rules/no-heading-like-lines.js rename to markdownlint-rules/no-heading-like-lines.js diff --git a/.markdownlint-rules/utils.js b/markdownlint-rules/utils.js similarity index 79% rename from .markdownlint-rules/utils.js rename to markdownlint-rules/utils.js index c510caa..48cf5e4 100644 --- a/.markdownlint-rules/utils.js +++ b/markdownlint-rules/utils.js @@ -220,9 +220,62 @@ function pathMatchesAny(path, patterns) { return false; } +/** + * Parse fence line (e.g. "```text" or "~~~") to get info string (first word, lowercased). + * @param {string} line - Fence delimiter line + * @returns {string} Block type or "" + */ +function parseFenceInfo(line) { + const trimmed = line.trim(); + const match = trimmed.match(/^(```+|~~~+)\s*(\S*)/); + if (!match) { + return ""; + } + const rest = match[2].trim(); + const first = rest.split(/\s+/)[0] || ""; + return first.toLowerCase(); +} + +/** + * Iterate all lines with fence state; yields { lineNumber, line, inFencedBlock, blockType } for each line. + * Fence delimiter lines are not yielded; blockType is the info string (first word) of the opening fence. + * @param {string[]} lines - All lines + * @yields {{ lineNumber: number, line: string, inFencedBlock: boolean, blockType: string }} + */ +function* iterateLinesWithFenceInfo(lines) { + let inFence = false; + let fenceMarker = null; + let blockType = ""; + + for (let index = 0; index < lines.length; index++) { + const lineNumber = index + 1; + const line = lines[index]; + const trimmed = line.trim(); + const fenceMatch = trimmed.match(/^(```+|~~~+)/); + + if (fenceMatch) { + const marker = fenceMatch[1][0] === "`" ? "```" : "~~~"; + if (!inFence) { + inFence = true; + fenceMarker = marker; + blockType = parseFenceInfo(line); + } else if (fenceMarker === marker) { + inFence = false; + fenceMarker = null; + blockType = ""; + } + continue; + } + + yield { lineNumber, line, inFencedBlock: inFence, blockType }; + } +} + module.exports = { stripInlineCode, iterateNonFencedLines, + iterateLinesWithFenceInfo, + parseFenceInfo, extractHeadings, parseHeadingNumberPrefix, normalizeHeadingTitleForDup, diff --git a/md_test_files/expected_errors.yml b/md_test_files/expected_errors.yml index 9156bc0..4afff45 100644 --- a/md_test_files/expected_errors.yml +++ b/md_test_files/expected_errors.yml @@ -63,7 +63,10 @@ negative_ascii_only.md: rule: ascii-only column: 45 message_contains: "U+2019" - - line: 10 + - line: 9 + rule: ascii-only + message_contains: "U+0142" + - line: 11 rule: ascii-only column: 42 message_contains: use ASCII only @@ -89,6 +92,16 @@ negative_duplicate_headings_normalized.md: rule: no-duplicate-headings-normalized message_contains: Duplicate heading title "overview" +# negative_document_length.md is generated automatically (not committed). +# See test-scripts/verify_markdownlint_fixtures.py: ensure_long_document_fixture() and +# main() which creates it when missing; test_verify_markdownlint_fixtures.py generates +# it in test_verify_document_length_generated_fixture then removes it. +negative_document_length.md: + errors: + - line: 1 + rule: document-length + message_contains: "maximum" + negative_heading_like.md: errors: - line: 9 diff --git a/md_test_files/negative_ascii_only.md b/md_test_files/negative_ascii_only.md index c50c1dd..5d856b7 100644 --- a/md_test_files/negative_ascii_only.md +++ b/md_test_files/negative_ascii_only.md @@ -6,5 +6,6 @@ This file intentionally contains non-ASCII characters to trigger the ascii-only - Line with Unicode arrow: use → here (should highlight only the arrow). - Line with smart quotes: “curly” and ‘curly’ (each curly quote highlighted). +- Char not in default or config: Polish ł is reported when not in allowedUnicode. No allowed emoji in this list; if we add ✅ it should be reported. diff --git a/md_test_files/positive.md b/md_test_files/positive.md index eb5322e..3718d3b 100644 --- a/md_test_files/positive.md +++ b/md_test_files/positive.md @@ -39,6 +39,20 @@ If you need inline code that contains backticks, use a longer delimiter like: func Example() {} ``` +## Unicode/Emoji in Code (No Ascii-Only Errors) + +Unicode and emoji inside inline code or fenced blocks are ignored by ascii-only. +Default-allowed letters (e.g. é, ï, ñ) are allowed in prose; config `allowedUnicode` can add more (e.g. `ń`). + +- Default-allowed in prose: Café and naïve are allowed by default. +- Config-allowed in prose: `ń` is allowed when listed in `allowedUnicode` in config. +- Inline code only: use `→` or `ł` or `😀` here (no violation; rule ignores content inside backticks). +- Fenced block below is skipped by rule; unicode/emoji inside are ignored: + +```text +Arrow → and ł and 😀 inside fenced block — no ascii-only error. +``` + ## Allowed Inline HTML Anchors These anchor examples are permitted by the custom markdownlint rule `allow-custom-anchors`. diff --git a/package-lock.json b/package-lock.json index 1678521..0dc31c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,12 @@ "": { "name": "docs-as-code-tools", "devDependencies": { - "@eslint/js": "^9.0.0", - "c8": "^10.1.2", - "eslint": "^9.15.0", + "@eslint/js": "^9.17.0", + "c8": "^10.1.3", + "eslint": "^9.17.0", "eslint-plugin-security": "^3.0.1", - "globals": "^15.0.0", - "markdownlint-cli2": "^0.14.0" + "globals": "^17.3.0", + "markdownlint-cli2": "^0.20.0" } }, "node_modules/@bcoe/v8-coverage": { @@ -339,9 +339,9 @@ } }, "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, "license": "MIT", "engines": { @@ -351,6 +351,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -372,6 +382,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -540,6 +571,39 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -638,6 +702,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -685,6 +759,20 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -692,6 +780,30 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1064,6 +1176,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -1126,9 +1251,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { @@ -1139,26 +1264,36 @@ } }, "node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-15.0.0.tgz", + "integrity": "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw==", "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "path-type": "^6.0.0", "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1213,6 +1348,43 @@ "node": ">=0.8.19" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1246,6 +1418,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1359,6 +1542,23 @@ "dev": true, "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.28", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -1458,50 +1658,58 @@ } }, "node_modules/markdownlint": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.35.0.tgz", - "integrity": "sha512-wgp8yesWjFBL7bycA3hxwHRdsZGJhjhyP1dSxKVKrza0EPFYtn+mHtkVy6dvP1kGSjovyG5B8yNP6Frj0UFUJg==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", + "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", "dev": true, "license": "MIT", "dependencies": { - "markdown-it": "14.1.0", - "markdownlint-micromark": "0.1.10" + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.2", + "string-width": "8.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli2": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.14.0.tgz", - "integrity": "sha512-2cqdWy56frU2FTpbuGb83mEWWYuUIYv6xS8RVEoUAuKNw/hXPar2UYGpuzUhlFMngE8Omaz4RBH52MzfRbGshw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.20.0.tgz", + "integrity": "sha512-esPk+8Qvx/f0bzI7YelUeZp+jCtFOk3KjZ7s9iBQZ6HlymSXoTtWGiIRZP05/9Oy2ehIoIjenVwndxGtxOIJYQ==", "dev": true, "license": "MIT", "dependencies": { - "globby": "14.0.2", - "js-yaml": "4.1.0", + "globby": "15.0.0", + "js-yaml": "4.1.1", "jsonc-parser": "3.3.1", - "markdownlint": "0.35.0", - "markdownlint-cli2-formatter-default": "0.0.5", + "markdown-it": "14.1.0", + "markdownlint": "0.40.0", + "markdownlint-cli2-formatter-default": "0.0.6", "micromatch": "4.0.8" }, "bin": { - "markdownlint-cli2": "markdownlint-cli2.js" + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli2-formatter-default": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.5.tgz", - "integrity": "sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", "dev": true, "license": "MIT", "funding": { @@ -1511,30 +1719,21 @@ "markdownlint-cli2": ">=0.0.4" } }, - "node_modules/markdownlint-cli2/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/markdownlint/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/markdownlint-micromark": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.10.tgz", - "integrity": "sha512-no5ZfdqAdWGxftCLlySHSgddEjyW4kui4z7amQcGsSKfYC5v/ou+8mIQVyg9KQMeEZLNtz9OPDTj7nnTnoR4FQ==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/DavidAnson" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/mdurl": { @@ -1554,6 +1753,542 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -1675,6 +2410,26 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1713,13 +2468,13 @@ } }, "node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2131,9 +2886,9 @@ "license": "MIT" }, "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 6a34cb0..543dcbd 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,15 @@ "test:rules:coverage": "c8 --per-file node --test test/markdownlint-rules/*.test.js" }, "devDependencies": { - "@eslint/js": "^9.0.0", - "c8": "^10.1.2", - "eslint": "^9.15.0", + "@eslint/js": "^9.17.0", + "c8": "^10.1.3", + "eslint": "^9.17.0", "eslint-plugin-security": "^3.0.1", - "globals": "^15.0.0", - "markdownlint-cli2": "^0.14.0" + "globals": "^17.3.0", + "markdownlint-cli2": "^0.20.0" }, "c8": { - "include": [".markdownlint-rules/**/*.js"], + "include": ["markdownlint-rules/**/*.js"], "exclude": ["test/**"], "reporter": ["text", "lcov"], "all": true, diff --git a/test-scripts/test_verify_markdownlint_fixtures.py b/test-scripts/test_verify_markdownlint_fixtures.py index 318977d..2983fa0 100644 --- a/test-scripts/test_verify_markdownlint_fixtures.py +++ b/test-scripts/test_verify_markdownlint_fixtures.py @@ -294,6 +294,24 @@ def test_verify_file_positive_integration(self): except (OSError, FileNotFoundError) as e: self.skipTest(f"markdownlint not runnable: {e}") + def test_verify_document_length_generated_fixture(self): + """Generate a 1501-line fixture, verify document-length error, then remove file.""" + path = _REPO_ROOT / "md_test_files" / "negative_document_length.md" + expect_path = _REPO_ROOT / "md_test_files" / "expected_errors.yml" + if not expect_path.exists(): + self.skipTest("expected_errors.yml not found") + v.ensure_long_document_fixture(path) + try: + self.assertIn("negative_document_length.md", v.load_expected_errors(expect_path)) + cmd = v.find_markdownlint_cmd() + expectations = v.load_expected_errors(expect_path) + try: + v.verify_file(cmd, path, expectations) + except (OSError, FileNotFoundError) as e: + self.skipTest(f"markdownlint not runnable: {e}") + finally: + path.unlink(missing_ok=True) + def test_verify_file_raises_when_exit_code_mismatch(self): """Exit code mismatch (expect 0 errors, got non-zero) raises AssertionError.""" path = _REPO_ROOT / "md_test_files" / "positive.md" diff --git a/test-scripts/verify_markdownlint_fixtures.py b/test-scripts/verify_markdownlint_fixtures.py index 16142cc..57bf816 100644 --- a/test-scripts/verify_markdownlint_fixtures.py +++ b/test-scripts/verify_markdownlint_fixtures.py @@ -27,6 +27,8 @@ import yaml +LONG_FIXTURE_LINES = 1501 + @dataclass(frozen=True) class ExpectedError: @@ -60,11 +62,21 @@ def find_markdownlint_cmd() -> List[str]: return ["npx", "markdownlint-cli2"] +def ensure_long_document_fixture(path: Path) -> None: + """Write a markdown file with LONG_FIXTURE_LINES lines so document-length rule fails.""" + # Blank after heading to satisfy MD022; trailing newline to satisfy MD047 + lines = ["# Title", ""] + [f"line {i}" for i in range(3, LONG_FIXTURE_LINES + 1)] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + def list_fixture_files() -> List[Path]: """Return fixture paths: positive.md plus sorted negative_*.md in md_test_files.""" md_dir = repo_root() / "md_test_files" positive = md_dir / "positive.md" negatives = sorted(md_dir.glob("negative_*.md")) + long_fixture = md_dir / "negative_document_length.md" + if long_fixture not in negatives: + negatives = sorted(negatives + [long_fixture]) return [positive, *negatives] @@ -311,6 +323,8 @@ def main() -> int: failures: List[str] = [] for f in files: + if f.name == "negative_document_length.md" and not f.exists(): + ensure_long_document_fixture(f) if verbose: exp_total = 0 if f.name in expectations_by_file: diff --git a/test/markdownlint-rules/allow-custom-anchors.test.js b/test/markdownlint-rules/allow-custom-anchors.test.js index 2c4313d..948199c 100644 --- a/test/markdownlint-rules/allow-custom-anchors.test.js +++ b/test/markdownlint-rules/allow-custom-anchors.test.js @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/allow-custom-anchors.js"); +const rule = require("../../markdownlint-rules/allow-custom-anchors.js"); const { runRule } = require("./run-rule.js"); describe("allow-custom-anchors", () => { diff --git a/test/markdownlint-rules/ascii-only.test.js b/test/markdownlint-rules/ascii-only.test.js index 55f2d49..592488a 100644 --- a/test/markdownlint-rules/ascii-only.test.js +++ b/test/markdownlint-rules/ascii-only.test.js @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/ascii-only.js"); +const rule = require("../../markdownlint-rules/ascii-only.js"); const { runRule } = require("./run-rule.js"); describe("ascii-only", () => { @@ -20,8 +20,8 @@ describe("ascii-only", () => { }); it("reports error for non-ASCII when path not allowlisted", () => { - // Accented characters (é, ï) are reported when path is not in allowlist. - const lines = ["Café and naïve"]; + // Arrow → is not in default allowed set; reported when path is not in allowlist. + const lines = ["Use arrow \u2192 here"]; const errors = runRule(rule, lines, {}, "doc.md"); assert.ok(errors.length >= 1); assert.ok(errors.some((e) => e.detail.includes("ASCII") || e.detail.includes("U+"))); @@ -40,7 +40,7 @@ describe("ascii-only", () => { }); it("reports error with emoji-list message when path is emoji-only and char not in list", () => { - const lines = ["Café"]; + const lines = ["Arrow \u2192 here"]; const errors = runRule(rule, lines, { allowedPathPatternsEmoji: ["*.md"], allowedEmoji: ["\u263A"], @@ -58,7 +58,7 @@ describe("ascii-only", () => { }); it("reports error when path does not match any unicode pattern (utils pathMatchesAny)", () => { - const lines = ["Café"]; + const lines = ["Arrow \u2192"]; const errors = runRule(rule, lines, { allowedPathPatternsUnicode: ["other.md"], }, "doc.md"); @@ -71,6 +71,66 @@ describe("ascii-only", () => { assert.strictEqual(errors.length, 0); }); + it("skips content inside ``` fenced code block (ignore unicode in code blocks)", () => { + const lines = ["```", "Café and 😀 inside backtick fence", "```", "Plain"]; + const errors = runRule(rule, lines, {}, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("allowUnicodeInCodeBlocks: false reports unicode inside fenced block", () => { + const lines = ["```", "Arrow \u2192 inside fence", "```"]; + const errors = runRule(rule, lines, { allowUnicodeInCodeBlocks: false }, "doc.md"); + assert.ok(errors.length >= 1); + assert.ok(errors.some((e) => e.lineNumber === 2 && (e.detail.includes("U+2192") || e.detail.includes("→")))); + }); + + it("allowUnicodeInCodeBlocks: false with disallowUnicodeInCodeBlockTypes: [\"text\"] reports in ```text only", () => { + const lines = [ + "```text", + "Unicode \u2192 here", + "```", + "```go", + "Unicode \u2192 here", + "```", + ]; + const errors = runRule(rule, lines, { + allowUnicodeInCodeBlocks: false, + disallowUnicodeInCodeBlockTypes: ["text"], + }, "doc.md"); + assert.ok(errors.length >= 1, "should report in ```text block"); + assert.ok(errors.some((e) => e.lineNumber === 2), "error on line 2 (text block)"); + const goBlockErrors = errors.filter((e) => e.lineNumber === 5); + assert.strictEqual(goBlockErrors.length, 0, "no errors in ```go block when only text is in disallow list"); + }); + + it("disallowUnicodeInCodeBlockTypes: [] with allowUnicodeInCodeBlocks false checks all blocks", () => { + const lines = ["```go", "Arrow \u2192", "```"]; + const errors = runRule(rule, lines, { + allowUnicodeInCodeBlocks: false, + disallowUnicodeInCodeBlockTypes: [], + }, "doc.md"); + assert.ok(errors.length >= 1); + }); + + it("reports no errors when unicode is only inside single backticks", () => { + const lines = ["Use `café` or `naïve` in code."]; + const errors = runRule(rule, lines, {}, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports no errors when emoji is only inside backticks", () => { + const lines = ["Run `echo 😀` for a smile."]; + const errors = runRule(rule, lines, {}, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports error for unicode outside backticks but not for inside", () => { + const lines = ["Arrow \u2192 has `\u2192` in code."]; + const errors = runRule(rule, lines, {}, "doc.md"); + assert.ok(errors.length >= 1, "should report arrow outside backticks"); + assert.ok(errors.some((e) => e.detail.includes("U+2192") || e.detail.includes("→")), "detail should mention the character"); + }); + it("reports no errors when path is emoji-only and content has only allowed emoji", () => { const lines = ["Hello \u263A"]; // ☺ in allowed list const errors = runRule(rule, lines, { @@ -89,27 +149,85 @@ describe("ascii-only", () => { assert.strictEqual(errors.length, 0); }); - it("reports no errors when non-ASCII char is in allowedUnicode set", () => { - const lines = ["Café"]; + it("reports no errors for default-allowed letters (é, ï) without config", () => { + const lines = ["Café and naïve"]; + const errors = runRule(rule, lines, {}, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports no errors when non-ASCII char is in config allowedUnicode (extends default)", () => { + const lines = ["\u0144"]; // ń not in default set const errors = runRule(rule, lines, { - allowedUnicode: ["\u00E9"], + allowedUnicode: ["\u0144"], + }, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports no errors when multiple chars from config allowedUnicode (config file style)", () => { + const lines = ["Polish: \u0144 and \u0142"]; // ń, ł not in default + const errors = runRule(rule, lines, { + allowedUnicode: ["\u0144", "\u0142"], + }, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports no errors when line has both default-allowed and config allowedUnicode", () => { + const lines = ["Café and Zdu\u0144"]; // é default, ń from config + const errors = runRule(rule, lines, { + allowedUnicode: ["\u0144"], + }, "doc.md"); + assert.strictEqual(errors.length, 0); + }); + + it("reports error for char not in default and not in config allowedUnicode", () => { + const lines = ["\u0144"]; // ń + const errors = runRule(rule, lines, {}, "doc.md"); + assert.ok(errors.length >= 1); + assert.ok(errors.some((e) => e.detail.includes("U+0144") || e.detail.includes("ń"))); + }); + + it("reports error when config allowedUnicode omits a char used in line", () => { + const lines = ["\u0144 and \u0142"]; // ń allowed by config, ł not + const errors = runRule(rule, lines, { + allowedUnicode: ["\u0144"], + }, "doc.md"); + assert.ok(errors.length >= 1); + assert.ok(errors.some((e) => e.detail.includes("U+0142") || e.detail.includes("ł"))); + }); + + it("allowedUnicodeReplaceDefault: true uses only config list (no default set)", () => { + const lines = ["Café and \u2192"]; // é in default, → not; with replace, only → in list + const errors = runRule(rule, lines, { + allowedUnicode: ["\u2192"], + allowedUnicodeReplaceDefault: true, + }, "doc.md"); + assert.ok(errors.length >= 1, "é should be reported when default is replaced"); + assert.ok(errors.some((e) => e.detail.includes("U+00E9") || e.detail.includes("é"))); + assert.ok(!errors.some((e) => e.detail.includes("U+2192")), "→ should be allowed by config list"); + }); + + it("allowedUnicodeReplaceDefault: false (default) extends default set", () => { + const lines = ["Café and \u0144"]; + const errors = runRule(rule, lines, { + allowedUnicode: ["\u0144"], + allowedUnicodeReplaceDefault: false, }, "doc.md"); assert.strictEqual(errors.length, 0); }); it("includes suggested replacement when unicodeReplacements is object", () => { - const lines = ["Café"]; + const lines = ["Arrow \u2192"]; const errors = runRule(rule, lines, { - unicodeReplacements: { "\u00E9": "e" }, + unicodeReplacements: { "\u2192": "->" }, }, "doc.md"); assert.ok(errors.length >= 1); - assert.ok(errors.some((e) => e.detail.includes("suggested replacement") && e.detail.includes("e"))); + assert.ok(errors.some((e) => e.detail.includes("suggested replacement") && e.detail.includes("->"))); }); it("includes suggested replacement when unicodeReplacements is array", () => { - const lines = ["Café"]; + const lines = ["Arrow \u2192"]; const errors = runRule(rule, lines, { - unicodeReplacements: [["\u00E9", "e"]], + unicodeReplacements: [["\u2192", "->"]], }, "doc.md"); assert.ok(errors.length >= 1); assert.ok(errors.some((e) => e.detail.includes("suggested replacement"))); @@ -123,10 +241,10 @@ describe("ascii-only", () => { }); it("strips inline code before checking (utils stripInlineCode fence match)", () => { - const lines = ["Café ``code``"]; + const lines = ["Arrow \u2192 ``code``"]; const errors = runRule(rule, lines, {}, "doc.md"); assert.ok(errors.length >= 1); - assert.ok(errors.some((e) => e.detail.includes("Café") || e.detail.includes("U+"))); + assert.ok(errors.some((e) => e.detail.includes("U+2192") || e.detail.includes("→"))); }); it("skips non-string entries in path patterns (utils pathMatchesAny)", () => { @@ -146,7 +264,7 @@ describe("ascii-only", () => { }); it("uses default replacements when unicodeReplacements is falsy (buildReplacementsMap early return)", () => { - const lines = ["Café"]; + const lines = ["Arrow \u2192"]; const errors = runRule(rule, lines, { unicodeReplacements: "", }, "doc.md"); diff --git a/test/markdownlint-rules/document-length.test.js b/test/markdownlint-rules/document-length.test.js new file mode 100644 index 0000000..6fb6ad4 --- /dev/null +++ b/test/markdownlint-rules/document-length.test.js @@ -0,0 +1,77 @@ +"use strict"; + +/** + * Unit tests for document-length: disallow documents longer than a configured + * maximum number of lines; reports one error on line 1 when over the limit. + */ + +const { describe, it } = require("node:test"); +const assert = require("node:assert"); +const rule = require("../../markdownlint-rules/document-length.js"); +const { runRule } = require("./run-rule.js"); + +function makeLines(n) { + return Array.from({ length: n }, (_, i) => (i === 0 ? "# Title" : `line ${i + 1}`)); +} + +describe("document-length", () => { + it("reports no errors when lines.length <= maximum (default 1500)", () => { + const lines = makeLines(1500); + const errors = runRule(rule, lines); + assert.strictEqual(errors.length, 0); + }); + + it("reports one error when lines.length > maximum (1501 lines)", () => { + const lines = makeLines(1501); + const errors = runRule(rule, lines); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].lineNumber, 1); + assert.ok(errors[0].detail.includes("1501") && errors[0].detail.includes("1500")); + assert.ok(errors[0].detail.includes("maximum") || errors[0].detail.includes("Consider splitting")); + }); + + it("respects custom maximum: 11 lines with max 10 reports one error", () => { + const lines = makeLines(11); + const errors = runRule(rule, lines, { maximum: 10 }); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].lineNumber, 1); + assert.ok(errors[0].detail.includes("11") && errors[0].detail.includes("10")); + }); + + it("respects custom maximum: 10 lines with max 10 reports no error", () => { + const lines = makeLines(10); + const errors = runRule(rule, lines, { maximum: 10 }); + assert.strictEqual(errors.length, 0); + }); + + it("reports no error for 0 lines", () => { + const errors = runRule(rule, []); + assert.strictEqual(errors.length, 0); + }); + + it("reports no error for 1 line when max is 1", () => { + const errors = runRule(rule, ["only line"], { maximum: 1 }); + assert.strictEqual(errors.length, 0); + }); + + it("reports one error for 2 lines when max is 1", () => { + const errors = runRule(rule, ["first", "second"], { maximum: 1 }); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].lineNumber, 1); + }); + + it("uses default maximum when config.maximum is not a positive integer", () => { + const lines = makeLines(1501); + const errors = runRule(rule, lines, { maximum: "1500" }); + assert.strictEqual(errors.length, 1); + assert.ok(errors[0].detail.includes("1500")); + }); + + it("reports error with empty context when first line is undefined (over limit)", () => { + const lines = Array(2); + lines[1] = "second"; + const errors = runRule(rule, lines, { maximum: 1 }); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].context, ""); + }); +}); diff --git a/test/markdownlint-rules/heading-numbering.test.js b/test/markdownlint-rules/heading-numbering.test.js index bd4a8c8..98fa12d 100644 --- a/test/markdownlint-rules/heading-numbering.test.js +++ b/test/markdownlint-rules/heading-numbering.test.js @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/heading-numbering.js"); +const rule = require("../../markdownlint-rules/heading-numbering.js"); const { runRule } = require("./run-rule.js"); describe("heading-numbering", () => { diff --git a/test/markdownlint-rules/heading-title-case.test.js b/test/markdownlint-rules/heading-title-case.test.js index d275e52..e7dc632 100644 --- a/test/markdownlint-rules/heading-title-case.test.js +++ b/test/markdownlint-rules/heading-title-case.test.js @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/heading-title-case.js"); +const rule = require("../../markdownlint-rules/heading-title-case.js"); const { runRule } = require("./run-rule.js"); describe("heading-title-case", () => { @@ -31,12 +31,28 @@ describe("heading-title-case", () => { assert.ok(Array.isArray(errors[0].range) && errors[0].range.length === 2, "error should include range [column, length] for the violating word"); }); - it("allows lowercase and/or in the middle when configured", () => { - // Custom lowercaseWords permits "through" and "and"; first/last ("Use", "Other") stay capped. - // Without config, AP-style rules treat "through" as a major word (so "through" would be invalid). + it("allows lowercase in the middle when lowercaseWords extends default", () => { + // Config extends default: "through" added; "and" already in default. First/last ("Use", "Other") stay capped. const lines = ["# Use through and Other"]; const errors = runRule(rule, lines, { - "heading-title-case": { lowercaseWords: ["through", "and"] }, + "heading-title-case": { lowercaseWords: ["through"] }, + }); + assert.strictEqual(errors.length, 0); + }); + + it("lowercaseWordsReplaceDefault: true uses only config list", () => { + const lines = ["# This And That"]; + const errors = runRule(rule, lines, { + "heading-title-case": { lowercaseWords: ["and"], lowercaseWordsReplaceDefault: true }, + }); + assert.strictEqual(errors.length, 1); + assert.ok(errors[0].detail.includes("And") && errors[0].detail.includes("lowercase")); + }); + + it("lowercaseWordsReplaceDefault: false (default) merges config with default", () => { + const lines = ["# Use through and Other"]; + const errors = runRule(rule, lines, { + "heading-title-case": { lowercaseWords: ["through"], lowercaseWordsReplaceDefault: false }, }); assert.strictEqual(errors.length, 0); }); diff --git a/test/markdownlint-rules/no-duplicate-headings-normalized.test.js b/test/markdownlint-rules/no-duplicate-headings-normalized.test.js index 27d9d3c..cafd45a 100644 --- a/test/markdownlint-rules/no-duplicate-headings-normalized.test.js +++ b/test/markdownlint-rules/no-duplicate-headings-normalized.test.js @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/no-duplicate-headings-normalized.js"); +const rule = require("../../markdownlint-rules/no-duplicate-headings-normalized.js"); const { runRule } = require("./run-rule.js"); describe("no-duplicate-headings-normalized", () => { diff --git a/test/markdownlint-rules/no-heading-like-lines.test.js b/test/markdownlint-rules/no-heading-like-lines.test.js index b348e70..150ace9 100644 --- a/test/markdownlint-rules/no-heading-like-lines.test.js +++ b/test/markdownlint-rules/no-heading-like-lines.test.js @@ -8,7 +8,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const rule = require("../../.markdownlint-rules/no-heading-like-lines.js"); +const rule = require("../../markdownlint-rules/no-heading-like-lines.js"); const { runRule } = require("./run-rule.js"); describe("no-heading-like-lines", () => { diff --git a/test/markdownlint-rules/security.test.js b/test/markdownlint-rules/security.test.js index f36e530..8616be8 100644 --- a/test/markdownlint-rules/security.test.js +++ b/test/markdownlint-rules/security.test.js @@ -1,7 +1,7 @@ "use strict"; /** - * Security-focused tests for .markdownlint-rules: + * Security-focused tests for markdownlint-rules: * - Invalid or malicious regex in config does not throw (safeRegExp / defensive parsing). * - Rules complete within a timeout when given ReDoS-prone patterns and long input * (ensures we don't hang on malicious or accidental catastrophic backtracking). @@ -9,7 +9,7 @@ const { describe, it } = require("node:test"); const assert = require("node:assert"); -const ruleAnchors = require("../../.markdownlint-rules/allow-custom-anchors.js"); +const ruleAnchors = require("../../markdownlint-rules/allow-custom-anchors.js"); const { runRule } = require("./run-rule.js"); describe("security", () => {