@scalar/code-highlight
Version:
Central methods and themes for code highlighting in Scalar projects
161 lines (158 loc) • 4.96 kB
JavaScript
import rehypeExternalLinks from 'rehype-external-links';
import rehypeFormat from 'rehype-format';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkStringify from 'remark-stringify';
import { unified } from 'unified';
import { visit, SKIP } from 'unist-util-visit';
import { rehypeAlert } from '../rehype-alert/rehype-alert.js';
import { rehypeHighlight } from '../rehype-highlight/rehype-highlight.js';
import { standardLanguages } from '../languages/standard.js';
/**
* Plugin to transform nodes in a Markdown AST
*/
const transformNodes = (options, ..._ignored) => (tree) => {
if (!options?.transform || !options?.type) {
return;
}
visit(tree, options?.type, (node) => {
options?.transform ? options?.transform(node) : node;
return SKIP;
});
return;
};
/**
* Take a Markdown string and generate HTML from it
*/
function htmlFromMarkdown(markdown, options) {
// Add permitted tags and remove stripped ones
const removeTags = options?.removeTags ?? [];
const tagNames = [...(defaultSchema.tagNames ?? []), ...(options?.allowTags ?? [])].filter((t) => !removeTags.includes(t));
const html = unified()
// Parses markdown
.use(remarkParse)
// Support autolink literals, footnotes, strikethrough, tables and tasklists
.use(remarkGfm)
.use(transformNodes, {
transform: options?.transform,
type: options?.transformType,
})
// Allows any HTML tags
.use(remarkRehype, { allowDangerousHtml: true })
// Adds GitHub alerts
.use(rehypeAlert)
// Creates a HTML AST
.use(rehypeRaw)
// Removes disallowed tags
.use(rehypeSanitize, {
...defaultSchema,
// Don’t prefix the heading ids
clobberPrefix: '',
// Makes it even more strict
tagNames,
attributes: {
...defaultSchema.attributes,
abbr: ['title'],
// Allow alert classes
div: ['class', ['className', /^markdown-alert(-.*)?$/]],
},
})
// Syntax highlighting
.use(rehypeHighlight, {
languages: standardLanguages,
// Enable auto detection
detect: true,
})
// Adds target="_blank" to external links
.use(rehypeExternalLinks, { target: '_blank' })
// Formats the HTML
.use(rehypeFormat)
// Converts the HTML AST to a string
.use(rehypeStringify)
// Run the pipeline
.processSync(markdown);
return html.toString();
}
/**
* Create a Markdown AST from a string.
*/
function getMarkdownAst(markdown) {
return unified().use(remarkParse).use(remarkGfm).parse(markdown);
}
/**
* Find all headings of a specific type in a Markdown AST.
*/
function getHeadings(markdown, depth = 1) {
const tree = getMarkdownAst(markdown);
const nodes = [];
visit(tree, 'heading', (node) => {
const text = findTextInHeading(node);
if (text) {
nodes.push({ depth: node.depth ?? depth, value: text.value });
}
});
return nodes;
}
/**
* Find the text in a Markdown node (recursively).
*/
function findTextInHeading(node) {
if (node.type === 'text') {
return node;
}
if ('children' in node && node.children) {
for (const child of node.children) {
const text = findTextInHeading(child);
if (text) {
return text;
}
}
}
return null;
}
/**
* Return multiple Markdown documents. Every heading should be its own document.
*/
function splitContent(markdown) {
const tree = getMarkdownAst(markdown);
/** Sections */
const sections = [];
/** Nodes inside a section */
let nodes = [];
tree.children?.forEach((node) => {
// If the node is a heading, start a new section
if (node.type === 'heading') {
if (nodes.length) {
sections.push(nodes);
}
sections.push([node]);
nodes = [];
}
// Otherwise, add the node to the current section
else {
nodes.push(node);
}
});
// Add any remaining nodes
if (nodes.length) {
sections.push(nodes);
}
return sections.map((section) => createDocument(section));
}
/**
* Use remark to create a Markdown document from a list of nodes.
*/
function createDocument(nodes) {
// Create the Markdown string
const markdown = unified().use(remarkStringify).use(remarkGfm).stringify({
type: 'root',
children: nodes,
});
// Remove the whitespace
return markdown.trim();
}
export { getHeadings, htmlFromMarkdown, splitContent };