Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: anchor link checks support HTML tags like <a name="foo"></a> #331

Merged
merged 3 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,32 @@ function performSpecialReplacements(str, opts) {
return str;
}

function removeCodeBlocks(markdown) {
return markdown.replace(/^```[\S\s]+?^```$/gm, '');
}

function extractHtmlSections(markdown) {
markdown =
// remove code blocks
removeCodeBlocks(markdown)
// remove HTML comments
.replace(/<!--[\S\s]+?-->/gm, '')
// remove single line code (if not escaped with "\")
.replace(/(?<!\\)`[\S\s]+?(?<!\\)`/gm, '');

const regexAllId = /<(?<tag>[^\s]+).*?id=["'](?<id>[^"']*?)["'].*?>/gmi;
const regexAName = /<a.*?name=["'](?<name>[^"']*?)["'].*?>/gmi;

const sections = []
.concat(Array.from(markdown.matchAll(regexAllId), (match) => match.groups.id))
.concat(Array.from(markdown.matchAll(regexAName), (match) => match.groups.name));

return sections
}

function extractSections(markdown) {
// First remove code blocks.
markdown = markdown.replace(/^```[\S\s]+?^```$/mg, '');
markdown = removeCodeBlocks(markdown);

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

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

const links = markdownLinkExtractor(markdown);
const sections = extractSections(markdown);
const sections = extractSections(markdown).concat(extractHtmlSections(markdown));
const linksCollection = [...new Set(links)]
const bar = (opts.showProgressBar) ?
new ProgressBar('Checking... [:bar] :percent', {
Expand Down
41 changes: 39 additions & 2 deletions test/hash-links.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
# Foo

<!-- markdownlint-disable MD033 -->
<a id="tomato"></a>
<a id="tomato_id"></a>
<a name="tomato_name"></a>

<a id='tomato_id_single_quote'></a>
<a name='tomato_name_single_quote'></a>

<div id="onion"></div>
<div id="onion_outer">
<div id="onion_inner"></div>
</div>

<!--
<a id="tomato_comment"></a>
-->

<!-- markdownlint-enable MD033 -->

This is a test.

HTML anchor in code `<a id="tomato_code"></a>` should be ignored.

<!-- markdownlint-disable-next-line MD033 -->
Ignore escaped backticks \`<a id="tomato_escaped_backticks"></a>\`. Link should work.

## Bar

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

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

There is an anchor named [Tomato](#tomato).
There is an anchor named with `id` [Tomato](#tomato_id).

There is an anchor named with `name` [Tomato](#tomato_name).

There is an anchor named with `id` [Tomato in single quote](#tomato_id_single_quote).

There is an anchor named with `name` [Tomato in single quote](#tomato_name_single_quote).

There is an anchor in code [Tomato in code](#tomato_code).

There is an anchor in escaped code [Tomato in escaped backticks](#tomato_escaped_backticks).

There is an anchor in HTML comment [Tomato in comment](#tomato_comment).

There is an anchor in single div [Onion](#onion).

There is an anchor in outer div [Onion outer](#onion_outer).

There is an anchor in inner div [Onion inner](#onion_inner).

## Header with special char at end ✨

Expand Down
11 changes: 10 additions & 1 deletion test/markdown-link-check.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,16 @@ describe('markdown-link-check', function () {
{ link: '#bar', statusCode: 200, err: null, status: 'alive' },
{ link: '#does-not-exist', statusCode: 404, err: null, status: 'dead' },
{ link: '#potato', statusCode: 404, err: null, status: 'dead' },
{ link: '#tomato', statusCode: 404, err: null, status: 'dead' },
{ link: '#tomato_id', statusCode: 200, err: null, status: 'alive' },
{ link: '#tomato_name', statusCode: 200, err: null, status: 'alive' },
{ link: '#tomato_id_single_quote', statusCode: 200, err: null, status: 'alive' },
{ link: '#tomato_name_single_quote', statusCode: 200, err: null, status: 'alive' },
{ link: '#tomato_code', statusCode: 404, err: null, status: 'dead' },
{ link: '#tomato_escaped_backticks', statusCode: 200, err: null, status: 'alive' },
{ link: '#tomato_comment', statusCode: 404, err: null, status: 'dead' },
{ link: '#onion', statusCode: 200, err: null, status: 'alive' },
{ link: '#onion_outer', statusCode: 200, err: null, status: 'alive' },
{ link: '#onion_inner', statusCode: 200, err: null, status: 'alive' },
{ link: '#header-with-special-char-at-end-', statusCode: 200, err: null, status: 'alive' },
{ link: '#header-with-multiple-special-chars-at-end-', statusCode: 200, err: null, status: 'alive' },
{ link: '#header-with-special--char', statusCode: 200, err: null, status: 'alive' },
Expand Down