Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/markdownlint-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: Markdownlint tests

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

on:
push:
Expand All @@ -10,12 +11,16 @@ on:
- '.markdownlint.yml'
- 'markdownlint-rules/**'
- 'md_test_files/**'
- 'test-scripts/test_fix_*.py'
- 'test-scripts/markdownlint_config_helper.py'
pull_request:
paths:
- '.markdownlint-cli2.jsonc'
- '.markdownlint.yml'
- 'markdownlint-rules/**'
- 'md_test_files/**'
- 'test-scripts/test_fix_*.py'
- 'test-scripts/markdownlint_config_helper.py'

jobs:
markdownlint-tests:
Expand Down Expand Up @@ -43,3 +48,6 @@ jobs:

- name: Run markdownlint fixture tests
run: make test-markdownlint

- name: Run markdownlint --fix functional tests
run: make test-markdownlint-fix
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Directories
.vscode/
.cursor/
.venv/
node_modules/
.vscode/
Abacus.ai*/
dev_docs/
node_modules/
tmp/

# File Patterns
Expand Down
3 changes: 2 additions & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"markdownlint-rules/no-duplicate-headings-normalized.js",
"markdownlint-rules/no-empty-heading.js",
"markdownlint-rules/no-h1-content.js",
"markdownlint-rules/no-heading-like-lines.js"
"markdownlint-rules/no-heading-like-lines.js",
"markdownlint-rules/one-sentence-per-line.js"
],
"ignores": [
".github/**",
Expand Down
161 changes: 116 additions & 45 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@

default: true

# MD013/line-length - line length limits (non-default)
# MD013/line-length - line length limits (built-in; default off when overridden).
# Options:
# line_length: number (default 80); max characters per line for body
# heading_line_length: number (default 80); max for heading lines
# code_block_line_length: number (default 80); max for code block lines
# code_blocks: bool (default true); include code blocks in checks
# tables: bool (default true); include tables in checks
# headings: bool (default true); include headings in checks
# strict: bool (default false); enforce length even when no whitespace beyond limit
# stern: bool (default false); warn for fixable long lines but allow long lines without spaces
MD013:
line_length: 500
heading_line_length: 500
Expand All @@ -14,21 +23,23 @@ MD013:
strict: false
stern: false

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

# 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: disallow non-ASCII except in configured paths; inline code (backticks) always stripped. Fixable when replacement in unicodeReplacements.
# Options (all optional):
# allowedEmoji: list of chars/emoji allowed in paths matching allowedPathPatternsEmoji (multi-codepoint OK)
# allowedPathPatternsEmoji: list of globs; files where only allowedEmoji chars are allowed
# allowedPathPatternsUnicode: list of globs; files where any non-ASCII is allowed
# allowedUnicode: list of single-char strings; allowed in all files (extends default é, ï, ñ, etc. unless replace below)
# allowedUnicodeReplaceDefault: bool (default false); true = use only allowedUnicode list, no built-in default
# allowUnicodeInCodeBlocks: bool (default true); false = check inside fenced blocks
# disallowUnicodeInCodeBlockTypes: list of block types (e.g. "text", "bash"); when allowUnicodeInCodeBlocks false, only these checked; empty = all
# excludePathPatterns: list of globs; skip this rule for matching file paths
# unicodeReplacements: object or [[char, replacement], ...]; built-in defaults (arrows, quotes, etc.) if omitted
ascii-only:
allowedPathPatternsUnicode:
# - "**/README.md"
Expand All @@ -43,31 +54,54 @@ ascii-only:
- "📊"
- "⚠️"

# document-length: disallow documents longer than maximum lines (default 1500)
# - maximum: positive integer; default 1500
# document-length: disallow documents longer than maximum lines.
# Options:
# maximum: number (default 1500); max allowed line count; must be positive integer
# excludePathPatterns: list of globs; skip this rule for matching file paths
document-length:
maximum: 1500

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

# no-h1-content: under first h1 allow only TOC (blank, list-of-links, HTML comments)
# - excludePathPatterns: glob list; skip this rule for matching paths (e.g. md_test_files for fixtures)
# no-heading-like-lines: report lines that look like headings but are not (e.g. **Text:**, 1. **Text**, MD036-style **Intro**). Fixable.
# Options (all optional):
# convertToHeading: bool (default false); when true, fix converts to ATX heading; when false, fix strips emphasis to plain text
# defaultHeadingLevel: number 1-6 (default 2); used when converting to heading and there is no preceding heading
# fixedHeadingLevel: number 1-6; when set, suggested heading uses this level and ignores context
# punctuationMarks: string (default ".,;!?"); for whole-line emphasis, skip when content ends with one of these (no colon = catch colon lines)
# excludePathPatterns: list of globs; skip this rule for matching file paths
# With convertToHeading true, heading-title-case.js and heading-numbering.js in same dir (if present) add AP title case and numbering.
# no-heading-like-lines:
# convertToHeading: false
# defaultHeadingLevel: 2
# # fixedHeadingLevel: 3
# # excludePathPatterns: ["**/README.md"]

# no-h1-content: under first h1 allow only TOC (blank lines, list-of-links, badges, HTML comments including multi-line).
# Options:
# excludePathPatterns: list of globs; skip this rule for matching file paths
# no-h1-content:
# excludePathPatterns:
# - "README.md"
# - "CONTRIBUTING.md"
# - "**/README.md"

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

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

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

# 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: allow only configured <a id="..."></a> anchor id patterns; optional placement per pattern.
# Options:
# allowedIdPatterns: array (required); each entry regex string or { pattern: string, placement?: object }
# placement: headingMatch (regex), lineMatch (regex), standaloneLine (bool), requireAfter (["blank","fencedBlock","list"]),
# anchorImmediatelyAfterHeading (bool), maxPerSection (number)
# strictPlacement: bool (default true); when true, enforce placement when the matching pattern has placement set
# excludePathPatterns: list of globs; skip this rule for matching file paths
allow-custom-anchors:
allowedIdPatterns:
- pattern: "^spec-[a-z0-9-]+$"
Expand All @@ -138,4 +180,33 @@ allow-custom-anchors:
anchorImmediatelyAfterHeading: true
maxPerSection: 1
strictPlacement: true

# heading-numbering: enforce numeric outline (e.g. 1, 1.1, 1.2). Fixable. Not configured here; options documented for reference.
# Options (all optional):
# maxHeadingLevel: number 1-6 (default 6); headings at or below this level must be numbered
# maxSegmentValue: number (default 99); max value for any segment (e.g. 99 => 1.99.3 allowed)
# maxSegmentValueMinLevel: number 1-6 (default 2); min heading level for maxSegmentValue cap
# maxSegmentValueMaxLevel: number 1-6 (default 6); max heading level for maxSegmentValue cap
# excludePathPatterns: list of globs; skip this rule for matching file paths
# heading-numbering:
# maxHeadingLevel: 6
# maxSegmentValue: 99
# maxSegmentValueMinLevel: 2
# maxSegmentValueMaxLevel: 6
# # excludePathPatterns: ["**/README.md"]

# no-duplicate-headings-normalized: disallow duplicate heading text after normalizing (whitespace/case). Not configured here.
# Options:
# excludePathPatterns: list of globs; skip this rule for matching file paths
# no-duplicate-headings-normalized:
# # excludePathPatterns: ["**/README.md"]

# one-sentence-per-line: enforce one sentence per line in prose and list content. Fixable (repeated Fix all).
# Options (all optional):
# continuationIndent: number (default 4); spaces for paragraph continuation lines
# strictAbbreviations: list of strings; abbreviations that do not end a sentence (extends/replaces default)
# excludePathPatterns: list of globs; skip this rule for matching file paths
# one-sentence-per-line:
# continuationIndent: 4
# # excludePathPatterns: ["**/README.md"]
...
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"./markdownlint-rules/no-duplicate-headings-normalized.js",
"./markdownlint-rules/no-empty-heading.js",
"./markdownlint-rules/no-h1-content.js",
"./markdownlint-rules/no-heading-like-lines.js"
"./markdownlint-rules/no-heading-like-lines.js",
"./markdownlint-rules/one-sentence-per-line.js"
]
}
14 changes: 9 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ npm install

Run the Makefile targets below so your PR stays green.

## Checks - Makefile Targets
## Checks - `Makefile` Targets

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

### Lint Rule JavaScript (`make lint-js`)

Expand All @@ -57,7 +58,8 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi
make test-markdownlint
```

When adding or changing a custom rule, add or update a `negative_*.md` fixture so the intended violation is covered. See [md_test_files/README.md](md_test_files/README.md) for which file exercises which rule.
When adding or changing a custom rule, add or update a `negative_*.md` fixture so the intended violation is covered.
See [md_test_files/README.md](md_test_files/README.md) for which file exercises which rule.

### Rule Unit Tests (`make test-rules`)

Expand Down Expand Up @@ -109,8 +111,10 @@ 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

Expand Down
Loading