Skip to content

Commit 5796931

Browse files
Copilotota-meshi
andauthored
Add max-len rule to enforce configurable line length limits for Markdown entities (#224)
* Initial plan * chore: add max-len rule template Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> * feat: implement max-len rule for enforcing maximum entity lengths Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> * test: add table test cases for max-len rule Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> * refactor: address code review feedback for max-len rule - Change option names: listItem→list, tableRow→table, codeBlock→code - Replace null with "ignore" keyword for all options - Add new entity types: footnoteDefinition, html, frontmatter, math - Make all options ignorable using "ignore" keyword - Add nested configuration support for blockquote, list, footnoteDefinition - Add language-specific configuration for code and frontmatter - Fix ignoreUrls to match @stylistic/max-len behavior - Use sourceCode.getLoc() and sourceCode.lines instead of manual parsing - Remove categories from rule metadata - Update documentation with RFC status and nested options - Add rule to comparison table Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> * refactor * update doc * fix * update eslint config * docs: clarify containers vs entities and add comprehensive test coverage - Separate entity options from container options in documentation - Clarify that containers can configure all entity types, not just heading/paragraph - Add test coverage for nested container configuration - Add test coverage for language-specific code block configuration - Add test coverage for language-specific frontmatter configuration - Add test coverage for html, math, footnote definition entities - All 4293 tests passing (12 new tests added) Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> * fix doc --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ota-meshi <16508807+ota-meshi@users.noreply.github.com> Co-authored-by: yosuke ota <otameshiyo23@gmail.com>
1 parent 62bd913 commit 5796931

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1203
-4
lines changed

.changeset/max-len.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-markdown-preferences": minor
3+
---
4+
5+
feat: add `markdown-preferences/max-len` rule

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,16 @@ The rules with the following 💄 are included in the `standard` config.
216216

217217
- Rules related to visual or stylistic decorations in Markdown.
218218

219+
<!-- eslint-disable markdown-links/no-dead-urls -- Auto generated -->
220+
219221
<!-- prettier-ignore-start -->
220222

221223
| Rule ID | Description | Fixable | Config |
222224
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------- | :-----: | :----: |
223225
| [markdown-preferences/atx-heading-closing-sequence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-heading-closing-sequence-length.html) | enforce consistent length for the closing sequence (trailing #s) in ATX headings. | 🔧 | 💄 |
224226
| [markdown-preferences/atx-heading-closing-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-heading-closing-sequence.html) | enforce consistent use of closing sequence in ATX headings. | 🔧 | 💄 |
225227
| [markdown-preferences/code-fence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/code-fence-length.html) | enforce consistent code fence length in fenced code blocks. | 🔧 | 💄 |
228+
| [markdown-preferences/max-len](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/max-len.html) | enforce maximum length for various Markdown entities | | |
226229
| [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐💄 |
227230
| [markdown-preferences/ordered-list-marker-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-sequence.html) | enforce consistent ordered list marker numbering (sequential or flat) | 🔧 | 💄 |
228231
| [markdown-preferences/setext-heading-underline-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html) | enforce setext heading underline length | 🔧 | 💄 |
@@ -234,6 +237,8 @@ The rules with the following 💄 are included in the `standard` config.
234237

235238
<!-- prettier-ignore-end -->
236239

240+
<!-- eslint-enable markdown-links/no-dead-urls -- Auto generated -->
241+
237242
<!--RULES_TABLE_END-->
238243

239244
<!--RULES_SECTION_END-->

docs/appendix/comparison-with-markdownlint-rules.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,10 @@ Please note that each OSS is constantly evolving, so this list is not exhaustive
159159

160160
## Rules Related to Documents
161161

162-
| Description | [markdownlint] Rules | [@eslint/markdown] Rules | `eslint-plugin-markdown-preferences` Rules |
163-
| ----------------------------- | ----------------------------------------------------------------------------- | ------------------------ | ------------------------------------------ |
164-
| Enforce a maximum line length | [MD013] _line-length_<br>Line length | -- | -- |
165-
| Proper names | [MD044] _proper-names_<br>Proper names should have the correct capitalization | -- | -- |
162+
| Description | [markdownlint] Rules | [@eslint/markdown] Rules | `eslint-plugin-markdown-preferences` Rules |
163+
| ----------------------------- | ----------------------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------- |
164+
| Enforce a maximum line length | [MD013] _line-length_<br>Line length | -- | [markdown-preferences/max-len]<br>enforce maximum length for various Markdown entities |
165+
| Proper names | [MD044] _proper-names_<br>Proper names should have the correct capitalization | -- | -- |
166166

167167
## Rules Related to Syntax
168168

@@ -256,6 +256,7 @@ Please note that each OSS is constantly evolving, so this list is not exhaustive
256256
[markdown-preferences/link-paren-spacing]: ./../rules/link-paren-spacing.md
257257
[markdown-preferences/link-title-style]: ./../rules/link-title-style.md
258258
[markdown-preferences/list-marker-alignment]: ../rules/list-marker-alignment.md
259+
[markdown-preferences/max-len]: ./../rules/max-len.md
259260
[markdown-preferences/no-heading-trailing-punctuation]: ./../rules/no-heading-trailing-punctuation.md
260261
[markdown-preferences/no-implicit-block-closing]: ../rules/no-implicit-block-closing.md
261262
[markdown-preferences/no-laziness-blockquotes]: ../rules/no-laziness-blockquotes.md

docs/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ The rules with the following 💄 are included in the `plugin.configs.standard`
9595
| [markdown-preferences/atx-heading-closing-sequence-length](./atx-heading-closing-sequence-length.md) | enforce consistent length for the closing sequence (trailing #s) in ATX headings. | 🔧 | 💄 |
9696
| [markdown-preferences/atx-heading-closing-sequence](./atx-heading-closing-sequence.md) | enforce consistent use of closing sequence in ATX headings. | 🔧 | 💄 |
9797
| [markdown-preferences/code-fence-length](./code-fence-length.md) | enforce consistent code fence length in fenced code blocks. | 🔧 | 💄 |
98+
| [markdown-preferences/max-len](./max-len.md) | enforce maximum length for various Markdown entities | | |
9899
| [markdown-preferences/no-laziness-blockquotes](./no-laziness-blockquotes.md) | disallow laziness in blockquotes | | ⭐💄 |
99100
| [markdown-preferences/ordered-list-marker-sequence](./ordered-list-marker-sequence.md) | enforce consistent ordered list marker numbering (sequential or flat) | 🔧 | 💄 |
100101
| [markdown-preferences/setext-heading-underline-length](./setext-heading-underline-length.md) | enforce setext heading underline length | 🔧 | 💄 |

docs/rules/max-len.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "markdown-preferences/max-len"
5+
description: "enforce maximum length for various Markdown entities"
6+
---
7+
8+
# markdown-preferences/max-len
9+
10+
> enforce maximum length for various Markdown entities
11+
12+
- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## 📖 Rule Details
15+
16+
This rule enforces a configurable maximum length for different Markdown entities such as headings, paragraphs, lists, blockquotes, tables, code blocks, frontmatter, footnote definitions, HTML blocks, and math blocks.
17+
18+
Overly long Markdown elements reduce readability, make diffs harder to review, and complicate maintenance. By enforcing reasonable maximum lengths for various entities, documentation remains cleaner, easier to navigate, and more consistent across the project.
19+
20+
<!-- prettier-ignore-start -->
21+
22+
<!-- eslint-skip -->
23+
24+
```md
25+
<!-- eslint markdown-preferences/max-len: ['error', { heading: 80, paragraph: 120 }] -->
26+
27+
<!-- ✓ GOOD -->
28+
29+
## This heading is within the limit
30+
31+
This is a paragraph that fits comfortably within the specified maximum length limit.
32+
33+
- A list item that is short enough
34+
35+
<!-- ✗ BAD -->
36+
37+
## This is a very long heading that exceeds the default eighty character maximum length
38+
39+
This is an extremely long paragraph that goes on and on and on well beyond the one hundred and twenty character maximum length limit and should be broken up.
40+
41+
- This is a very long list item that definitely exceeds the default one hundred and twenty character maximum length limit and should be reported
42+
```
43+
44+
<!-- prettier-ignore-end -->
45+
46+
## 🔧 Options
47+
48+
This rule accepts an object with the following properties:
49+
50+
```json
51+
{
52+
"markdown-preferences/max-len": [
53+
"error",
54+
{
55+
"heading": 80,
56+
"paragraph": 120,
57+
"list": 120,
58+
"blockquote": 120,
59+
"table": 120,
60+
"footnoteDefinition": 120,
61+
"html": 120,
62+
"code": "ignore",
63+
"frontmatter": "ignore",
64+
"math": "ignore",
65+
"ignoreUrls": true
66+
}
67+
]
68+
}
69+
```
70+
71+
### Entity Options
72+
73+
These options control the maximum line length for specific Markdown entity types:
74+
75+
- `heading` (default: `80`): Maximum line length for headings. Set to `"ignore"` to skip checking.
76+
- `paragraph` (default: `120`): Maximum line length for paragraphs. Set to `"ignore"` to skip checking.
77+
- `table` (default: `120`): Maximum line length for tables. Set to `"ignore"` to skip checking.
78+
- `html` (default: `120`): Maximum line length for HTML blocks. Set to `"ignore"` to skip checking.
79+
- `code` (default: `"ignore"`): Maximum line length for code blocks. Set to `"ignore"` to skip checking (recommended). See [notes on code blocks](#notes-on-code-blocks) below.
80+
- `frontmatter` (default: `"ignore"`): Maximum line length for frontmatter. Set to `"ignore"` to skip checking (recommended).
81+
- `math` (default: `"ignore"`): Maximum line length for math blocks. Set to `"ignore"` to skip checking.
82+
83+
### Container Options
84+
85+
These options control line length checking for entities within container elements. Containers can either have a simple numeric limit (applied to all nested content) or an object specifying different limits for different entity types within that container:
86+
87+
- `list`: Override limits for content within lists. When not specified, nested content inherits from entity-level defaults.
88+
- `blockquote`: Override limits for content within blockquotes. When not specified, nested content inherits from entity-level defaults.
89+
- `footnoteDefinition`: Override limits for content within footnote definitions. When not specified, nested content inherits from entity-level defaults.
90+
91+
#### Nested Configuration for Containers
92+
93+
For `list`, `blockquote`, and `footnoteDefinition`, you can specify different limits for any entity types within these containers (including `heading`, `paragraph`, `table`, `html`, `math`, `code`, and `frontmatter`):
94+
95+
```json
96+
{
97+
"markdown-preferences/max-len": [
98+
"error",
99+
{
100+
"heading": 80,
101+
"paragraph": 120,
102+
"table": 100,
103+
"blockquote": {
104+
"heading": 70,
105+
"paragraph": 100,
106+
"table": 90
107+
}
108+
}
109+
]
110+
}
111+
```
112+
113+
In this example:
114+
115+
- Standalone headings are limited to 80 characters, but headings inside blockquotes are limited to 70 characters
116+
- Standalone tables are limited to 100 characters, but tables inside blockquotes are limited to 90 characters
117+
- All entity types (`heading`, `paragraph`, `table`, `html`, `math`, `code`, `frontmatter`) can be configured within containers
118+
119+
### Language-specific Code Block Configuration
120+
121+
For `code` and `frontmatter`, you can specify different limits per language:
122+
123+
```json
124+
{
125+
"markdown-preferences/max-len": [
126+
"error",
127+
{
128+
"code": {
129+
"javascript": 100,
130+
"python": 80,
131+
"shell": "ignore"
132+
},
133+
"frontmatter": {
134+
"yaml": 120,
135+
"toml": "ignore"
136+
}
137+
}
138+
]
139+
}
140+
```
141+
142+
### URL Handling
143+
144+
- `ignoreUrls` (default: `true`): When enabled, lines containing URLs are ignored.
145+
146+
This option works similarly to [`@stylistic/eslint-plugin`'s `max-len` rule](https://eslint.style/rules/max-len#ignoreurls).
147+
148+
### Notes on Code Blocks
149+
150+
**Note:** Code blocks are ignored by default (`code: "ignore"`) because:
151+
152+
1. Code inside code blocks should be linted using language-specific linters via ESLint's language plugins
153+
2. The [`@eslint/markdown`](https://github.com/eslint/markdown) plugin supports linting code blocks as their respective languages
154+
3. This rule cannot understand the syntax inside code blocks (e.g., comments, strings)
155+
156+
If you need to enforce line length for code blocks in Markdown, you can:
157+
158+
- Use the language-specific ESLint configuration for code blocks (recommended):
159+
160+
```js
161+
{
162+
files: ["**/*.md/*.js"],
163+
rules: {
164+
"@stylistic/max-len": ["error", { "ignoreComments": true }]
165+
}
166+
}
167+
```
168+
169+
**However**, this feature is not currently available with this plugin. It may be possible in the future. The RFC is at: <https://github.com/eslint/rfcs/pull/105>
170+
171+
- Set the `code` option to enforce a simple line length limit (not syntax-aware)
172+
173+
For more information, see the [advanced configuration guide](https://github.com/eslint/markdown/blob/main/docs/processors/markdown.md#advanced-configuration).
174+
175+
## 📚 Further Reading
176+
177+
- [Markdownlint MD013](https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md013.md) - Similar rule in markdownlint
178+
- [@stylistic/eslint-plugin max-len](https://eslint.style/rules/max-len) - ESLint stylistic rule for code
179+
180+
## 👫 Related Rules
181+
182+
- [no-multiple-empty-lines](./no-multiple-empty-lines.md) - Disallow multiple empty lines
183+
184+
## 🔍 Implementation
185+
186+
<!-- eslint-disable markdown-links/no-dead-urls -- Auto generated -->
187+
188+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/blob/main/src/rules/max-len.ts)
189+
- [Test source](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/blob/main/tests/src/rules/max-len.ts)
190+
- [Test fixture sources](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/tree/main/tests/fixtures/rules/max-len)
191+
192+
<!-- eslint-enable markdown-links/no-dead-urls -- Auto generated -->

eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export default defineConfig([
158158
...Object.fromEntries(
159159
rules.map((rule) => [rule.meta.docs.ruleId, "error"]),
160160
),
161+
"markdown-preferences/max-len": "off", // 今は無効化します
161162
"markdown-preferences/prefer-linked-words": [
162163
"error",
163164
{
@@ -197,8 +198,12 @@ export default defineConfig([
197198
"/^https:\\/\\/www\\.npmtrends\\.com\\//u",
198199
],
199200
allowedAnchors: {
201+
// https://eslint-online-playground.netlify.app/
200202
"/^https:\\/\\/eslint-online-playground\\.netlify\\.app\\//u":
201203
"/.*/u",
204+
// https://github.com/eslint/markdown/blob/main/docs/processors/markdown.md
205+
"/^https:\\/\\/github\\.com\\/eslint\\/markdown\\/blob\\/main\\/docs\\/processors\\/markdown\\.md/u":
206+
"/.*/u",
202207
},
203208
},
204209
],

src/rule-types.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ export interface RuleOptions {
139139
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html
140140
*/
141141
'markdown-preferences/list-marker-alignment'?: Linter.RuleEntry<MarkdownPreferencesListMarkerAlignment>
142+
/**
143+
* enforce maximum length for various Markdown entities
144+
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/max-len.html
145+
*/
146+
'markdown-preferences/max-len'?: Linter.RuleEntry<MarkdownPreferencesMaxLen>
142147
/**
143148
* disallow trailing punctuation in headings.
144149
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-heading-trailing-punctuation.html
@@ -433,6 +438,60 @@ type MarkdownPreferencesLinkTitleStyle = []|[{
433438
type MarkdownPreferencesListMarkerAlignment = []|[{
434439
align?: ("left" | "right")
435440
}]
441+
// ----- markdown-preferences/max-len -----
442+
type MarkdownPreferencesMaxLen = []|[{
443+
heading?: (number | "ignore")
444+
paragraph?: (number | "ignore")
445+
table?: (number | "ignore")
446+
html?: (number | "ignore")
447+
math?: (number | "ignore")
448+
code?: ((number | "ignore") | {
449+
[k: string]: (number | "ignore")
450+
})
451+
frontmatter?: ((number | "ignore") | {
452+
[k: string]: (number | "ignore")
453+
})
454+
list?: ((number | "ignore") | {
455+
heading?: (number | "ignore")
456+
paragraph?: (number | "ignore")
457+
table?: (number | "ignore")
458+
html?: (number | "ignore")
459+
math?: (number | "ignore")
460+
code?: ((number | "ignore") | {
461+
[k: string]: (number | "ignore")
462+
})
463+
frontmatter?: ((number | "ignore") | {
464+
[k: string]: (number | "ignore")
465+
})
466+
})
467+
blockquote?: ((number | "ignore") | {
468+
heading?: (number | "ignore")
469+
paragraph?: (number | "ignore")
470+
table?: (number | "ignore")
471+
html?: (number | "ignore")
472+
math?: (number | "ignore")
473+
code?: ((number | "ignore") | {
474+
[k: string]: (number | "ignore")
475+
})
476+
frontmatter?: ((number | "ignore") | {
477+
[k: string]: (number | "ignore")
478+
})
479+
})
480+
footnoteDefinition?: ((number | "ignore") | {
481+
heading?: (number | "ignore")
482+
paragraph?: (number | "ignore")
483+
table?: (number | "ignore")
484+
html?: (number | "ignore")
485+
math?: (number | "ignore")
486+
code?: ((number | "ignore") | {
487+
[k: string]: (number | "ignore")
488+
})
489+
frontmatter?: ((number | "ignore") | {
490+
[k: string]: (number | "ignore")
491+
})
492+
})
493+
ignoreUrls?: boolean
494+
}]
436495
// ----- markdown-preferences/no-heading-trailing-punctuation -----
437496
type MarkdownPreferencesNoHeadingTrailingPunctuation = []|[{
438497
punctuation?: (string | {

0 commit comments

Comments
 (0)