Files
jiang13-bbs/frontend/lib/directiveBlocks.ts
freefire a83276060f feat: 站点单页、时间线导入与行级 BBCode 短代码
首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:52:06 +08:00

206 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 统一拆分正文中的 [hide] / [timeline] 指令块(忽略代码围栏) */
import {
splitHideSegments,
type HideKind,
type HideSegment,
} from "@/lib/hideBlocks";
import { parseTimelineBody, type TimelineItem } from "@/lib/timelineBlocks";
export type ContentSegment =
| { type: "md"; text: string }
| {
type: "hide";
kind: HideKind;
points: number;
locked: boolean;
body: string;
}
| { type: "timeline"; items: TimelineItem[]; rawBody: string };
function isTimelineClose(line: string): boolean {
return line.trim().toLowerCase() === "[/timeline]";
}
function isTimelineOpen(trimmed: string): boolean {
return trimmed.toLowerCase() === "[timeline]";
}
/**
* 先拆时间线,再对普通 md/hide 段做 hide 拆分。
* 时间线与 hide 互不嵌套;解析失败时整段当普通 Markdown。
*/
export function splitContentSegments(content: string): ContentSegment[] {
if (!content) return [{ type: "md", text: "" }];
const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
const raw: Array<{ type: "md"; text: string } | { type: "timeline"; body: string }> = [];
let buf: string[] = [];
let inCode = false;
let i = 0;
const flushMd = () => {
if (buf.length === 0) return;
raw.push({ type: "md", text: buf.join("\n") });
buf = [];
};
while (i < lines.length) {
const trimmed = lines[i].trim();
if (trimmed.startsWith("```")) {
inCode = !inCode;
buf.push(lines[i]);
i++;
continue;
}
if (inCode) {
buf.push(lines[i]);
i++;
continue;
}
if (isTimelineOpen(trimmed)) {
flushMd();
const bodyLines: string[] = [];
let j = i + 1;
let innerCode = false;
let found = false;
while (j < lines.length) {
const inner = lines[j].trim();
if (inner.startsWith("```")) {
innerCode = !innerCode;
bodyLines.push(lines[j]);
j++;
continue;
}
if (innerCode) {
bodyLines.push(lines[j]);
j++;
continue;
}
if (isTimelineOpen(inner)) {
// 嵌套视为失败:整段当 md
buf.push(lines[i]);
i++;
found = false;
break;
}
if (isTimelineClose(inner)) {
found = true;
break;
}
bodyLines.push(lines[j]);
j++;
}
if (!found) {
buf.push(lines[i]);
i++;
continue;
}
raw.push({ type: "timeline", body: bodyLines.join("\n") });
i = j + 1;
continue;
}
buf.push(lines[i]);
i++;
}
flushMd();
const out: ContentSegment[] = [];
for (const part of raw) {
if (part.type === "timeline") {
out.push({
type: "timeline",
items: parseTimelineBody(part.body),
rawBody: part.body,
});
continue;
}
const hideSegs = splitHideSegments(part.text) as HideSegment[];
for (const s of hideSegs) {
if (s.type === "md") {
out.push({ type: "md", text: s.text });
} else {
out.push({
type: "hide",
kind: s.kind,
points: s.points,
locked: s.locked,
body: s.body,
});
}
}
}
return out.length > 0 ? out : [{ type: "md", text: content }];
}
/** 光标是否落在某个 [timeline]…[/timeline] 块内;返回块起止与正文 */
export function findTimelineBlockAt(
content: string,
cursor: number
): { start: number; end: number; body: string } | null {
const blocks = listTimelineBlocks(content);
for (const b of blocks) {
if (cursor >= b.start && cursor <= b.end) return b;
}
return null;
}
/**
* 编辑器打开时间线面板时的目标块:
* 1) 光标落在某块内 → 该块
* 2) 正文恰有一块 → 该块(changelog 页常见:光标在标题区仍应更新已有时间线)
* 3) 否则 null(新建插入)
*/
export function resolveTimelineBlockForEdit(
content: string,
cursor: number
): { start: number; end: number; body: string } | null {
const at = findTimelineBlockAt(content, cursor);
if (at) return at;
const blocks = listTimelineBlocks(content);
if (blocks.length === 1) return blocks[0];
return null;
}
function listTimelineBlocks(
content: string
): Array<{ start: number; end: number; body: string }> {
const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
const out: Array<{ start: number; end: number; body: string }> = [];
let offset = 0;
let inCode = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineStart = offset;
const lineEnd = offset + line.length;
const trimmed = line.trim();
if (trimmed.startsWith("```")) inCode = !inCode;
if (!inCode && isTimelineOpen(trimmed)) {
const openStart = lineStart;
let j = i + 1;
let innerCode = false;
let closeEnd = -1;
const bodyLines: string[] = [];
let pos = lineEnd + 1;
while (j < lines.length) {
const l = lines[j];
const t = l.trim();
if (t.startsWith("```")) innerCode = !innerCode;
if (!innerCode && isTimelineClose(t)) {
closeEnd = pos + l.length;
break;
}
bodyLines.push(l);
pos += l.length + 1;
j++;
}
if (closeEnd >= 0) {
out.push({ start: openStart, end: closeEnd, body: bodyLines.join("\n") });
}
}
offset = lineEnd + 1;
}
return out;
}