UNPKG

@scalar/code-highlight

Version:

Central methods and themes for code highlighting in Scalar projects

161 lines (158 loc) 4.96 kB
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 };