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", () => {