feat: 代码片段功能与 Markdown 内嵌片段渲染、文档截图等

This commit is contained in:
2026-10-09 18:53:38 +08:00
parent 5598e1cbb7
commit da72843c70
64 changed files with 7047 additions and 248 deletions

View File

@@ -27,7 +27,8 @@ export type ContentSegment =
| { type: "timeline"; items: TimelineItem[]; rawBody: string }
| { type: "entries"; items: EntryItem[]; rawBody: string }
| { type: "details"; title: string; body: string }
| { type: "video"; provider: "bilibili" | "youtube"; id: string };
| { type: "video"; provider: "bilibili" | "youtube"; id: string }
| { type: "snippet"; id: number };
function isTimelineClose(line: string): boolean {
return line.trim().toLowerCase() === "[/timeline]";
@@ -57,6 +58,18 @@ export function parseDetailsOpen(trimmed: string): string {
return title || DETAILS_DEFAULT_TITLE;
}
/* ---------- [snippet id=N] 代码片段嵌入(整行单标记) ---------- */
const SNIPPET_LINE_RE = /^\[snippet\s+id=(\d+)\]$/i;
/** 整行 `[snippet id=N]`。解析失败返回 null(该行按普通 Markdown 处理)。 */
export function parseSnippetLine(trimmed: string): number | null {
const m = trimmed.match(SNIPPET_LINE_RE);
if (!m) return null;
const id = Number(m[1]);
return Number.isInteger(id) && id > 0 ? id : null;
}
/* ---------- [bilibili] / [youtube] 视频嵌入(整行单标记) ---------- */
const VIDEO_LINE_RE = /^\[(bilibili|youtube)\]([^\[\]]+)\[\/\1\]$/i;
@@ -122,6 +135,7 @@ export function splitContentSegments(
| { type: "entries"; body: string }
| { type: "details"; title: string; body: string }
| { type: "video"; provider: "bilibili" | "youtube"; id: string }
| { type: "snippet"; id: number }
> = [];
let buf: string[] = [];
let inCode = false;
@@ -228,6 +242,15 @@ export function splitContentSegments(
continue;
}
// [snippet id=N] 整行片段嵌入
const snippetId = parseSnippetLine(trimmed);
if (snippetId != null) {
flushMd();
raw.push({ type: "snippet", id: snippetId });
i++;
continue;
}
// [entries:off] 例外块:body 原样进 md 缓冲(标记行丢弃),
// 不再参与 entries/timeline 拆分;未闭合时整块当普通 Markdown
if (isEntriesOffOpen(trimmed)) {
@@ -273,6 +296,10 @@ export function splitContentSegments(
out.push({ type: "video", provider: part.provider, id: part.id });
continue;
}
if (part.type === "snippet") {
out.push({ type: "snippet", id: part.id });
continue;
}
const hideSegs = splitHideSegments(part.text) as HideSegment[];
for (const s of hideSegs) {
if (s.type === "md") {