Skip to content

Commit 0348390

Browse files
authored
Merge pull request #331 from dklimpel/html_anchors
feat: anchor link checks support HTML tags like `<a name="foo"></a>`
2 parents 014ff95 + 51fc856 commit 0348390

File tree

3 files changed

+74
-5
lines changed

3 files changed

+74
-5
lines changed

index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,32 @@ function performSpecialReplacements(str, opts) {
4242
return str;
4343
}
4444

45+
function removeCodeBlocks(markdown) {
46+
return markdown.replace(/^```[\S\s]+?^```$/gm, '');
47+
}
48+
49+
function extractHtmlSections(markdown) {
50+
markdown =
51+
// remove code blocks
52+
removeCodeBlocks(markdown)
53+
// remove HTML comments
54+
.replace(/<!--[\S\s]+?-->/gm, '')
55+
// remove single line code (if not escaped with "\")
56+
.replace(/(?<!\\)`[\S\s]+?(?<!\\)`/gm, '');
57+
58+
const regexAllId = /<(?<tag>[^\s]+).*?id=["'](?<id>[^"']*?)["'].*?>/gmi;
59+
const regexAName = /<a.*?name=["'](?<name>[^"']*?)["'].*?>/gmi;
60+
61+
const sections = []
62+
.concat(Array.from(markdown.matchAll(regexAllId), (match) => match.groups.id))
63+
.concat(Array.from(markdown.matchAll(regexAName), (match) => match.groups.name));
64+
65+
return sections
66+
}
67+
4568
function extractSections(markdown) {
4669
// First remove code blocks.
47-
markdown = markdown.replace(/^```[\S\s]+?^```$/mg, '');
70+
markdown = removeCodeBlocks(markdown);
4871

4972
const sectionTitles = markdown.match(/^#+ .*$/gm) || [];
5073

@@ -106,7 +129,7 @@ module.exports = function markdownLinkCheck(markdown, opts, callback) {
106129
}
107130

108131
const links = markdownLinkExtractor(markdown);
109-
const sections = extractSections(markdown);
132+
const sections = extractSections(markdown).concat(extractHtmlSections(markdown));
110133
const linksCollection = [...new Set(links)]
111134
const bar = (opts.showProgressBar) ?
112135
new ProgressBar('Checking... [:bar] :percent', {

test/hash-links.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
# Foo
22

33
<!-- markdownlint-disable MD033 -->
4-
<a id="tomato"></a>
4+
<a id="tomato_id"></a>
5+
<a name="tomato_name"></a>
6+
7+
<a id='tomato_id_single_quote'></a>
8+
<a name='tomato_name_single_quote'></a>
9+
10+
<div id="onion"></div>
11+
<div id="onion_outer">
12+
<div id="onion_inner"></div>
13+
</div>
14+
15+
<!--
16+
<a id="tomato_comment"></a>
17+
-->
18+
519
<!-- markdownlint-enable MD033 -->
620

721
This is a test.
822

23+
HTML anchor in code `<a id="tomato_code"></a>` should be ignored.
24+
25+
<!-- markdownlint-disable-next-line MD033 -->
26+
Ignore escaped backticks \`<a id="tomato_escaped_backticks"></a>\`. Link should work.
27+
928
## Bar
1029

1130
The title is [Foo](#foo).
@@ -20,7 +39,25 @@ To test a failure. Link that [does not exist](#does-not-exist).
2039

2140
There is no section named [Potato](#potato).
2241

23-
There is an anchor named [Tomato](#tomato).
42+
There is an anchor named with `id` [Tomato](#tomato_id).
43+
44+
There is an anchor named with `name` [Tomato](#tomato_name).
45+
46+
There is an anchor named with `id` [Tomato in single quote](#tomato_id_single_quote).
47+
48+
There is an anchor named with `name` [Tomato in single quote](#tomato_name_single_quote).
49+
50+
There is an anchor in code [Tomato in code](#tomato_code).
51+
52+
There is an anchor in escaped code [Tomato in escaped backticks](#tomato_escaped_backticks).
53+
54+
There is an anchor in HTML comment [Tomato in comment](#tomato_comment).
55+
56+
There is an anchor in single div [Onion](#onion).
57+
58+
There is an anchor in outer div [Onion outer](#onion_outer).
59+
60+
There is an anchor in inner div [Onion inner](#onion_inner).
2461

2562
## Header with special char at end ✨
2663

test/markdown-link-check.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,16 @@ describe('markdown-link-check', function () {
405405
{ link: '#bar', statusCode: 200, err: null, status: 'alive' },
406406
{ link: '#does-not-exist', statusCode: 404, err: null, status: 'dead' },
407407
{ link: '#potato', statusCode: 404, err: null, status: 'dead' },
408-
{ link: '#tomato', statusCode: 404, err: null, status: 'dead' },
408+
{ link: '#tomato_id', statusCode: 200, err: null, status: 'alive' },
409+
{ link: '#tomato_name', statusCode: 200, err: null, status: 'alive' },
410+
{ link: '#tomato_id_single_quote', statusCode: 200, err: null, status: 'alive' },
411+
{ link: '#tomato_name_single_quote', statusCode: 200, err: null, status: 'alive' },
412+
{ link: '#tomato_code', statusCode: 404, err: null, status: 'dead' },
413+
{ link: '#tomato_escaped_backticks', statusCode: 200, err: null, status: 'alive' },
414+
{ link: '#tomato_comment', statusCode: 404, err: null, status: 'dead' },
415+
{ link: '#onion', statusCode: 200, err: null, status: 'alive' },
416+
{ link: '#onion_outer', statusCode: 200, err: null, status: 'alive' },
417+
{ link: '#onion_inner', statusCode: 200, err: null, status: 'alive' },
409418
{ link: '#header-with-special-char-at-end-', statusCode: 200, err: null, status: 'alive' },
410419
{ link: '#header-with-multiple-special-chars-at-end-', statusCode: 200, err: null, status: 'alive' },
411420
{ link: '#header-with-special--char', statusCode: 200, err: null, status: 'alive' },

0 commit comments

Comments
 (0)