UNPKG

@tanstack/highlight

Version:

Tiny class-based syntax highlighting for documentation.

97 lines (96 loc) • 4.62 kB
import { defineLanguage } from '../core.js'; import { collectPatternRanges } from './patterns.js'; // Shared by the optional lexical definitions. No grammar registry or language // data lives here, so a selective import only retains its own vocabulary. export const cComments = /\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|(?![\s\S]))/; export const quotedStrings = /"(?:\\[\s\S]|[^"\\])*(?:"|(?![\s\S]))|'(?:\\[^\r\n]|[^'\\\r\n])*(?:'|(?![\s\S]))/; export const doubleStrings = /"(?:\\[\s\S]|[^"\\])*(?:"|(?![\s\S]))/; export function simpleLanguage(options, wordPatterns = regexWordPatterns, owned = false) { let pending = options; let tokenize; return defineLanguage({ name: options.name, aliases: options.aliases, tokenize(code) { if (!tokenize) { tokenize = createSimpleTokenizer(pending, wordPatterns, owned); pending = undefined; } return tokenize(code); }, }); } export function createSimpleTokenizer(options, wordPatterns = regexWordPatterns, owned = false) { const flags = options.insensitive ? 'gmi' : 'gm'; // Shield comments and strings in source order. These expressions must use // noncapturing groups so the first capture always identifies a comment. // Multiline rules use (?![\s\S]) for end of input: with the m flag, $ also // matches line endings and would truncate lazy strings and block comments. const lexical = new RegExp(`(${options.comments.source})|(?:${options.strings.source})`, flags); const runtime = owned ? { nestedComments: options.nestedComments } : options; const collectLexicalRanges = function (code) { const ranges = []; lexical.lastIndex = 0; let match; while ((match = lexical.exec(code))) { if (!match[0].length) { lexical.lastIndex++; continue; } let end = lexical.lastIndex; if (match[1] !== undefined) { for (const [open, close] of runtime.nestedComments || []) { if (!code.startsWith(open, match.index)) continue; let cursor = match.index + open.length; let depth = 1; // Walk once rather than repeatedly searching the remaining suffix // for an opener while unwinding many nested closing delimiters. while (depth && cursor < code.length) { if (code.startsWith(open, cursor)) { depth++; cursor += open.length; } else if (code.startsWith(close, cursor)) { depth--; cursor += close.length; } else { cursor++; } } end = cursor; lexical.lastIndex = end; break; } } ranges.push({ start: match.index, end, className: match[1] !== undefined ? 'comment' : 'string' }); } return ranges; }; const patterns = []; patterns.push(...options.beforeWords || []); const wordCharacters = options.wordCharacters || '\\w$'; patterns.push(...wordPatterns(options, flags, wordCharacters)); patterns.push(...options.rules || []); if (options.numbers !== false) patterns.push([options.numbers || new RegExp(String.raw `(?<![\w.])(?:0[xX][\da-fA-F]+|0[bB][01]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?)${options.numberSuffix || ''}(?![\w.])`, 'g'), 'number']); return (code) => { // Lexical matches are fresh and non-overlapping, reuse their owned array. const ranges = collectLexicalRanges(code); return collectPatternRanges(code, patterns, ranges, undefined, ranges); }; } export function wordCategories(options) { return [['keyword', options.keywords], ['literal', options.literals], ['type', options.types]]; } function regexWordPatterns(options, flags, wordCharacters) { const patterns = []; for (const [className, words] of [ ['keyword', options.keywords], ['literal', options.literals], ['type', options.types], ]) { if (words) patterns.push([new RegExp(`(?<![${wordCharacters}])(?:${words.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?![${wordCharacters}])`, flags), className]); } return patterns; }