UNPKG

@tanstack/highlight

Version:

Tiny class-based syntax highlighting for documentation.

70 lines (69 loc) • 3.89 kB
import { defineLanguage } from '../core.js'; import { patternTokenizer } 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) { 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 patterns = [{ collect(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 options.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; }, }]; patterns.push(...options.beforeWords || []); const wordCharacters = options.wordCharacters || '\\w$'; for (const [className, words] of [ ['keyword', options.keywords], ['literal', options.literals], ['type', options.types], ]) { if (words) patterns.push({ className, regex: new RegExp(`(?<![${wordCharacters}])(?:${words.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})(?![${wordCharacters}])`, flags) }); } patterns.push(...options.rules || []); if (options.numbers !== false) patterns.push({ className: 'number', regex: 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') }); return defineLanguage({ name: options.name, aliases: options.aliases, tokenize: patternTokenizer(patterns) }); }