/** * 行内短代码解析(纯函数): * ==高亮== / [kbd]Ctrl[/kbd] / [color=前景( bg=背景)]文字[/color] / [bg=背景]文字[/bg] * 颜色值白名单(#hex 或 CSS 颜色名),非法值整段按普通文本回退,防 CSS 注入。 */ export type InlineShortcodeSegment = | { type: "text"; value: string } | { type: "mark"; value: string } | { type: "kbd"; value: string } | { type: "color"; value: string; fg?: string; bg?: string }; export const COLOR_VALUE_RE = /^(#[0-9a-fA-F]{3,8}|[a-zA-Z]{1,32})$/; export function isValidColorValue(v: string): boolean { return COLOR_VALUE_RE.test(v); } /** * 扫描顺序:[kbd] → [color] → [bg] → ==…==。 * 标签内容不含 [ 与换行,避免嵌套歧义;未闭合标记保持字面。 * 无任何短代码命中时返回 null(调用方保留原节点)。 */ const SCAN_RE = /\[kbd\]([^\[\]\n]+)\[\/kbd\]|\[color=([^\]\s\[]+)(?:\s+bg=([^\]\s\[]+))?\]([^\[\]\n]+)\[\/color\]|\[bg=([^\]\s\[]+)\]([^\[\]\n]+)\[\/bg\]|==([^=\n]+)==/gi; export function splitInlineShortcodes( text: string ): InlineShortcodeSegment[] | null { if (!text) return null; const re = new RegExp(SCAN_RE.source, "gi"); const out: InlineShortcodeSegment[] = []; let last = 0; let changed = false; let m: RegExpExecArray | null; while ((m = re.exec(text))) { const [full, kbd, colorFg, colorBg, colorBody, bgVal, bgBody, markBody] = m; const before = text.slice(last, m.index); if (before) out.push({ type: "text", value: before }); last = m.index + full.length; if (kbd !== undefined) { out.push({ type: "kbd", value: kbd }); changed = true; continue; } if (colorFg !== undefined) { if (!isValidColorValue(colorFg) || (colorBg !== undefined && !isValidColorValue(colorBg))) { out.push({ type: "text", value: full }); continue; } out.push( colorBg ? { type: "color", value: colorBody, fg: colorFg, bg: colorBg } : { type: "color", value: colorBody, fg: colorFg } ); changed = true; continue; } if (bgVal !== undefined) { if (!isValidColorValue(bgVal)) { out.push({ type: "text", value: full }); continue; } out.push({ type: "color", value: bgBody, bg: bgVal }); changed = true; continue; } out.push({ type: "mark", value: markBody }); changed = true; } const rest = text.slice(last); if (rest) out.push({ type: "text", value: rest }); return changed ? out : null; }