@tanstack/highlight
Version:
Tiny class-based syntax highlighting for documentation.
107 lines (106 loc) • 4.08 kB
JavaScript
import { defineLanguage } from '../core.js';
import { patternTokenizer } from '../internal/patterns.js';
export const shell = defineLanguage({
name: 'shell',
aliases: ['bash', 'sh', 'zsh', 'cmd', 'console'],
tokenize: patternTokenizer([
{ collect: collectShellLexicalRanges },
{ className: 'variable', regex: /\$[A-Za-z_][A-Za-z0-9_]*|\b[A-Z_][A-Z0-9_]*(?==)/g },
{ className: 'keyword', regex: /\b(?:case|do|done|elif|else|esac|export|fi|for|function|if|in|select|then|time|until|while)\b/g },
{ className: 'command', regex: /(^|[\n;|&])\s*([A-Za-z0-9_./-]+)(?=\s|$)/g, group: 2 },
]),
});
function collectShellLexicalRanges(code) {
const heredocs = [];
collectHeredocs(code, heredocs);
const ranges = [...heredocs];
let heredocIndex = 0;
let index = 0;
while (index < code.length) {
while (heredocIndex < heredocs.length && heredocs[heredocIndex].end <= index) {
heredocIndex++;
}
const heredoc = heredocs[heredocIndex];
if (heredoc && heredoc.start <= index) {
index = heredoc.end;
continue;
}
const character = code[index];
if (character === '"' || character === "'") {
const quote = character;
let end = index + 1;
while (end < code.length) {
if (code[end] === '\\' && quote === '"')
end += 2;
else if (code[end] === quote) {
end++;
break;
}
else
end++;
}
end = Math.min(end, code.length);
ranges.push({ start: index, end, className: 'string' });
index = end;
continue;
}
if (character === '$' && code[index + 1] === '{') {
const close = code.indexOf('}', index + 2);
const end = close < 0 ? code.length : close + 1;
ranges.push({ start: index, end, className: 'variable' });
index = end;
continue;
}
if (character === '#' &&
(index === 0 || code[index - 1] === '\n' || /[\s;|&()]/.test(code[index - 1]))) {
const newline = code.indexOf('\n', index);
const end = newline < 0 ? code.length : newline;
ranges.push({ start: index, end, className: 'comment' });
index = end;
continue;
}
index++;
}
return ranges;
}
function collectHeredocs(code, ranges) {
const regex = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
let match;
while ((match = regex.exec(code))) {
if (code[match.index - 1] === '<' || isInsideQuote(code, match.index))
continue;
const bodyStartLine = code.indexOf('\n', match.index + match[0].length);
if (bodyStartLine < 0)
continue;
const bodyStart = bodyStartLine + 1;
const close = new RegExp(`^\\t*${match[2]}\\s*$`, 'm');
const closingMatch = close.exec(code.slice(bodyStart));
if (!closingMatch) {
ranges.push({ start: bodyStart, end: code.length, className: 'string' });
continue;
}
const end = bodyStart + closingMatch.index;
if (end > bodyStart)
ranges.push({ start: bodyStart, end, className: 'string' });
const closingStart = bodyStart + closingMatch.index;
ranges.push({
start: closingStart,
end: closingStart + closingMatch[0].length,
className: 'meta',
});
}
}
function isInsideQuote(code, index) {
const lineStart = code.lastIndexOf('\n', index - 1) + 1;
let quote = '';
for (let cursor = lineStart; cursor < index; cursor++) {
if (code[cursor] === '\\' && quote !== "'")
cursor++;
else if (quote && code[cursor] === quote)
quote = '';
else if (!quote && (code[cursor] === '"' || code[cursor] === "'")) {
quote = code[cursor];
}
}
return Boolean(quote);
}