Skip to content

Commit 065cabb

Browse files
committed
Add excludePathPatterns and fixInfo across rules; config helper and option tests
- All custom rules depend on utils.js and support excludePathPatterns. - no-heading-like-lines: whole-line emphasis, punctuationMarks option; document-length: excludePathPatterns; ascii-only: en-dash replacement, excludePathPatterns. - Add markdownlint_config_helper.py and test_markdownlint_options.py; expand fix tests and rule unit tests.
1 parent 35eecf0 commit 065cabb

38 files changed

Lines changed: 2187 additions & 320 deletions

‎.github/workflows/markdownlint-tests.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
name: Markdownlint tests
22

33
# Positive tests must pass markdownlint; negative tests must fail.
4-
# Fails if positive fails lint or if any negative passes lint.
4+
# Also runs markdownlint --fix functional tests (test_fix_*.py).
5+
# Keep in sync with Makefile targets test-markdownlint and test-markdownlint-fix.
56

67
on:
78
push:
@@ -10,12 +11,16 @@ on:
1011
- '.markdownlint.yml'
1112
- 'markdownlint-rules/**'
1213
- 'md_test_files/**'
14+
- 'test-scripts/test_fix_*.py'
15+
- 'test-scripts/markdownlint_config_helper.py'
1316
pull_request:
1417
paths:
1518
- '.markdownlint-cli2.jsonc'
1619
- '.markdownlint.yml'
1720
- 'markdownlint-rules/**'
1821
- 'md_test_files/**'
22+
- 'test-scripts/test_fix_*.py'
23+
- 'test-scripts/markdownlint_config_helper.py'
1924

2025
jobs:
2126
markdownlint-tests:
@@ -43,3 +48,6 @@ jobs:
4348

4449
- name: Run markdownlint fixture tests
4550
run: make test-markdownlint
51+
52+
- name: Run markdownlint --fix functional tests
53+
run: make test-markdownlint-fix

‎.markdownlint.yml‎

Lines changed: 100 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33

44
default: true
55

6-
# MD013/line-length - line length limits (non-default)
6+
# MD013/line-length - line length limits (built-in; default off when overridden).
7+
# Options:
8+
# line_length: number (default 80); max characters per line for body
9+
# heading_line_length: number (default 80); max for heading lines
10+
# code_block_line_length: number (default 80); max for code block lines
11+
# code_blocks: bool (default true); include code blocks in checks
12+
# tables: bool (default true); include tables in checks
13+
# headings: bool (default true); include headings in checks
14+
# strict: bool (default false); enforce length even when no whitespace beyond limit
15+
# stern: bool (default false); warn for fixable long lines but allow long lines without spaces
716
MD013:
817
line_length: 500
918
heading_line_length: 500
@@ -14,21 +23,23 @@ MD013:
1423
strict: false
1524
stern: false
1625

17-
# MD033/no-inline-html - allow anchor elements for custom anchors
26+
# MD033/no-inline-html - allow only listed HTML elements (built-in).
27+
# Options:
28+
# allowed_elements: list of element names (e.g. "a", "br"); only these HTML elements are allowed
1829
MD033:
1930
allowed_elements: ["a"]
2031

21-
# ascii-only: disallow non-ASCII except in configured paths; inline code (backticks) always stripped
22-
# No built-in path/emoji defaults. Options:
23-
# - allowedPathPatternsUnicode: glob list; files where any non-ASCII is allowed
24-
# - allowedPathPatternsEmoji: glob list; files where only allowedEmoji chars are allowed
25-
# - allowedEmoji: emoji/chars allowed in paths matching allowedPathPatternsEmoji (multi-codepoint OK)
26-
# - allowedUnicode: chars allowed in all files; extends default set (é, ï, ñ, ç, etc.) unless replace below
27-
# - allowedUnicodeReplaceDefault: true = use only allowedUnicode list (no default set)
28-
# - allowUnicodeInCodeBlocks: true (default) = skip fenced blocks; false = check them for unicode
29-
# - disallowUnicodeInCodeBlockTypes: when allowUnicodeInCodeBlocks false, only these block types checked
30-
# (block type = first word after opening fence, e.g. "text" from ```text); empty = check all blocks
31-
# - unicodeReplacements: object or [char, replacement] array; built-in defaults (arrows, quotes, <=>=*) if omitted
32+
# ascii-only: disallow non-ASCII except in configured paths; inline code (backticks) always stripped. Fixable when replacement in unicodeReplacements.
33+
# Options (all optional):
34+
# allowedEmoji: list of chars/emoji allowed in paths matching allowedPathPatternsEmoji (multi-codepoint OK)
35+
# allowedPathPatternsEmoji: list of globs; files where only allowedEmoji chars are allowed
36+
# allowedPathPatternsUnicode: list of globs; files where any non-ASCII is allowed
37+
# allowedUnicode: list of single-char strings; allowed in all files (extends default é, ï, ñ, etc. unless replace below)
38+
# allowedUnicodeReplaceDefault: bool (default false); true = use only allowedUnicode list, no built-in default
39+
# allowUnicodeInCodeBlocks: bool (default true); false = check inside fenced blocks
40+
# disallowUnicodeInCodeBlockTypes: list of block types (e.g. "text", "bash"); when allowUnicodeInCodeBlocks false, only these checked; empty = all
41+
# excludePathPatterns: list of globs; skip this rule for matching file paths
42+
# unicodeReplacements: object or [[char, replacement], ...]; built-in defaults (arrows, quotes, etc.) if omitted
3243
ascii-only:
3344
allowedPathPatternsUnicode:
3445
# - "**/README.md"
@@ -43,43 +54,54 @@ ascii-only:
4354
- "📊"
4455
- "⚠️"
4556

46-
# document-length: disallow documents longer than maximum lines (default 1500)
47-
# - maximum: positive integer; default 1500
57+
# document-length: disallow documents longer than maximum lines.
58+
# Options:
59+
# maximum: number (default 1500); max allowed line count; must be positive integer
60+
# excludePathPatterns: list of globs; skip this rule for matching file paths
4861
document-length:
4962
maximum: 1500
5063

51-
# heading-title-case: AP-style headline capitalization; words in backticks ignored
64+
# heading-title-case: AP-style headline capitalization; words in backticks and file names (suggest backticks) handled. Fixable.
65+
# Options (all optional):
66+
# lowercaseWords: list of strings; words that must be lowercase in middle (extends default list)
67+
# lowercaseWordsReplaceDefault: bool (default false); true = use only lowercaseWords list, no built-in default
68+
# excludePathPatterns: list of globs; skip this rule for matching file paths
5269
# heading-title-case:
53-
# lowercaseWords: ["through", ...] # optional; extends default list (add words)
54-
# lowercaseWordsReplaceDefault: true # optional; true = use only lowercaseWords list, no default set
70+
# lowercaseWords: ["through"]
71+
# lowercaseWordsReplaceDefault: true
5572

56-
# no-heading-like-lines: report lines that look like headings but are not (e.g. **Text:**, 1. **Text**); fixable
57-
# - convertToHeading: bool (default false); when false, fix strips emphasis to plain text; when true, fix converts to ATX heading (context-aware level, optional blank after)
58-
# - defaultHeadingLevel: number 1-6 (default 2); used when converting to heading and there is no preceding heading
59-
# - fixedHeadingLevel: number 1-6 (optional); when set, suggested heading uses this level and ignores context
60-
# - excludePathPatterns: list of globs; skip this rule for matching file paths
61-
# With convertToHeading true, optional heading-title-case.js and heading-numbering.js in same dir add AP title case and numbering.
73+
# no-heading-like-lines: report lines that look like headings but are not (e.g. **Text:**, 1. **Text**, MD036-style **Intro**). Fixable.
74+
# Options (all optional):
75+
# convertToHeading: bool (default false); when true, fix converts to ATX heading; when false, fix strips emphasis to plain text
76+
# defaultHeadingLevel: number 1-6 (default 2); used when converting to heading and there is no preceding heading
77+
# fixedHeadingLevel: number 1-6; when set, suggested heading uses this level and ignores context
78+
# punctuationMarks: string (default ".,;!?"); for whole-line emphasis, skip when content ends with one of these (no colon = catch colon lines)
79+
# excludePathPatterns: list of globs; skip this rule for matching file paths
80+
# With convertToHeading true, heading-title-case.js and heading-numbering.js in same dir (if present) add AP title case and numbering.
6281
# no-heading-like-lines:
6382
# convertToHeading: false
6483
# defaultHeadingLevel: 2
6584
# # fixedHeadingLevel: 3
6685
# # excludePathPatterns: ["**/README.md"]
6786

68-
# no-h1-content: under first h1 allow only TOC (blank, list-of-links, HTML comments)
69-
# - excludePathPatterns: glob list; skip this rule for matching paths (e.g. md_test_files for fixtures)
87+
# no-h1-content: under first h1 allow only TOC (blank lines, list-of-links, badges, HTML comments including multi-line).
88+
# Options:
89+
# excludePathPatterns: list of globs; skip this rule for matching file paths
7090
# no-h1-content:
7191
# excludePathPatterns:
7292
# - "README.md"
7393
# - "CONTRIBUTING.md"
7494
# - "**/README.md"
7595

76-
# fenced-code-under-heading: code blocks for given languages must sit under an H2-H6 heading; use when grouping code under section titles
77-
# - languages: list of info strings (e.g. go, bash); only these block types are checked (first word after opening fence)
78-
# - minHeadingLevel / maxHeadingLevel: numbers (default 2 and 6); heading levels that count; blocks must fall under a heading in this range
79-
# - maxBlocksPerHeading: number (optional); max blocks that match languages per heading; only configured language(s) count (other block types allowed)
80-
# - requireHeading: bool (default true); when true, every applicable block must have a preceding heading
81-
# - exclusive: bool (default false); when true, at most one fenced code block (any language) per heading, and it must be one of languages
82-
# - excludePaths / includePaths: glob lists; skip or restrict which paths are checked (includePaths set = only those files)
96+
# fenced-code-under-heading: code blocks for given languages must sit under an H2-H6 heading.
97+
# Options:
98+
# languages: list of strings (required); info strings (e.g. go, bash) to check; block type = first word after opening fence
99+
# minHeadingLevel / maxHeadingLevel: numbers (default 2 and 6); heading levels that count
100+
# maxBlocksPerHeading: number; max blocks matching languages per heading; only configured language(s) count unless exclusive
101+
# requireHeading: bool (default true); when true, every applicable block must have a preceding heading
102+
# exclusive: bool (default false); when true, at most one fenced block (any language) per heading, and it must be one of languages
103+
# excludePaths / includePaths: list of globs; excludePaths skips matching paths; includePaths (when set) = only those files checked
104+
# excludePathPatterns: list of globs; skip this rule for matching file paths (alias for excludePaths in this rule)
83105
fenced-code-under-heading:
84106
languages:
85107
- "go"
@@ -99,32 +121,39 @@ fenced-code-under-heading:
99121
- "md_test_files/README.md"
100122
- "test-scripts/README.md"
101123

102-
# heading-min-words: headings at or below a level must have at least N words; use to avoid single-word or empty-looking headings
103-
# - minWords: number (required); minimum word count per heading (after optional numbering strip)
104-
# - applyToLevelsAtOrBelow: number (required); apply to headings at this level or deeper (e.g. 4 = H1-H4 checked)
105-
# - minLevel / maxLevel: numbers (optional); only headings in this level range checked (default 1-6)
106-
# - excludePaths / includePaths: glob lists; skip or restrict which paths are checked
107-
# - allowList: list of exact heading titles (after strip) allowed even if below minWords (e.g. "Overview", "Summary")
108-
# - stripNumbering: bool (default true); when true, leading numbering (e.g. 1.2.3) stripped before counting words and allowList match
124+
# heading-min-words: headings at or below a level must have at least N words (after optional numbering strip).
125+
# Options:
126+
# minWords: number (required); minimum word count per heading
127+
# applyToLevelsAtOrBelow: number (required); apply to headings at this level or deeper (e.g. 4 = H1-H4)
128+
# minLevel / maxLevel: numbers (optional); only headings in this level range checked (default 1-6)
129+
# excludePaths / includePaths: list of globs; excludePaths skips; includePaths (when set) = only those files
130+
# allowList: list of exact heading titles (after strip) allowed even if below minWords (e.g. "Overview", "Summary")
131+
# stripNumbering: bool (default true); when true, leading numbering (e.g. 1.2.3) stripped before counting and allowList match
132+
# excludePathPatterns: list of globs; skip this rule for matching file paths
109133
heading-min-words:
110134
minWords: 2
111135
applyToLevelsAtOrBelow: 4
112136

113-
# no-empty-heading: H2+ must have content; allow file-level override or per-section suppress comment
114-
# - minimumContentLines: number (default 1); minimum lines that count as content under each H2+
115-
# - countBlankLinesAsContent: bool (default false); count blank lines toward minimum
116-
# - countHTMLCommentsAsContent: bool (default false); count HTML-comment-only lines
117-
# - countHtmlLinesAsContent: bool (default false); count HTML-tag-only lines (e.g. <br>)
118-
# - countCodeBlockLinesAsContent: bool (default true); count lines inside ```/~~~ blocks
119-
# - excludePathPatterns: glob list; skip this rule for matching paths (e.g. **/*_index.md)
120-
# - Only "<!-- no-empty-heading allow -->" on its own line suppresses the error.
137+
# no-empty-heading: H2+ must have content before any subheading; single- and multi-line HTML comments supported.
138+
# Options (all optional):
139+
# minimumContentLines: number (default 1); minimum lines that count as content under each H2+
140+
# countBlankLinesAsContent: bool (default false); count blank lines toward minimum
141+
# countHTMLCommentsAsContent: bool (default false); count HTML-comment-only lines (including multi-line <!-- ... -->)
142+
# countHtmlLinesAsContent: bool (default false); count HTML-tag-only lines (e.g. <br>)
143+
# countCodeBlockLinesAsContent: bool (default true); count lines inside ```/~~~ blocks
144+
# excludePathPatterns: list of globs; skip this rule for matching file paths
145+
# Suppress per section: "<!-- no-empty-heading allow -->" on its own line allows that section to be empty.
121146
no-empty-heading:
122147
excludePathPatterns:
123148
- "**/*_index.md"
124149

125-
# allow-custom-anchors: require anchor ids to match patterns; optional placement (line/heading) per pattern
126-
# - allowedIdPatterns: list of regex strings or { pattern, placement? }; placement applies per pattern
127-
# - strictPlacement: true (default) = enforce placement when a pattern has placement set
150+
# allow-custom-anchors: allow only configured <a id="..."></a> anchor id patterns; optional placement per pattern.
151+
# Options:
152+
# allowedIdPatterns: array (required); each entry regex string or { pattern: string, placement?: object }
153+
# placement: headingMatch (regex), lineMatch (regex), standaloneLine (bool), requireAfter (["blank","fencedBlock","list"]),
154+
# anchorImmediatelyAfterHeading (bool), maxPerSection (number)
155+
# strictPlacement: bool (default true); when true, enforce placement when the matching pattern has placement set
156+
# excludePathPatterns: list of globs; skip this rule for matching file paths
128157
allow-custom-anchors:
129158
allowedIdPatterns:
130159
- pattern: "^spec-[a-z0-9-]+$"
@@ -151,4 +180,24 @@ allow-custom-anchors:
151180
anchorImmediatelyAfterHeading: true
152181
maxPerSection: 1
153182
strictPlacement: true
183+
184+
# heading-numbering: enforce numeric outline (e.g. 1, 1.1, 1.2). Fixable. Not configured here; options documented for reference.
185+
# Options (all optional):
186+
# maxHeadingLevel: number 1-6 (default 6); headings at or below this level must be numbered
187+
# maxSegmentValue: number (default 99); max value for any segment (e.g. 99 => 1.99.3 allowed)
188+
# maxSegmentValueMinLevel: number 1-6 (default 2); min heading level for maxSegmentValue cap
189+
# maxSegmentValueMaxLevel: number 1-6 (default 6); max heading level for maxSegmentValue cap
190+
# excludePathPatterns: list of globs; skip this rule for matching file paths
191+
# heading-numbering:
192+
# maxHeadingLevel: 6
193+
# maxSegmentValue: 99
194+
# maxSegmentValueMinLevel: 2
195+
# maxSegmentValueMaxLevel: 6
196+
# # excludePathPatterns: ["**/README.md"]
197+
198+
# no-duplicate-headings-normalized: disallow duplicate heading text after normalizing (whitespace/case). Not configured here.
199+
# Options:
200+
# excludePathPatterns: list of globs; skip this rule for matching file paths
201+
# no-duplicate-headings-normalized:
202+
# # excludePathPatterns: ["**/README.md"]
154203
...

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ npm install
2828

2929
Run the Makefile targets below so your PR stays green.
3030

31-
## Checks - Makefile Targets
31+
## Checks - `Makefile` Targets
3232

3333
These targets mirror the GitHub Actions workflows. Run them locally before pushing.
3434

‎Makefile‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
.PHONY: ci lint-js lint-readmes lint-python test-markdownlint test-markdownlint-fix test-python test-python-coverage test-rules test-rules-coverage venv
1+
.PHONY: ci lint-js lint-readmes lint-python test-markdownlint test-markdownlint-fix test-markdownlint-options test-python test-python-coverage test-rules test-rules-coverage venv
22

33
# Run all CI checks (same as GitHub Actions workflows). Run after 'npm install' and optionally 'make venv'.
4-
ci: lint-js test-rules-coverage lint-python test-python-coverage test-python test-markdownlint test-markdownlint-fix lint-readmes
4+
# Python: unit tests (test-python) vs functional tests (test-markdownlint-options, test-markdownlint-fix) are separate.
5+
ci: lint-js test-rules-coverage lint-python test-python-coverage test-python test-markdownlint test-markdownlint-options test-markdownlint-fix lint-readmes
56

67
# README linting - performs same checks as GitHub Actions workflow
78
# NOTE: This target must be kept in sync with .github/workflows/lint-readmes.yml.
@@ -149,7 +150,20 @@ test-markdownlint:
149150
}
150151
@python3 test-scripts/verify_markdownlint_fixtures.py $(if $(filter 1,$(VERBOSE)),--verbose)
151152

152-
# Markdownlint --fix functional tests - run Python tests that invoke markdownlint-cli2 --fix and assert file content.
153+
# Markdownlint rule-options functional tests - Python tests that run markdownlint with temp configs to exercise rule options.
154+
# Requires: Node.js, npm (for markdownlint-cli2); Python 3.
155+
test-markdownlint-options:
156+
@command -v node >/dev/null 2>&1 || { \
157+
echo "Error: node not found. Install Node.js and run npm install."; \
158+
exit 1; \
159+
}
160+
@command -v python3 >/dev/null 2>&1 || { \
161+
echo "Error: python3 not found. Install Python 3 to run tests."; \
162+
exit 1; \
163+
}
164+
@PYTHONPATH="$(CURDIR)/test-scripts:$${PYTHONPATH:-}" python3 -m unittest discover -s test-scripts -p "test_markdownlint_options.py" -v
165+
166+
# Markdownlint --fix functional tests - Python tests that run markdownlint-cli2 --fix and assert file content.
153167
# Requires: Node.js, npm (for markdownlint-cli2); Python 3.
154168
test-markdownlint-fix:
155169
@command -v node >/dev/null 2>&1 || { \
@@ -160,17 +174,17 @@ test-markdownlint-fix:
160174
echo "Error: python3 not found. Install Python 3 to run tests."; \
161175
exit 1; \
162176
}
163-
@python3 -m unittest discover -s test-scripts -p "test_fix_*.py" -v
177+
@PYTHONPATH="$(CURDIR)/test-scripts:$${PYTHONPATH:-}" python3 -m unittest discover -s test-scripts -p "test_fix_*.py" -v
164178

165-
# Python unit tests - same as .github/workflows/python-tests.yml
179+
# Python unit tests - test the Python code itself (verifier parsing, expectations, etc.). Same as .github/workflows/python-tests.yml
166180
# NOTE: Keep in sync with that workflow. Requires: Python 3.
167-
# Includes fix tests (test_fix_*.py) that run markdownlint-cli2 --fix and assert file content.
181+
# Only test_verify_*.py (unit). Functional tests that exercise markdownlint rules: test-markdownlint-options, test-markdownlint-fix.
168182
test-python:
169183
@command -v python3 >/dev/null 2>&1 || { \
170184
echo "Error: python3 not found. Install Python 3 to run tests."; \
171185
exit 1; \
172186
}
173-
@python3 -m unittest discover -s test-scripts -p "test_*.py" -v
187+
@PYTHONPATH="$(CURDIR)/test-scripts:$${PYTHONPATH:-}" python3 -m unittest discover -s test-scripts -p "test_verify_*.py" -v
174188

175189
# Python unit test coverage - runs tests with coverage, fails if coverage < 90%.
176190
# Requires: pip install coverage (or make venv). Sources: test-scripts/*.py (excl. test_*.py).
@@ -184,7 +198,7 @@ test-python-coverage:
184198
exit 1; \
185199
}
186200
@if [ -d .venv ]; then PATH="$(CURDIR)/.venv/bin:$$PATH"; export PATH; fi; \
187-
coverage run -m unittest discover -s test-scripts -p "test_*.py" && \
201+
PYTHONPATH="$(CURDIR)/test-scripts:$${PYTHONPATH:-}" coverage run -m unittest discover -s test-scripts -p "test_*.py" && \
188202
coverage report --include="test-scripts/*.py" --omit="test-scripts/test_*.py" --fail-under=90
189203

190204
# Python venv for lint tooling - creates .venv and installs test-scripts/requirements-lint.txt

0 commit comments

Comments
 (0)