新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
/**
|
||
* 行内短代码解析(纯函数):
|
||
* ==高亮== / [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;
|
||
}
|