mirror of
https://github.com/theoludwig/markdownlint-rule-relative-links.git
synced 2024-12-08 00:45:32 +01:00
feat: html anchor support
This commit is contained in:
parent
9d2cc818d5
commit
24a0788d32
@ -19,6 +19,8 @@
|
|||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": 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,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"markdown-it": "14.0.0"
|
"markdown-it": "14.0.0",
|
||||||
|
"markdownlint-rule-helpers": "0.24.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "18.4.4",
|
"@commitlint/cli": "18.4.4",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"lint-staged": "15.2.0",
|
"lint-staged": "15.2.0",
|
||||||
"markdownlint": "0.33.0",
|
"markdownlint": "0.33.0",
|
||||||
"markdownlint-cli2": "0.11.0",
|
"markdownlint-cli2": "0.11.0",
|
||||||
|
"markdownlint-rule-helpers": "0.24.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"semantic-release": "22.0.12",
|
"semantic-release": "22.0.12",
|
||||||
@ -5174,6 +5176,30 @@
|
|||||||
"url": "https://github.com/sponsors/DavidAnson"
|
"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": {
|
"node_modules/marked": {
|
||||||
"version": "9.1.6",
|
"version": "9.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
|
||||||
|
@ -37,14 +37,15 @@
|
|||||||
"lint:prettier": "prettier . --check --ignore-path .gitignore",
|
"lint:prettier": "prettier . --check --ignore-path .gitignore",
|
||||||
"lint:javascript": "tsc --project jsconfig.json --noEmit",
|
"lint:javascript": "tsc --project jsconfig.json --noEmit",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test": "node --test --experimental-test-coverage ./test",
|
"test": "node --test --experimental-test-coverage",
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
"prepublishOnly": "pinst --disable",
|
"prepublishOnly": "pinst --disable",
|
||||||
"postpublish": "pinst --enable"
|
"postpublish": "pinst --enable"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"markdown-it": "14.0.0"
|
"markdown-it": "14.0.0",
|
||||||
|
"markdownlint-rule-helpers": "0.24.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "18.4.4",
|
"@commitlint/cli": "18.4.4",
|
||||||
@ -63,6 +64,7 @@
|
|||||||
"lint-staged": "15.2.0",
|
"lint-staged": "15.2.0",
|
||||||
"markdownlint": "0.33.0",
|
"markdownlint": "0.33.0",
|
||||||
"markdownlint-cli2": "0.11.0",
|
"markdownlint-cli2": "0.11.0",
|
||||||
|
"markdownlint-rule-helpers": "0.24.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"semantic-release": "22.0.12",
|
"semantic-release": "22.0.12",
|
||||||
|
@ -7,6 +7,7 @@ const {
|
|||||||
filterTokens,
|
filterTokens,
|
||||||
convertHeadingToHTMLFragment,
|
convertHeadingToHTMLFragment,
|
||||||
getMarkdownHeadings,
|
getMarkdownHeadings,
|
||||||
|
getMarkdownAnchorHTMLFragments,
|
||||||
} = require("./utils.js")
|
} = require("./utils.js")
|
||||||
|
|
||||||
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
|
/** @typedef {import('markdownlint').Rule} MarkdownLintRule */
|
||||||
@ -63,6 +64,8 @@ const customRule = {
|
|||||||
if (type === "link_open" && url.hash !== "") {
|
if (type === "link_open" && url.hash !== "") {
|
||||||
const fileContent = fs.readFileSync(url, { encoding: "utf8" })
|
const fileContent = fs.readFileSync(url, { encoding: "utf8" })
|
||||||
const headings = getMarkdownHeadings(fileContent)
|
const headings = getMarkdownHeadings(fileContent)
|
||||||
|
const anchorHTMLFragments =
|
||||||
|
getMarkdownAnchorHTMLFragments(fileContent)
|
||||||
|
|
||||||
/** @type {Map<string, number>} */
|
/** @type {Map<string, number>} */
|
||||||
const fragments = new Map()
|
const fragments = new Map()
|
||||||
@ -77,6 +80,8 @@ const customRule = {
|
|||||||
return fragment
|
return fragment
|
||||||
})
|
})
|
||||||
|
|
||||||
|
headingsHTMLFragments.push(...anchorHTMLFragments)
|
||||||
|
|
||||||
if (!headingsHTMLFragments.includes(url.hash)) {
|
if (!headingsHTMLFragments.includes(url.hash)) {
|
||||||
onError({
|
onError({
|
||||||
lineNumber,
|
lineNumber,
|
||||||
|
49
src/utils.js
49
src/utils.js
@ -1,4 +1,6 @@
|
|||||||
const MarkdownIt = require("markdown-it")
|
const MarkdownIt = require("markdown-it")
|
||||||
|
// @ts-ignore
|
||||||
|
const { getHtmlAttributeRe } = require("markdownlint-rule-helpers")
|
||||||
|
|
||||||
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */
|
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */
|
||||||
/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */
|
/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */
|
||||||
@ -94,8 +96,55 @@ const getMarkdownHeadings = (content) => {
|
|||||||
return headings
|
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 = {
|
module.exports = {
|
||||||
filterTokens,
|
filterTokens,
|
||||||
convertHeadingToHTMLFragment,
|
convertHeadingToHTMLFragment,
|
||||||
getMarkdownHeadings,
|
getMarkdownHeadings,
|
||||||
|
getMarkdownAnchorHTMLFragments,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
# Valid
|
# Valid
|
||||||
|
|
||||||
|
<a name="existing-heading-anchor" ></a>
|
||||||
|
|
||||||
## Existing Heading
|
## Existing Heading
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
# Invalid
|
# Invalid
|
||||||
|
|
||||||
[Link fragment](./awesome.md#non-existing-heading)
|
[Link fragment](./awesome.md#non-existing-heading)
|
||||||
|
|
||||||
|
[Link fragment](./awesome.md#non-existing-heading-anchor)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Valid
|
# Valid
|
||||||
|
|
||||||
|
<a name="existing-heading-anchor" ></a>
|
||||||
|
|
||||||
## Existing Heading
|
## Existing Heading
|
||||||
|
|
||||||
### Repeated Heading
|
### Repeated Heading
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
[Link fragment](./awesome.md#existing-heading)
|
[Link fragment](./awesome.md#existing-heading)
|
||||||
|
|
||||||
|
[Link fragment](./awesome.md#existing-heading-anchor)
|
||||||
|
|
||||||
[Link fragment Repeated 0](./awesome.md#repeated-heading)
|
[Link fragment Repeated 0](./awesome.md#repeated-heading)
|
||||||
|
|
||||||
[Link fragment Repeated 1](./awesome.md#repeated-heading-1)
|
[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(
|
const lintResults = await validateMarkdownLint(
|
||||||
"test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
|
"test/fixtures/invalid/non-existing-heading-fragment/non-existing-heading-fragment.md",
|
||||||
)
|
)
|
||||||
assert.equal(lintResults?.length, 1)
|
assert.equal(lintResults?.length, 2)
|
||||||
assert.deepEqual(lintResults?.[0]?.ruleNames, relativeLinksRule.names)
|
for (let i = 0; i < 2; i++) {
|
||||||
assert.equal(
|
assert.deepEqual(lintResults?.[i]?.ruleNames, relativeLinksRule.names)
|
||||||
lintResults?.[0]?.ruleDescription,
|
assert.equal(
|
||||||
relativeLinksRule.description,
|
lintResults?.[i]?.ruleDescription,
|
||||||
)
|
relativeLinksRule.description,
|
||||||
|
)
|
||||||
|
}
|
||||||
assert.equal(
|
assert.equal(
|
||||||
lintResults?.[0]?.errorDetail,
|
lintResults?.[0]?.errorDetail,
|
||||||
'"./awesome.md#non-existing-heading" should have a valid fragment identifier',
|
'"./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 () => {
|
await t.test("with a non-existing file", async () => {
|
||||||
|
@ -4,6 +4,7 @@ const assert = require("node:assert/strict")
|
|||||||
const {
|
const {
|
||||||
convertHeadingToHTMLFragment,
|
convertHeadingToHTMLFragment,
|
||||||
getMarkdownHeadings,
|
getMarkdownHeadings,
|
||||||
|
getMarkdownAnchorHTMLFragments,
|
||||||
} = require("../src/utils.js")
|
} = require("../src/utils.js")
|
||||||
|
|
||||||
test("utils", async (t) => {
|
test("utils", async (t) => {
|
||||||
@ -34,4 +35,17 @@ test("utils", async (t) => {
|
|||||||
["Hello", "World", "Hello, world!"],
|
["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