Skip to content

Commit 614ef3e

Browse files
authored
feat: add frontmatterTitle option to heading-increment (#454)
* feat: add frontmatterTitle option to heading-increment * add tests * clarify frontmatter title as level 1 heading * add tests for frontmatterHasTitle
1 parent 1fdbd62 commit 614ef3e

File tree

6 files changed

+1264
-26
lines changed

6 files changed

+1264
-26
lines changed

docs/rules/heading-increment.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,68 @@ Goodbye World!
2525
#EEE Goodbye World!
2626
```
2727

28+
## Options
29+
30+
The following options are available on this rule:
31+
32+
* `frontmatterTitle: string` - A regex pattern to match title fields in front matter. Front matter titles matching this pattern are treated as level 1 headings. The default pattern matches YAML (`title:`), TOML (`title =`), and JSON (`"title":`) formats. Set to an empty string to disable front matter title checking.
33+
34+
Examples of **incorrect** code for this rule:
35+
36+
```markdown
37+
<!-- eslint markdown/heading-increment: "error" -->
38+
39+
---
40+
title: My Title
41+
---
42+
43+
### Heading 3 with YAML front matter
44+
```
45+
46+
```markdown
47+
<!-- eslint markdown/heading-increment: "error" -->
48+
49+
+++
50+
title = "My Title"
51+
+++
52+
53+
### Heading 3 with TOML front matter
54+
```
55+
56+
```markdown
57+
<!-- eslint markdown/heading-increment: "error" -->
58+
59+
---
60+
{ "title": "My Title" }
61+
---
62+
63+
### Heading 3 with JSON front matter
64+
```
65+
66+
Examples of **incorrect** code when configured as `"heading-increment": ["error", { frontmatterTitle: "\\s*heading\\s*[:=]" }]`:
67+
68+
```markdown
69+
<!-- eslint markdown/heading-increment: ["error", { frontmatterTitle: "\\s*heading\\s*[:=]" }] -->
70+
71+
---
72+
heading: My Title
73+
---
74+
75+
### Heading 3
76+
```
77+
78+
Examples of **correct** code when configured as `"heading-increment": ["error", { frontmatterTitle: "" }]`:
79+
80+
```markdown
81+
<!-- eslint markdown/heading-increment: ["error", { frontmatterTitle: "" }] -->
82+
83+
---
84+
title: My Title
85+
---
86+
87+
### Heading 3
88+
```
89+
2890
## When Not to Use It
2991

3092
If you aren't concerned with enforcing heading levels increment by one, you can safely disable this rule.

src/rules/heading-increment.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
* @author Nicholas C. Zakas
44
*/
55

6+
//-----------------------------------------------------------------------------
7+
// Imports
8+
//-----------------------------------------------------------------------------
9+
10+
import { frontmatterHasTitle } from "../util.js";
11+
612
//-----------------------------------------------------------------------------
713
// Type Definitions
814
//-----------------------------------------------------------------------------
915

1016
/**
1117
* @import { MarkdownRuleDefinition } from "../types.js";
1218
* @typedef {"skippedHeading"} HeadingIncrementMessageIds
13-
* @typedef {[]} HeadingIncrementOptions
19+
* @typedef {[{ frontmatterTitle?: string }]} HeadingIncrementOptions
1420
* @typedef {MarkdownRuleDefinition<{ RuleOptions: HeadingIncrementOptions, MessageIds: HeadingIncrementMessageIds }>} HeadingIncrementRuleDefinition
1521
*/
1622

@@ -33,12 +39,52 @@ export default {
3339
skippedHeading:
3440
"Heading level skipped from {{fromLevel}} to {{toLevel}}.",
3541
},
42+
43+
schema: [
44+
{
45+
type: "object",
46+
properties: {
47+
frontmatterTitle: {
48+
type: "string",
49+
},
50+
},
51+
additionalProperties: false,
52+
},
53+
],
54+
55+
defaultOptions: [
56+
{
57+
frontmatterTitle:
58+
"^(?!\\s*['\"]title[:=]['\"])\\s*\\{?\\s*['\"]?title['\"]?\\s*[:=]",
59+
},
60+
],
3661
},
3762

3863
create(context) {
64+
const [{ frontmatterTitle }] = context.options;
65+
const titlePattern =
66+
frontmatterTitle === "" ? null : new RegExp(frontmatterTitle, "iu");
3967
let lastHeadingDepth = 0;
4068

4169
return {
70+
yaml(node) {
71+
if (frontmatterHasTitle(node.value, titlePattern)) {
72+
lastHeadingDepth = 1;
73+
}
74+
},
75+
76+
toml(node) {
77+
if (frontmatterHasTitle(node.value, titlePattern)) {
78+
lastHeadingDepth = 1;
79+
}
80+
},
81+
82+
json(node) {
83+
if (frontmatterHasTitle(node.value, titlePattern)) {
84+
lastHeadingDepth = 1;
85+
}
86+
},
87+
4288
heading(node) {
4389
if (lastHeadingDepth > 0 && node.depth > lastHeadingDepth + 1) {
4490
context.report({

src/rules/no-multiple-h1.js

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Imports
88
//-----------------------------------------------------------------------------
99

10-
import { findOffsets } from "../util.js";
10+
import { findOffsets, frontmatterHasTitle } from "../util.js";
1111

1212
//-----------------------------------------------------------------------------
1313
// Type Definitions
@@ -26,25 +26,6 @@ import { findOffsets } from "../util.js";
2626

2727
const h1TagPattern = /(?<!<!--[\s\S]*?)<h1[^>]*>[\s\S]*?<\/h1>/giu;
2828

29-
/**
30-
* Checks if a frontmatter block contains a title matching the given pattern
31-
* @param {string} value The frontmatter content
32-
* @param {RegExp|null} pattern The pattern to match against
33-
* @returns {boolean} Whether a title was found
34-
*/
35-
function frontmatterHasTitle(value, pattern) {
36-
if (!pattern) {
37-
return false;
38-
}
39-
const lines = value.split("\n");
40-
for (const line of lines) {
41-
if (pattern.test(line)) {
42-
return true;
43-
}
44-
}
45-
return false;
46-
}
47-
4829
//-----------------------------------------------------------------------------
4930
// Rule Definition
5031
//-----------------------------------------------------------------------------

src/util.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,22 @@ export function findOffsets(text, offset) {
3737
columnOffset,
3838
};
3939
}
40+
41+
/**
42+
* Checks if a frontmatter block contains a title matching the given pattern
43+
* @param {string} value The frontmatter content
44+
* @param {RegExp|null} pattern The pattern to match against
45+
* @returns {boolean} Whether a title was found
46+
*/
47+
export function frontmatterHasTitle(value, pattern) {
48+
if (!pattern) {
49+
return false;
50+
}
51+
const lines = value.split("\n");
52+
for (const line of lines) {
53+
if (pattern.test(line)) {
54+
return true;
55+
}
56+
}
57+
return false;
58+
}

0 commit comments

Comments
 (0)