mirror of
https://github.com/theoludwig/markdownlint-rule-relative-links.git
synced 2024-11-03 18:11:31 +01:00
feat: html anchor support
This commit is contained in:
parent
9d2cc818d5
commit
24a0788d32
@ -19,6 +19,8 @@
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"maxNodeModuleJsDepth": 0
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
28
package-lock.json
generated
28
package-lock.json
generated
@ -10,7 +10,8 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"markdown-it": "14.0.0"
|
||||
"markdown-it": "14.0.0",
|
||||
"markdownlint-rule-helpers": "0.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "18.4.4",
|
||||
@ -29,6 +30,7 @@
|
||||
"lint-staged": "15.2.0",
|
||||
"markdownlint": "0.33.0",
|
||||
"markdownlint-cli2": "0.11.0",
|
||||
"markdownlint-rule-helpers": "0.24.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.1.1",
|
||||
"semantic-release": "22.0.12",
|
||||
@ -5174,6 +5176,30 @@
|
||||
"url": "https://github.com/sponsors/DavidAnson"
|
||||
}
|
||||
},
|
||||
"node_modules/markdownlint-rule-helpers": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.24.0.tgz",
|
||||
"integrity": "sha512-Q7l/VO0ro3+oK7ZzB0aODVFwD0/X+S3JXM9ItdTTgo23yqsv7XzGHx+cGp/zlqD1W8+uiSoXflMVXzh5KSKskA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"markdownlint-micromark": "0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/DavidAnson"
|
||||
}
|
||||
},
|
||||
"node_modules/markdownlint-rule-helpers/node_modules/markdownlint-micromark": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.2.tgz",
|
||||
"integrity": "sha512-jRxlQg8KpOfM2IbCL9RXM8ZiYWz2rv6DlZAnGv8ASJQpUh6byTBnEsbuMZ6T2/uIgntyf7SKg/mEaEBo1164fQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
|
||||
|
@ -37,14 +37,15 @@
|
||||
"lint:prettier": "prettier . --check --ignore-path .gitignore",
|
||||
"lint:javascript": "tsc --project jsconfig.json --noEmit",
|
||||
"lint:staged": "lint-staged",
|
||||
"test": "node --test --experimental-test-coverage ./test",
|
||||
"test": "node --test --experimental-test-coverage",
|
||||
"release": "semantic-release",
|
||||
"postinstall": "husky install",
|
||||
"prepublishOnly": "pinst --disable",
|
||||
"postpublish": "pinst --enable"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it": "14.0.0"
|
||||
"markdown-it": "14.0.0",
|
||||
"markdownlint-rule-helpers": "0.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "18.4.4",
|
||||
@ -63,6 +64,7 @@
|
||||
"lint-staged": "15.2.0",
|
||||
"markdownlint": "0.33.0",
|
||||
"markdownlint-cli2": "0.11.0",
|
||||
"markdownlint-rule-helpers": "0.24.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.1.1",
|
||||
"semantic-release": "22.0.12",
|
||||
|
@ -7,6 +7,7 @@ const {
|
||||
filterTokens,
|
||||
convertHeadingToHTMLFragment,
|
||||
getMarkdownHeadings,
|
||||
getMarkdownAnchorHTMLFragments,
|
||||
} = require("./utils.js")
|
||||
|
||||
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
|
||||
@ -63,6 +64,8 @@ const customRule = {
|
||||
if (type === "link_open" && url.hash !== "") {
|
||||
const fileContent = fs.readFileSync(url, { encoding: "utf8" })
|
||||
const headings = getMarkdownHeadings(fileContent)
|
||||
const anchorHTMLFragments =
|
||||
getMarkdownAnchorHTMLFragments(fileContent)
|
||||
|
||||
/** @type {Map<string, number>} */
|
||||
const fragments = new Map()
|
||||
@ -77,6 +80,8 @@ const customRule = {
|
||||
return fragment
|
||||
})
|
||||
|
||||
headingsHTMLFragments.push(...anchorHTMLFragments)
|
||||
|
||||
if (!headingsHTMLFragments.includes(url.hash)) {
|
||||
onError({
|
||||
lineNumber,
|
||||
|
49
src/utils.js
49
src/utils.js
@ -1,4 +1,6 @@
|
||||
const MarkdownIt = require("markdown-it")
|
||||
// @ts-ignore
|
||||
const { getHtmlAttributeRe } = require("markdownlint-rule-helpers")
|
||||
|
||||
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */
|
||||
/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */
|
||||
@ -94,8 +96,55 @@ const getMarkdownHeadings = (content) => {
|
||||
return headings
|
||||
}
|
||||
|
||||
const anchorNameRe = getHtmlAttributeRe("name")
|
||||
const anchorIdRe = getHtmlAttributeRe("id")
|
||||
|
||||
/**
|
||||
* Gets the anchor HTML fragments from a Markdown string.
|
||||
* @param {string} content
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const getMarkdownAnchorHTMLFragments = (content) => {
|
||||
const markdownIt = new MarkdownIt({ html: true })
|
||||
const tokens = markdownIt.parse(content, {})
|
||||
|
||||
/** @type {string[]} */
|
||||
const result = []
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.type === "inline") {
|
||||
if (!token.children) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const child of token.children) {
|
||||
if (child.type === "html_inline") {
|
||||
const anchorMatch =
|
||||
anchorIdRe.exec(token.content) || anchorNameRe.exec(token.content)
|
||||
if (!anchorMatch || anchorMatch.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const anchorIdOrName = anchorMatch[1]
|
||||
if (anchorMatch[1] === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const anchorHTMLFragment = "#" + anchorIdOrName
|
||||
if (!result.includes(anchorHTMLFragment)) {
|
||||
result.push(anchorHTMLFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
filterTokens,
|
||||
convertHeadingToHTMLFragment,
|
||||
getMarkdownHeadings,
|
||||
getMarkdownAnchorHTMLFragments,
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
# Valid
|
||||
|
||||
<a name="existing-heading-anchor" ></a>
|
||||
|
||||
## Existing Heading
|
||||
|
@ -1,3 +1,5 @@
|
||||
# Invalid
|
||||
|
||||
[Link fragment](./awesome.md#non-existing-heading)
|
||||
|
||||
[Link fragment](./awesome.md#non-existing-heading-anchor)
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Valid
|
||||
|
||||
<a name="existing-heading-anchor" ></a>
|
||||
|
||||
## Existing Heading
|
||||
|
||||
### Repeated Heading
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
[Link fragment](./awesome.md#existing-heading)
|
||||
|
||||
[Link fragment](./awesome.md#existing-heading-anchor)
|
||||
|
||||
[Link fragment Repeated 0](./awesome.md#repeated-heading)
|
||||
|
||||
[Link fragment Repeated 1](./awesome.md#repeated-heading-1)
|
||||
|
@ -65,16 +65,22 @@ test("ensure the rule validates correctly", async (t) => {
|
||||
const lintResults = await validateMarkdownLint(
|
||||
"test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
|
||||
)
|
||||
assert.equal(lintResults?.length, 1)
|
||||
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names)
|
||||
assert.equal(
|
||||
lintResults?.[0]?.ruleDescription,
|
||||
relativeLinksRule.description,
|
||||
)
|
||||
assert.equal(lintResults?.length, 2)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
assert.deepEqual(lintResults?.[i]?.ruleNames, relativeLinksRule.names)
|
||||
assert.equal(
|
||||
lintResults?.[i]?.ruleDescription,
|
||||
relativeLinksRule.description,
|
||||
)
|
||||
}
|
||||
assert.equal(
|
||||
lintResults?.[0]?.errorDetail,
|
||||
'"./awesome.md#non-existing-heading" should have a valid fragment identifier',
|
||||
)
|
||||
assert.equal(
|
||||
lintResults?.[1]?.errorDetail,
|
||||
'"./awesome.md#non-existing-heading-anchor" should have a valid fragment identifier',
|
||||
)
|
||||
})
|
||||
|
||||
await t.test("with a non-existing file", async () => {
|
||||
|
@ -4,6 +4,7 @@ const assert = require("node:assert/strict")
|
||||
const {
|
||||
convertHeadingToHTMLFragment,
|
||||
getMarkdownHeadings,
|
||||
getMarkdownAnchorHTMLFragments,
|
||||
} = require("../src/utils.js")
|
||||
|
||||
test("utils", async (t) => {
|
||||
@ -34,4 +35,17 @@ test("utils", async (t) => {
|
||||
["Hello", "World", "Hello, world!"],
|
||||
)
|
||||
})
|
||||
|
||||
await t.test("getMarkdownAnchorHTMLFragments", async () => {
|
||||
assert.deepStrictEqual(
|
||||
getMarkdownAnchorHTMLFragments('<a name="anchorName" id="anchorId"></a>'),
|
||||
["#anchorId"],
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
getMarkdownAnchorHTMLFragments('<a name="anchorName"></a>'),
|
||||
["#anchorName"],
|
||||
)
|
||||
assert.deepStrictEqual(getMarkdownAnchorHTMLFragments("<a></a>"), [])
|
||||
assert.deepStrictEqual(getMarkdownAnchorHTMLFragments("<a>"), [])
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user