113 lines
3.0 KiB
JavaScript
113 lines
3.0 KiB
JavaScript
const htmlEscapeMap = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
"'": "'",
|
|
'"': """
|
|
};
|
|
const htmlEscapeRegexp = /[&<>'"]/g;
|
|
const htmlEscape = (str) => str.replace(
|
|
htmlEscapeRegexp,
|
|
(char) => htmlEscapeMap[char]
|
|
);
|
|
|
|
const htmlUnescapeMap = {
|
|
"&": "&",
|
|
"&": "&",
|
|
"<": "<",
|
|
"<": "<",
|
|
">": ">",
|
|
">": ">",
|
|
"'": "'",
|
|
"'": "'",
|
|
""": '"',
|
|
""": '"'
|
|
};
|
|
const htmlUnescapeRegexp = /&(amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
|
|
const htmlUnescape = (str) => str.replace(
|
|
htmlUnescapeRegexp,
|
|
(char) => htmlUnescapeMap[char]
|
|
);
|
|
|
|
const resolveTitleFromToken = (token, { shouldAllowHtml, shouldEscapeText }) => {
|
|
const children = token.children ?? [];
|
|
const titleTokenTypes = ["text", "emoji", "code_inline"];
|
|
if (shouldAllowHtml) {
|
|
titleTokenTypes.push("html_inline");
|
|
}
|
|
const titleTokens = children.filter(
|
|
(item) => titleTokenTypes.includes(item.type) && // filter permalink symbol that generated by markdown-it-anchor
|
|
!item.meta?.isPermalinkSymbol
|
|
);
|
|
return titleTokens.reduce((result, item) => {
|
|
if (shouldEscapeText) {
|
|
if (item.type === "code_inline" || item.type === "text") {
|
|
return `${result}${htmlEscape(item.content)}`;
|
|
}
|
|
}
|
|
return `${result}${item.content}`;
|
|
}, "").trim();
|
|
};
|
|
|
|
const resolveHeadersFromTokens = (tokens, {
|
|
level,
|
|
shouldAllowHtml,
|
|
shouldAllowNested,
|
|
shouldEscapeText,
|
|
slugify,
|
|
format
|
|
}) => {
|
|
const headers = [];
|
|
const stack = [];
|
|
const push = (header) => {
|
|
while (stack.length !== 0 && header.level <= stack[0].level) {
|
|
stack.shift();
|
|
}
|
|
if (stack.length === 0) {
|
|
headers.push(header);
|
|
stack.push(header);
|
|
} else {
|
|
stack[0].children.push(header);
|
|
stack.unshift(header);
|
|
}
|
|
};
|
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
const token = tokens[i];
|
|
if (token.type !== "heading_open") {
|
|
continue;
|
|
}
|
|
if (token.level !== 0 && !shouldAllowNested) {
|
|
continue;
|
|
}
|
|
const headerLevel = Number.parseInt(token.tag.slice(1), 10);
|
|
if (!level.includes(headerLevel)) {
|
|
continue;
|
|
}
|
|
const nextToken = tokens[i + 1];
|
|
/* istanbul ignore if -- @preserve */
|
|
if (!nextToken) {
|
|
continue;
|
|
}
|
|
const title = resolveTitleFromToken(nextToken, {
|
|
shouldAllowHtml,
|
|
shouldEscapeText
|
|
});
|
|
const slug = token.attrGet("id") ?? slugify(title);
|
|
push({
|
|
level: headerLevel,
|
|
title: format?.(title) ?? title,
|
|
slug,
|
|
link: `#${slug}`,
|
|
children: []
|
|
});
|
|
}
|
|
return headers;
|
|
};
|
|
|
|
const rControl = /[\u0000-\u001f]/g;
|
|
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
|
|
const rCombining = /[\u0300-\u036F]/g;
|
|
const slugify = (str) => str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
|
|
|
|
export { htmlEscape, htmlUnescape, resolveHeadersFromTokens, resolveTitleFromToken, slugify };
|