const PROMPT_RE = /(?:^|\s)([~/][^$#❯➜›▶›⟩>]*)?\s*[#$❯➜›▶➤⟩>]\s*$/u;
const HEADER_BRACKET_RE = /^\s*\[[A-Za-z][A-Za-z0-9 _-]*\]\s*$/;
const HEADER_HASH_RE = /^\s*#{1,4}\s+\S/;
const BOX_DRAW_RE = /[\u2500-\u257F=\u2500\u2501\u2550]/g;
const FENCE_RE = /^\s*(?:```|~~~)\s*([A-Za-z0-9_+-]*)/;
function isRule(text) {
const trimmed = text.trim();
if (trimmed.length < 8) return false;
const hits = (trimmed.match(BOX_DRAW_RE) || []).length;
return hits / trimmed.length > 0.7;
}
function isPrompt(text) {
if (text.length === 0) return false;
const trimmedRight = text.replace(/\s+$/u, "");
const last = trimmedRight.slice(-1);
if ("#$>❯➜›▶➤⟩".indexOf(last) === -1) return false;
return PROMPT_RE.test(trimmedRight);
}
function isHeader(text) {
return HEADER_BRACKET_RE.test(text) || HEADER_HASH_RE.test(text);
}
function oscHas(osc, kind) {
if (!osc) return false;
for (const part of osc.split("|")) {
if (part.charAt(0) === kind) return true;
}
return false;
}
function oscExitCode(osc) {
if (!osc) return null;
for (const part of osc.split("|")) {
if (part.charAt(0) !== "D") continue;
const n = Number(part.slice(2));
return Number.isFinite(n) ? n : null;
}
return null;
}
function findCommandSpan(lines, promptIndex) {
const first = lines[promptIndex + 1];
if (!first || !oscHas(first.osc, "C")) return null;
let end = promptIndex + 1;
while (end < lines.length && !oscHas(lines[end].osc, "D")) end++;
const exitCode = end < lines.length ? oscExitCode(lines[end].osc) : null;
return {
outputLines: lines.slice(promptIndex + 1, end),
endIndex: end,
exitCode,
};
}
function lineBubbleBg(runs) {
let bg = null;
let sawContent = false;
for (const r of runs) {
if (!r.text || r.text.trim().length === 0) continue;
sawContent = true;
if (r.attrs.bg === null) return null;
if (bg === null) bg = r.attrs.bg;
else if (bg !== r.attrs.bg) return null;
}
return sawContent ? bg : null;
}
export function tokenize(lines) {
const blocks = [];
let inFence = false;
let codeLines = [];
let fenceLanguage = "";
let i = 0;
function flushCode() {
if (codeLines.length === 0) return;
blocks.push({ type: "code", lines: codeLines, language: fenceLanguage });
codeLines = [];
fenceLanguage = "";
}
function pushTextLine(line) {
const last = blocks[blocks.length - 1];
if (last && last.type === "text") last.lines.push(line);
else blocks.push({ type: "text", lines: [line] });
}
while (i < lines.length) {
const { runs, text, osc } = lines[i];
const fence = FENCE_RE.exec(text);
if (fence) {
if (inFence) {
flushCode();
inFence = false;
} else {
inFence = true;
fenceLanguage = (fence[1] || "").toLowerCase();
}
i++;
continue;
}
if (inFence) {
codeLines.push({ runs, text, bubbleBg: lineBubbleBg(runs) });
i++;
continue;
}
if (text.trim().length === 0) {
blocks.push({ type: "blank" });
i++;
continue;
}
if (isRule(text)) {
blocks.push({ type: "rule" });
i++;
continue;
}
if (isHeader(text)) {
blocks.push({ type: "header", runs, text });
i++;
continue;
}
const isOscPrompt = oscHas(osc, "A");
if (isOscPrompt) {
const span = findCommandSpan(lines, i);
if (span) {
blocks.push({
type: "command",
runs,
text,
lines: span.outputLines.map((l) => ({
runs: l.runs,
text: l.text,
bubbleBg: lineBubbleBg(l.runs),
})),
exitCode: span.exitCode,
});
i = span.endIndex;
continue;
}
}
if (isOscPrompt || isPrompt(text)) {
blocks.push({ type: "prompt", runs, text });
i++;
continue;
}
pushTextLine({ runs, text, bubbleBg: lineBubbleBg(runs) });
i++;
}
if (inFence) flushCode();
return blocks;
}
export const _internals = {
isRule,
isPrompt,
isHeader,
lineBubbleBg,
oscHas,
oscExitCode,
findCommandSpan,
};