feat: 品牌裁剪弹窗/Markdown 行内短代码与详情视频块等
新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
This commit is contained in:
75
frontend/lib/inlineShortcodes.ts
Normal file
75
frontend/lib/inlineShortcodes.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 行内短代码解析(纯函数):
|
||||
* ==高亮== / [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;
|
||||
}
|
||||
Reference in New Issue
Block a user