Skip to content

Commit 4d504e5

Browse files
authored
feat: add checkFootnoteDefinitions option to no-empty-definitions (#442)
* feat: add checkFootnoteDefinitions option to no-empty-definitions * correctly detect empty footnote definitions with only HTML comments * use `(default: `true`)` in docs
1 parent e9b9978 commit 4d504e5

File tree

3 files changed

+234
-7
lines changed

3 files changed

+234
-7
lines changed

docs/rules/no-empty-definitions.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@ Disallow empty definitions.
44

55
## Background
66

7-
Markdown allows you to specify a label as a placeholder for a URL in both links and images using square brackets, such as:
7+
Markdown allows you to specify a label as a placeholder for a URL in both links and images, or as a footnote reference, using square brackets. For example:
88

99
```markdown
1010
[ESLint][eslint]
1111

1212
[eslint]: https://eslint.org
13+
14+
[ESLint][^eslint]
15+
16+
[^eslint]: Find and fix problmes in your JavaScript code
1317
```
1418

15-
If the definition's URL is empty or only contains an empty fragment (`#`), then it's not providing any useful information and could be a mistake.
19+
Definitions with an empty URL or only an empty fragment (`#`), as well as footnote definitions with no content, are usually mistakes and do not provide useful information.
1620

1721
## Rule Details
1822

19-
This rule warns when it finds definitions where the URL is either not specified or contains only an empty fragment (`#`).
23+
> [!IMPORTANT] <!-- eslint-disable-line -- This should be fixed in https://github.com/eslint/markdown/issues/294 -->
24+
>
25+
> Footnotes are only supported when using `language` mode [`markdown/gfm`](/README.md#languages).
26+
27+
This rule warns when it finds definitions where the URL is either not specified or contains only an empty fragment (`#`). It also warns for empty footnote definitions by default.
2028

2129
Examples of **incorrect** code for this rule:
2230

@@ -25,15 +33,31 @@ Examples of **incorrect** code for this rule:
2533

2634
[earth]: <>
2735
[moon]: #
36+
[^note]:
2837
```
2938

30-
Examples of correct code:
39+
Examples of **correct** code for this rule:
3140

3241
```markdown
3342
<!-- eslint markdown/no-empty-definitions: "error" -->
3443

3544
[earth]: https://example.com/earth/
3645
[moon]: #section
46+
[^note]: This is a footnote.
47+
```
48+
49+
## Options
50+
51+
The following options are available on this rule:
52+
53+
* `checkFootnoteDefinitions: boolean` - When set to `false`, the rule will not report empty footnote definitions. (default: `true`).
54+
55+
Examples of **correct** code for this rule with `checkFootnoteDefinitions: false`:
56+
57+
```markdown
58+
<!-- eslint markdown/no-empty-definitions: ["error", { checkFootnoteDefinitions: false }] -->
59+
60+
[^note]:
3761
```
3862

3963
## When Not to Use It

src/rules/no-empty-definitions.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,29 @@
88
//-----------------------------------------------------------------------------
99

1010
/**
11-
* @typedef {import("../types.ts").MarkdownRuleDefinition<{ RuleOptions: []; }>}
12-
* NoEmptyDefinitionsRuleDefinition
11+
* @import { MarkdownRuleDefinition } from "../types.js";
12+
* @typedef {"emptyDefinition" | "emptyFootnoteDefinition"} NoEmptyDefinitionsMessageIds
13+
* @typedef {[{ checkFootnoteDefinitions?: boolean }]} NoEmptyDefinitionsOptions
14+
* @typedef {MarkdownRuleDefinition<{ RuleOptions: NoEmptyDefinitionsOptions, MessageIds: NoEmptyDefinitionsMessageIds }>} NoEmptyDefinitionsRuleDefinition
1315
*/
1416

17+
//-----------------------------------------------------------------------------
18+
// Helpers
19+
//-----------------------------------------------------------------------------
20+
21+
const htmlCommentPattern = /<!--[\s\S]*?-->/gu;
22+
23+
/**
24+
* Checks if a string contains only HTML comments.
25+
* @param {string} value The input string to check.
26+
* @returns {boolean} True if the string contains only HTML comments, false otherwise.
27+
*/
28+
function isOnlyComments(value) {
29+
const withoutComments = value.replace(htmlCommentPattern, "");
30+
31+
return withoutComments.trim().length === 0;
32+
}
33+
1534
//-----------------------------------------------------------------------------
1635
// Rule Definition
1736
//-----------------------------------------------------------------------------
@@ -29,10 +48,28 @@ export default {
2948

3049
messages: {
3150
emptyDefinition: "Unexpected empty definition found.",
51+
emptyFootnoteDefinition:
52+
"Unexpected empty footnote definition found.",
3253
},
54+
55+
schema: [
56+
{
57+
type: "object",
58+
properties: {
59+
checkFootnoteDefinitions: {
60+
type: "boolean",
61+
},
62+
},
63+
additionalProperties: false,
64+
},
65+
],
66+
67+
defaultOptions: [{ checkFootnoteDefinitions: true }],
3368
},
3469

3570
create(context) {
71+
const [{ checkFootnoteDefinitions }] = context.options;
72+
3673
return {
3774
definition(node) {
3875
if (!node.url || node.url === "#") {
@@ -42,6 +79,23 @@ export default {
4279
});
4380
}
4481
},
82+
83+
footnoteDefinition(node) {
84+
if (
85+
checkFootnoteDefinitions &&
86+
(node.children.length === 0 ||
87+
node.children.every(
88+
child =>
89+
child.type === "html" &&
90+
isOnlyComments(child.value),
91+
))
92+
) {
93+
context.report({
94+
loc: node.position,
95+
messageId: "emptyFootnoteDefinition",
96+
});
97+
}
98+
},
4599
};
46100
},
47101
};

tests/rules/no-empty-definitions.test.js

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const ruleTester = new RuleTester({
2020
plugins: {
2121
markdown,
2222
},
23-
language: "markdown/commonmark",
23+
language: "markdown/gfm",
2424
});
2525

2626
ruleTester.run("no-empty-definitions", rule, {
@@ -29,6 +29,28 @@ ruleTester.run("no-empty-definitions", rule, {
2929
"[foo]: #bar",
3030
"[foo]: http://bar.com",
3131
"[foo]: <https://bar.com>",
32+
"[^note]: This is a footnote.",
33+
"[^note]: ![]()",
34+
"[^note]: [text](url)",
35+
"[^note]:\n Content",
36+
"[^note]:\n > blockquote",
37+
"[^note]: <span></span>",
38+
"\\[^note]:",
39+
"[\\^note]:",
40+
"[^note\\]:",
41+
"[^note]\\:",
42+
"[^foo]: <span></span> <!-- comment -->",
43+
"[^foo]: content <!-- comment -->",
44+
"[^foo]: <!-- comment --> content",
45+
"[^foo]: <!-- comment --> content <!-- comment -->",
46+
dedent`
47+
[^foo]: <!-- comm
48+
ent --> content <!-- comment -->
49+
`,
50+
{
51+
code: "[^note]:",
52+
options: [{ checkFootnoteDefinitions: false }],
53+
},
3254
],
3355
invalid: [
3456
{
@@ -77,5 +99,132 @@ ruleTester.run("no-empty-definitions", rule, {
7799
},
78100
],
79101
},
102+
{
103+
code: "[^note]:",
104+
errors: [
105+
{
106+
messageId: "emptyFootnoteDefinition",
107+
line: 1,
108+
column: 1,
109+
endLine: 1,
110+
endColumn: 9,
111+
},
112+
],
113+
},
114+
{
115+
code: "[^note]: ",
116+
errors: [
117+
{
118+
messageId: "emptyFootnoteDefinition",
119+
line: 1,
120+
column: 1,
121+
endLine: 1,
122+
endColumn: 12,
123+
},
124+
],
125+
},
126+
{
127+
code: "[^note]:\n",
128+
errors: [
129+
{
130+
messageId: "emptyFootnoteDefinition",
131+
line: 1,
132+
column: 1,
133+
endLine: 1,
134+
endColumn: 9,
135+
},
136+
],
137+
},
138+
{
139+
code: "[^a]:\n[^b]:",
140+
errors: [
141+
{
142+
messageId: "emptyFootnoteDefinition",
143+
line: 1,
144+
column: 1,
145+
endLine: 1,
146+
endColumn: 6,
147+
},
148+
{
149+
messageId: "emptyFootnoteDefinition",
150+
line: 2,
151+
column: 1,
152+
endLine: 2,
153+
endColumn: 6,
154+
},
155+
],
156+
},
157+
{
158+
code: "[foo]: #\n[^note]:",
159+
errors: [
160+
{
161+
messageId: "emptyDefinition",
162+
line: 1,
163+
column: 1,
164+
endLine: 1,
165+
endColumn: 9,
166+
},
167+
{
168+
messageId: "emptyFootnoteDefinition",
169+
line: 2,
170+
column: 1,
171+
endLine: 2,
172+
endColumn: 9,
173+
},
174+
],
175+
},
176+
{
177+
code: "[foo]: #\n[^note]:",
178+
options: [{ checkFootnoteDefinitions: false }],
179+
errors: [
180+
{
181+
messageId: "emptyDefinition",
182+
line: 1,
183+
column: 1,
184+
endLine: 1,
185+
endColumn: 9,
186+
},
187+
],
188+
},
189+
{
190+
code: "[^foo]: <!-- comment -->",
191+
errors: [
192+
{
193+
messageId: "emptyFootnoteDefinition",
194+
line: 1,
195+
column: 1,
196+
endLine: 1,
197+
endColumn: 25,
198+
},
199+
],
200+
},
201+
{
202+
code: dedent`
203+
[^foo]: <!-- comment
204+
-->`,
205+
errors: [
206+
{
207+
messageId: "emptyFootnoteDefinition",
208+
line: 1,
209+
column: 1,
210+
endLine: 2,
211+
endColumn: 8,
212+
},
213+
],
214+
},
215+
{
216+
code: dedent`
217+
[^foo]: <!-- comment -->
218+
<!-- another comment -->`,
219+
errors: [
220+
{
221+
messageId: "emptyFootnoteDefinition",
222+
line: 1,
223+
column: 1,
224+
endLine: 2,
225+
endColumn: 29,
226+
},
227+
],
228+
},
80229
],
81230
});

0 commit comments

Comments
 (0)