Skip to content

Commit 450cdb8

Browse files
committed
feat: support columns numbers (and line range) in links fragments
Fixes #7
1 parent 2df95e9 commit 450cdb8

File tree

7 files changed

+123
-23
lines changed

7 files changed

+123
-23
lines changed

src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
isValidIntegerString,
1212
getNumberOfLines,
1313
getLineNumberStringFromFragment,
14+
lineFragmentRe,
1415
} = require("./utils.js")
1516

1617
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
@@ -133,6 +134,10 @@ const customRule = {
133134

134135
const hasOnlyDigits = isValidIntegerString(lineNumberFragmentString)
135136
if (!hasOnlyDigits) {
137+
if (lineFragmentRe.test(url.hash)) {
138+
continue
139+
}
140+
136141
onError({
137142
lineNumber,
138143
detail: `${detail} should have a valid fragment identifier`,

src/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const { getHtmlAttributeRe } = require("./markdownlint-rule-helpers/helpers.js")
44

55
const markdownIt = new MarkdownIt({ html: true })
66

7+
const lineFragmentRe = /^#(?:L\d+(?:C\d+)?-L\d+(?:C\d+)?|L\d+)$/
8+
79
/**
810
* Converts a Markdown heading into an HTML fragment according to the rules
911
* used by GitHub.
@@ -151,6 +153,7 @@ const getLineNumberStringFromFragment = (fragment) => {
151153
}
152154

153155
module.exports = {
156+
lineFragmentRe,
154157
convertHeadingToHTMLFragment,
155158
getMarkdownHeadings,
156159
getMarkdownIdOrAnchorNameFragments,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Awesome
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Invalid
2+
3+
[Invalid](./awesome.md#L12-not-a-line-link)
4+
5+
[Invalid](./awesome.md#l7)
6+
7+
[Invalid](./awesome.md#L)
8+
9+
[Invalid](./awesome.md#L7extra)
10+
11+
[Invalid](./awesome.md#L30C)
12+
13+
[Invalid](./awesome.md#L30Cextra)
14+
15+
[Invalid](./awesome.md#L30L12)
16+
17+
[Invalid](./awesome.md#L30C12)
18+
19+
[Invalid](./awesome.md#L30C11-)
20+
21+
[Invalid](./awesome.md#L30C11-L)
22+
23+
[Invalid](./awesome.md#L30C11-L31C)
24+
25+
[Invalid](./awesome.md#L30C11-C31)
26+
27+
[Invalid](./awesome.md#C30)
28+
29+
[Invalid](./awesome.md#C11-C31)
30+
31+
[Invalid](./awesome.md#C11-L4C31)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Awesome
2+
3+
## L12 Not A Line Link
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Valid
2+
3+
[Valid](./awesome.md#l12-not-a-line-link)
4+
5+
[Valid](./awesome.md#L30-L31)
6+
7+
[Valid](./awesome.md#L3C24-L88)
8+
9+
[Valid](./awesome.md#L304-L314C98)
10+
11+
[Valid](./awesome.md#L200C4-L3244C2)

test/index.test.js

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,92 +29,126 @@ test("ensure the rule validates correctly", async (t) => {
2929
name: "should be invalid with an empty id fragment",
3030
fixturePath:
3131
"test/fixtures/invalid/empty-id-fragment/empty-id-fragment.md",
32-
error: '"./awesome.md#" should have a valid fragment identifier',
32+
errors: ['"./awesome.md#" should have a valid fragment identifier'],
3333
},
3434
{
3535
name: "should be invalid with a name fragment other than for an anchor",
3636
fixturePath:
3737
"test/fixtures/invalid/ignore-name-fragment-if-not-an-anchor/ignore-name-fragment-if-not-an-anchor.md",
38-
error:
38+
errors: [
3939
'"./awesome.md#name-should-be-ignored" should have a valid fragment identifier',
40+
],
4041
},
4142
{
4243
name: "should be invalid with a non-existing id fragment (data-id !== id)",
4344
fixturePath:
4445
"test/fixtures/invalid/ignore-not-an-id-fragment/ignore-not-an-id-fragment.md",
45-
error:
46+
errors: [
4647
'"./awesome.md#not-an-id-should-be-ignored" should have a valid fragment identifier',
48+
],
4749
},
4850
{
4951
name: "should be invalid with invalid heading with #L fragment",
5052
fixturePath:
5153
"test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md",
52-
error: '"./awesome.md#L7abc" should have a valid fragment identifier',
54+
errors: [
55+
'"./awesome.md#L7abc" should have a valid fragment identifier',
56+
],
57+
},
58+
{
59+
name: "should be invalid with a invalid line column range number fragment",
60+
fixturePath:
61+
"test/fixtures/invalid/invalid-line-column-range-number-fragment/invalid-line-column-range-number-fragment.md",
62+
errors: [
63+
'"./awesome.md#L12-not-a-line-link" should have a valid fragment identifier',
64+
'"./awesome.md#l7" should have a valid fragment identifier',
65+
'"./awesome.md#L" should have a valid fragment identifier',
66+
'"./awesome.md#L7extra" should have a valid fragment identifier',
67+
'"./awesome.md#L30C" should have a valid fragment identifier',
68+
'"./awesome.md#L30Cextra" should have a valid fragment identifier',
69+
'"./awesome.md#L30L12" should have a valid fragment identifier',
70+
'"./awesome.md#L30C12" should have a valid fragment identifier',
71+
'"./awesome.md#L30C11-" should have a valid fragment identifier',
72+
'"./awesome.md#L30C11-L" should have a valid fragment identifier',
73+
'"./awesome.md#L30C11-L31C" should have a valid fragment identifier',
74+
'"./awesome.md#L30C11-C31" should have a valid fragment identifier',
75+
'"./awesome.md#C30" should have a valid fragment identifier',
76+
'"./awesome.md#C11-C31" should have a valid fragment identifier',
77+
'"./awesome.md#C11-L4C31" should have a valid fragment identifier',
78+
],
5379
},
5480
{
5581
name: "should be invalid with a invalid line number fragment",
5682
fixturePath:
5783
"test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md",
58-
error:
84+
errors: [
5985
'"./awesome.md#L7" should have a valid fragment identifier, "./awesome.md#L7" should have at least 7 lines to be valid',
86+
],
6087
},
6188
{
6289
name: "should be invalid with a non-existing anchor name fragment",
6390
fixturePath:
6491
"test/fixtures/invalid/non-existing-anchor-name-fragment/non-existing-anchor-name-fragment.md",
65-
error:
92+
errors: [
6693
'"./awesome.md#non-existing-anchor-name-fragment" should have a valid fragment identifier',
94+
],
6795
},
6896
{
6997
name: "should be invalid with a non-existing element id fragment",
7098
fixturePath:
7199
"test/fixtures/invalid/non-existing-element-id-fragment/non-existing-element-id-fragment.md",
72-
error:
100+
errors: [
73101
'"./awesome.md#non-existing-element-id-fragment" should have a valid fragment identifier',
102+
],
74103
},
75104
{
76105
name: "should be invalid with a non-existing heading fragment",
77106
fixturePath:
78107
"test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
79-
error:
108+
errors: [
80109
'"./awesome.md#non-existing-heading" should have a valid fragment identifier',
110+
],
81111
},
82112
{
83113
name: "should be invalid with a link to an image with a empty fragment",
84114
fixturePath:
85115
"test/fixtures/invalid/ignore-empty-fragment-checking-for-image.md",
86-
error:
116+
errors: [
87117
'"../image.png#" should not have a fragment identifier as it is an image',
118+
],
88119
},
89120
{
90121
name: "should be invalid with a link to an image with a fragment",
91122
fixturePath:
92123
"test/fixtures/invalid/ignore-fragment-checking-for-image.md",
93-
error:
124+
errors: [
94125
'"../image.png#non-existing-fragment" should not have a fragment identifier as it is an image',
126+
],
95127
},
96128
{
97129
name: "should be invalid with a non-existing file",
98130
fixturePath: "test/fixtures/invalid/non-existing-file.md",
99-
error: '"./index.test.js" should exist in the file system',
131+
errors: ['"./index.test.js" should exist in the file system'],
100132
},
101133
{
102134
name: "should be invalid with a non-existing image",
103135
fixturePath: "test/fixtures/invalid/non-existing-image.md",
104-
error: '"./image.png" should exist in the file system',
136+
errors: ['"./image.png" should exist in the file system'],
105137
},
106138
]
107139

108-
for (const { name, fixturePath, error } of testCases) {
140+
for (const { name, fixturePath, errors } of testCases) {
109141
await t.test(name, async () => {
110-
const lintResults = await validateMarkdownLint(fixturePath)
111-
assert.equal(lintResults?.length, 1)
112-
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names)
113-
assert.equal(
114-
lintResults?.[0]?.ruleDescription,
115-
relativeLinksRule.description,
116-
)
117-
assert.equal(lintResults?.[0]?.errorDetail, error)
142+
const lintResults = (await validateMarkdownLint(fixturePath)) ?? []
143+
const errorsDetails = lintResults.map((result) => {
144+
assert.deepEqual(result.ruleNames, relativeLinksRule.names)
145+
assert.deepEqual(
146+
result.ruleDescription,
147+
relativeLinksRule.description,
148+
)
149+
return result.errorDetail
150+
})
151+
assert.deepStrictEqual(errorsDetails, errors)
118152
})
119153
}
120154
})
@@ -151,6 +185,11 @@ test("ensure the rule validates correctly", async (t) => {
151185
fixturePath:
152186
"test/fixtures/valid/only-parse-markdown-files-for-fragments/only-parse-markdown-files-for-fragments.md",
153187
},
188+
{
189+
name: "should support lines and columns range numbers in link fragments",
190+
fixturePath:
191+
"test/fixtures/valid/valid-line-column-range-number-fragment/valid-line-column-range-number-fragment.md",
192+
},
154193
{
155194
name: 'should be valid with valid heading "like" line number fragment',
156195
fixturePath:
@@ -186,8 +225,15 @@ test("ensure the rule validates correctly", async (t) => {
186225

187226
for (const { name, fixturePath } of testCases) {
188227
await t.test(name, async () => {
189-
const lintResults = await validateMarkdownLint(fixturePath)
190-
assert.equal(lintResults?.length, 0)
228+
const lintResults = (await validateMarkdownLint(fixturePath)) ?? []
229+
const errorsDetails = lintResults.map((result) => {
230+
return result.errorDetail
231+
})
232+
assert.equal(
233+
errorsDetails.length,
234+
0,
235+
`Expected no errors, got ${errorsDetails.join(", ")}`,
236+
)
191237
})
192238
}
193239
})

0 commit comments

Comments
 (0)