UNPKG

@tanstack/highlight

Version:

Tiny class-based syntax highlighting for documentation.

117 lines (116 loc) • 3.99 kB
const codeDiffNotation = /(?:(?:\/\/|#)[ \t]*\[!code[ \t]+(\+\+|--)\]|\/\*[ \t]*\[!code[ \t]+(\+\+|--)\][ \t]*\*\/|<!--[ \t]*\[!code[ \t]+(\+\+|--)\][ \t]*-->)[ \t]*\r?$/; export function parseCodeDiffNotation(code) { const decorations = []; if (!code.includes('[!code')) return { code, decorations }; const lines = code.split('\n'); for (let index = 0; index < lines.length; index++) { const line = lines[index]; const match = codeDiffNotation.exec(line); if (!match) continue; const notation = match[1] || match[2] || match[3]; decorations.push({ className: notation === '++' ? 'th-line--inserted' : 'th-line--deleted', lines: index + 1, }); lines[index] = line.slice(0, match.index).replace(/[ \t]+$/, '') + (line.endsWith('\r') ? '\r' : ''); } return { code: lines.join('\n'), decorations, }; } const codeFenceMetadata = /"[^"]*"|'[^']*'|(?:^|[\s{])(?:(title|filename|file|name)=("[^"]*"|'[^']*'|[^\s}]+)|(del|error|focus|highlight|ins|warning)=\{([^}]*)\}|\{([\d,\s-]+)\}(?=\s|$)|(lineNumbers|showLineNumbers)(?=\s|}|$))/g; const annotationClasses = { del: 'th-line--deleted', error: 'th-line--error', focus: 'th-line--focused', highlight: 'th-line--highlighted', ins: 'th-line--inserted', warning: 'th-line--warning', }; export function parseCodeFenceMeta(meta) { if (!meta) return { decorations: [], lineNumbers: false }; const parsed = { decorations: [], lineNumbers: false, title: undefined, }; let hasTitle = false; for (const match of meta.matchAll(codeFenceMetadata)) { if (match[1] && !hasTitle) { parsed.title = unquoteTitle(match[2]); hasTitle = true; } else if (match[3]) { parseLineList(match[4], annotationClasses[match[3]], parsed.decorations); } else if (match[5]) { parseLineList(match[5], 'th-line--highlighted', parsed.decorations); } else if (match[6]) { parsed.lineNumbers = true; } } return parsed; } export function getCodeFenceTitle(meta) { if (!meta) return undefined; for (const match of meta.matchAll(codeFenceMetadata)) { if (match[1]) return unquoteTitle(match[2]); } return undefined; } function unquoteTitle(value) { const quote = value[0]; const unquoted = (quote === '"' || quote === "'") && value.endsWith(quote) ? value.slice(1, -1) : value; return unquoted.trim() || undefined; } export function prepareCodeFence({ code, decorations, lang, lineNumbers, meta, title, }) { const annotated = parseCodeDiffNotation(code); const parsed = parseCodeFenceMeta(meta); return { code: annotated.code, decorations: [ ...annotated.decorations, ...parsed.decorations, ...(decorations || []), ], lang: lang || undefined, lineNumbers: lineNumbers ?? parsed.lineNumbers, title: title || parsed.title, }; } export function tokenizeCodeFence(input, highlighter) { const prepared = prepareCodeFence(input); return { ...prepared, ...highlighter.tokenize(prepared.code.trimEnd(), { lang: prepared.lang }), }; } function parseLineList(value, className, decorations) { for (const part of value.split(',')) { const range = part.trim().match(/^(\d+)(?:-(\d+))?$/); if (!range) continue; const start = Number(range[1]); const end = Number(range[2] || range[1]); if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) continue; decorations.push({ className, lines: start === end ? start : [start, end], }); } }