mirror of
				https://github.com/theoludwig/markdownlint-rule-relative-links.git
				synced 2025-09-09 19:39:29 +02:00 
			
		
		
		
	feat: support line number checking in link fragment (e.g: '#L50')
Fixes #6
This commit is contained in:
		
							
								
								
									
										33
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								src/index.js
									
									
									
									
									
								
							| @@ -8,6 +8,9 @@ const { | ||||
|   convertHeadingToHTMLFragment, | ||||
|   getMarkdownHeadings, | ||||
|   getMarkdownIdOrAnchorNameFragments, | ||||
|   isValidIntegerString, | ||||
|   getNumberOfLines, | ||||
|   getLineNumberStringFromFragment, | ||||
| } = require("./utils.js") | ||||
|  | ||||
| /** @typedef {import('markdownlint').Rule} MarkdownLintRule */ | ||||
| @@ -121,7 +124,35 @@ const customRule = { | ||||
|  | ||||
|         fragmentsHTML.push(...idOrAnchorNameHTMLFragments) | ||||
|  | ||||
|         if (!fragmentsHTML.includes(url.hash)) { | ||||
|         if (!fragmentsHTML.includes(url.hash.toLowerCase())) { | ||||
|           if (url.hash.startsWith("#L")) { | ||||
|             const lineNumberFragmentString = getLineNumberStringFromFragment( | ||||
|               url.hash, | ||||
|             ) | ||||
|  | ||||
|             const hasOnlyDigits = isValidIntegerString(lineNumberFragmentString) | ||||
|             if (!hasOnlyDigits) { | ||||
|               onError({ | ||||
|                 lineNumber, | ||||
|                 detail: `${detail} should have a valid fragment identifier`, | ||||
|               }) | ||||
|               continue | ||||
|             } | ||||
|  | ||||
|             const lineNumberFragment = Number.parseInt( | ||||
|               lineNumberFragmentString, | ||||
|               10, | ||||
|             ) | ||||
|             const numberOfLines = getNumberOfLines(fileContent) | ||||
|             if (lineNumberFragment > numberOfLines) { | ||||
|               onError({ | ||||
|                 lineNumber, | ||||
|                 detail: `${detail} should have a valid fragment identifier, ${detail} should have at least ${lineNumberFragment} lines to be valid`, | ||||
|               }) | ||||
|               continue | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           onError({ | ||||
|             lineNumber, | ||||
|             detail: `${detail} should have a valid fragment identifier`, | ||||
|   | ||||
							
								
								
									
										39
									
								
								src/utils.js
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/utils.js
									
									
									
									
									
								
							| @@ -114,8 +114,47 @@ const getMarkdownIdOrAnchorNameFragments = (content) => { | ||||
|   return result | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Checks if a string is a valid integer. | ||||
|  * | ||||
|  * Using `Number.parseInt` combined with `Number.isNaN` will not be sufficient enough because `Number.parseInt("1abc", 10)` will return `1` (a valid number) instead of `NaN`. | ||||
|  * | ||||
|  * @param {string} value | ||||
|  * @returns {boolean} | ||||
|  * @example isValidIntegerString("1") // true | ||||
|  * @example isValidIntegerString("45") // true | ||||
|  * @example isValidIntegerString("1abc") // false | ||||
|  * @example isValidIntegerString("1.0") // false | ||||
|  */ | ||||
| const isValidIntegerString = (value) => { | ||||
|   const regex = /^\d+$/ | ||||
|   return regex.test(value) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the number of lines in a string, based on the number of `\n` characters. | ||||
|  * @param {string} content | ||||
|  * @returns {number} | ||||
|  */ | ||||
| const getNumberOfLines = (content) => { | ||||
|   return content.split("\n").length | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the line number string from a fragment. | ||||
|  * @param {string} fragment | ||||
|  * @returns {string} | ||||
|  * @example getLineNumberStringFromFragment("#L50") // 50 | ||||
|  */ | ||||
| const getLineNumberStringFromFragment = (fragment) => { | ||||
|   return fragment.slice(2) | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   convertHeadingToHTMLFragment, | ||||
|   getMarkdownHeadings, | ||||
|   getMarkdownIdOrAnchorNameFragments, | ||||
|   isValidIntegerString, | ||||
|   getNumberOfLines, | ||||
|   getLineNumberStringFromFragment, | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| # Awesome | ||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Invalid | ||||
|  | ||||
| [Link fragment line number 7](./awesome.md#L7abc) | ||||
							
								
								
									
										1
									
								
								test/fixtures/invalid/invalid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/invalid/invalid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| # Awesome | ||||
							
								
								
									
										3
									
								
								test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Invalid | ||||
|  | ||||
| [Link fragment line number 7](./awesome.md#L7) | ||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-heading-case-insensitive/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-heading-case-insensitive/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Awesome | ||||
|  | ||||
| ## Existing Heading | ||||
							
								
								
									
										3
									
								
								test/fixtures/valid/existing-heading-case-insensitive/existing-heading-case-insensitive.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/existing-heading-case-insensitive/existing-heading-case-insensitive.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Valid | ||||
|  | ||||
| [Link fragment](./awesome.md#ExistIng-Heading) | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Valid | ||||
| # Awesome | ||||
|  | ||||
| ## Existing Heading | ||||
|  | ||||
|   | ||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Awesome | ||||
|  | ||||
| ## L7 | ||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Valid | ||||
|  | ||||
| [Link fragment](./awesome.md#L7) | ||||
							
								
								
									
										9
									
								
								test/fixtures/valid/valid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/fixtures/valid/valid-line-number-fragment/awesome.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # Awesome | ||||
|  | ||||
| ABC | ||||
|  | ||||
| Line 5 | ||||
|  | ||||
| Line 7 Text | ||||
|  | ||||
| ## L7 | ||||
							
								
								
									
										3
									
								
								test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Valid | ||||
|  | ||||
| [Link fragment line number 7](./awesome.md#L7) | ||||
| @@ -45,6 +45,19 @@ test("ensure the rule validates correctly", async (t) => { | ||||
|         error: | ||||
|           '"./awesome.md#not-an-id-should-be-ignored" should have a valid fragment identifier', | ||||
|       }, | ||||
|       { | ||||
|         name: "with invalid heading with #L fragment", | ||||
|         fixturePath: | ||||
|           "test/fixtures/invalid/invalid-heading-with-L-fragment/invalid-heading-with-L-fragment.md", | ||||
|         error: '"./awesome.md#L7abc" should have a valid fragment identifier', | ||||
|       }, | ||||
|       { | ||||
|         name: "with a invalid line number fragment", | ||||
|         fixturePath: | ||||
|           "test/fixtures/invalid/invalid-line-number-fragment/invalid-line-number-fragment.md", | ||||
|         error: | ||||
|           '"./awesome.md#L7" should have a valid fragment identifier, "./awesome.md#L7" should have at least 7 lines to be valid', | ||||
|       }, | ||||
|       { | ||||
|         name: "with a non-existing anchor name fragment", | ||||
|         fixturePath: | ||||
| @@ -118,6 +131,11 @@ test("ensure the rule validates correctly", async (t) => { | ||||
|         fixturePath: | ||||
|           "test/fixtures/valid/existing-element-id-fragment/existing-element-id-fragment.md", | ||||
|       }, | ||||
|       { | ||||
|         name: "with an existing heading fragment (case insensitive)", | ||||
|         fixturePath: | ||||
|           "test/fixtures/valid/existing-heading-case-insensitive/existing-heading-case-insensitive.md", | ||||
|       }, | ||||
|       { | ||||
|         name: "with an existing heading fragment", | ||||
|         fixturePath: | ||||
| @@ -128,6 +146,16 @@ test("ensure the rule validates correctly", async (t) => { | ||||
|         fixturePath: | ||||
|           "test/fixtures/valid/only-parse-markdown-files-for-fragments/only-parse-markdown-files-for-fragments.md", | ||||
|       }, | ||||
|       { | ||||
|         name: 'with valid heading "like" line number fragment', | ||||
|         fixturePath: | ||||
|           "test/fixtures/valid/valid-heading-like-number-fragment/valid-heading-like-number-fragment.md", | ||||
|       }, | ||||
|       { | ||||
|         name: "with valid line number fragment", | ||||
|         fixturePath: | ||||
|           "test/fixtures/valid/valid-line-number-fragment/valid-line-number-fragment.md", | ||||
|       }, | ||||
|       { | ||||
|         name: "with an existing file", | ||||
|         fixturePath: "test/fixtures/valid/existing-file.md", | ||||
|   | ||||
| @@ -5,6 +5,9 @@ const { | ||||
|   convertHeadingToHTMLFragment, | ||||
|   getMarkdownHeadings, | ||||
|   getMarkdownIdOrAnchorNameFragments, | ||||
|   isValidIntegerString, | ||||
|   getNumberOfLines, | ||||
|   getLineNumberStringFromFragment, | ||||
| } = require("../src/utils.js") | ||||
|  | ||||
| test("utils", async (t) => { | ||||
| @@ -54,4 +57,23 @@ test("utils", async (t) => { | ||||
|     assert.deepStrictEqual(getMarkdownIdOrAnchorNameFragments("<a>"), []) | ||||
|     assert.deepStrictEqual(getMarkdownIdOrAnchorNameFragments("<a id=>"), []) | ||||
|   }) | ||||
|  | ||||
|   await t.test("isValidIntegerString", async () => { | ||||
|     assert.strictEqual(isValidIntegerString("1"), true) | ||||
|     assert.strictEqual(isValidIntegerString("45"), true) | ||||
|     assert.strictEqual(isValidIntegerString("1abc"), false) | ||||
|     assert.strictEqual(isValidIntegerString("1.0"), false) | ||||
|   }) | ||||
|  | ||||
|   await t.test("getNumberOfLines", async () => { | ||||
|     assert.strictEqual(getNumberOfLines(""), 1) | ||||
|     assert.strictEqual(getNumberOfLines("Hello"), 1) | ||||
|     assert.strictEqual(getNumberOfLines("Hello\nWorld"), 2) | ||||
|     assert.strictEqual(getNumberOfLines("Hello\nWorld\n"), 3) | ||||
|     assert.strictEqual(getNumberOfLines("Hello\nWorld\n\n"), 4) | ||||
|   }) | ||||
|  | ||||
|   await t.test("getLineNumberStringFromFragment", async () => { | ||||
|     assert.strictEqual(getLineNumberStringFromFragment("#L50"), "50") | ||||
|   }) | ||||
| }) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user