2023-10-23 23:11:41 +02:00
|
|
|
const MarkdownIt = require("markdown-it")
|
2024-01-11 13:42:34 +00:00
|
|
|
// @ts-ignore
|
|
|
|
const { getHtmlAttributeRe } = require("markdownlint-rule-helpers")
|
2023-06-24 11:42:09 +02:00
|
|
|
|
2024-01-09 23:20:17 +01:00
|
|
|
/** @typedef {import('markdownlint').RuleParams} MarkdownLintRuleParams */
|
|
|
|
/** @typedef {import('markdownlint').MarkdownItToken} MarkdownItToken */
|
|
|
|
|
2023-06-24 11:42:09 +02:00
|
|
|
/**
|
|
|
|
* Calls the provided function for each matching token.
|
|
|
|
*
|
2024-01-09 23:20:17 +01:00
|
|
|
* @param {MarkdownLintRuleParams} params RuleParams instance.
|
2023-06-24 11:42:09 +02:00
|
|
|
* @param {string} type Token type identifier.
|
2024-01-09 23:20:17 +01:00
|
|
|
* @param {(token: MarkdownItToken) => void} handler Callback function.
|
2023-06-24 11:42:09 +02:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
const filterTokens = (params, type, handler) => {
|
|
|
|
for (const token of params.tokens) {
|
|
|
|
if (token.type === type) {
|
|
|
|
handler(token)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a Markdown heading into an HTML fragment according to the rules
|
|
|
|
* used by GitHub.
|
|
|
|
*
|
2023-06-27 13:15:03 +02:00
|
|
|
* @see https://github.com/DavidAnson/markdownlint/blob/d01180ec5a014083ee9d574b693a8d7fbc1e566d/lib/md051.js#L1
|
2023-06-24 11:42:09 +02:00
|
|
|
* @param {string} inlineText Inline token for heading.
|
|
|
|
* @returns {string} Fragment string for heading.
|
|
|
|
*/
|
|
|
|
const convertHeadingToHTMLFragment = (inlineText) => {
|
|
|
|
return (
|
2023-10-23 23:11:41 +02:00
|
|
|
"#" +
|
2023-06-24 11:42:09 +02:00
|
|
|
encodeURIComponent(
|
|
|
|
inlineText
|
|
|
|
.toLowerCase()
|
|
|
|
// RegExp source with Ruby's \p{Word} expanded into its General Categories
|
|
|
|
// https://github.com/gjtorikian/html-pipeline/blob/main/lib/html/pipeline/toc_filter.rb
|
|
|
|
// https://ruby-doc.org/core-3.0.2/Regexp.html
|
|
|
|
.replace(
|
|
|
|
/[^\p{Letter}\p{Mark}\p{Number}\p{Connector_Punctuation}\- ]/gu,
|
2023-10-23 23:11:41 +02:00
|
|
|
"",
|
2023-06-24 11:42:09 +02:00
|
|
|
)
|
2023-10-23 23:11:41 +02:00
|
|
|
.replace(/ /gu, "-"),
|
2023-06-24 11:42:09 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-10-23 23:11:41 +02:00
|
|
|
const headingTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
|
|
|
|
const ignoredTokens = new Set(["heading_open", "heading_close"])
|
2023-06-24 11:42:09 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the headings from a Markdown string.
|
|
|
|
* @param {string} content
|
|
|
|
* @returns {string[]}
|
|
|
|
*/
|
|
|
|
const getMarkdownHeadings = (content) => {
|
|
|
|
const markdownIt = new MarkdownIt({ html: true })
|
|
|
|
const tokens = markdownIt.parse(content, {})
|
|
|
|
|
|
|
|
/** @type {string[]} */
|
|
|
|
const headings = []
|
|
|
|
|
|
|
|
/** @type {string | null} */
|
|
|
|
let headingToken = null
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
if (headingTags.has(token.tag)) {
|
2023-10-23 23:11:41 +02:00
|
|
|
if (token.type === "heading_open") {
|
2023-06-24 11:42:09 +02:00
|
|
|
headingToken = token.markup
|
2023-10-23 23:11:41 +02:00
|
|
|
} else if (token.type === "heading_close") {
|
2023-06-24 11:42:09 +02:00
|
|
|
headingToken = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ignoredTokens.has(token.type)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if (headingToken === null) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-01-09 23:20:17 +01:00
|
|
|
const children = token.children ?? []
|
|
|
|
|
2023-06-24 11:42:09 +02:00
|
|
|
headings.push(
|
2024-01-09 23:20:17 +01:00
|
|
|
`${children
|
2023-06-24 11:42:09 +02:00
|
|
|
.map((token) => {
|
|
|
|
return token.content
|
|
|
|
})
|
2023-10-23 23:11:41 +02:00
|
|
|
.join("")}`,
|
2023-06-24 11:42:09 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return headings
|
|
|
|
}
|
|
|
|
|
2024-01-11 13:42:34 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-06-24 11:42:09 +02:00
|
|
|
module.exports = {
|
|
|
|
filterTokens,
|
|
|
|
convertHeadingToHTMLFragment,
|
2023-10-23 23:11:41 +02:00
|
|
|
getMarkdownHeadings,
|
2024-01-11 13:42:34 +00:00
|
|
|
getMarkdownAnchorHTMLFragments,
|
2023-06-24 11:42:09 +02:00
|
|
|
}
|