Files
jiang13-bbs/frontend/lib/hideBlocks.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

189 lines
4.5 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]…[/hide] 块解析(与 backend/markdown/hide.go 契约一致) */
export type HideKind = "login" | "reply" | "points" | "password";
export type HideSegment =
| { type: "md"; text: string }
| {
type: "hide";
kind: HideKind;
points: number;
locked: boolean;
body: string;
};
const HIDE_KINDS = new Set<HideKind>(["login", "reply", "points", "password"]);
const HIDE_OPEN_RE = /^\[hide(?:\s+(.+))?\]$/i;
function isClose(line: string): boolean {
return line.trim().toLowerCase() === "[/hide]";
}
/** 与 Go parseOpenMarker 对齐:[hide <kind>[=arg] [locked]] */
function parseOpen(trimmed: string): {
kind: HideKind;
points: number;
locked: boolean;
} | null {
const m = trimmed.match(HIDE_OPEN_RE);
if (!m) return null;
const rest = (m[1] || "").trim();
if (!rest) return null;
const parts = rest.split(/\s+/).filter(Boolean);
if (parts.length === 0) return null;
let head = parts[0];
let kind = head;
let arg = "";
const eq = head.indexOf("=");
if (eq >= 0) {
kind = head.slice(0, eq);
arg = head.slice(eq + 1);
}
kind = kind.toLowerCase() as HideKind;
if (!HIDE_KINDS.has(kind)) return null;
let idx = 1;
let points = 0;
let locked = false;
if (kind === "points") {
if (!arg) return null;
const n = Number(arg);
if (!Number.isFinite(n) || n < 1 || n > 100000) return null;
points = n;
} else if (kind === "password") {
// 脱敏:[hide password locked];原文密码不在前端解析保留
if (arg) {
/* skip plaintext */
}
} else if (arg) {
return null;
}
for (; idx < parts.length; idx++) {
if (parts[idx].toLowerCase() === "locked") locked = true;
else return null;
}
return { kind, points, locked };
}
/**
* 将正文拆成公开 Markdown 段与隐藏块(忽略代码围栏内伪语法)。
* 解析失败时整篇当作普通 Markdown,避免渲染崩溃。
*/
export function splitHideSegments(content: string): HideSegment[] {
if (!content) return [{ type: "md", text: "" }];
const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
const out: HideSegment[] = [];
let buf: string[] = [];
let inCode = false;
let i = 0;
const flushMd = () => {
if (buf.length === 0) return;
out.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;
}
const open = parseOpen(trimmed);
if (open) {
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 (parseOpen(inner)) {
buf.push(lines[i]);
i++;
found = false;
break;
}
if (isClose(inner)) {
found = true;
break;
}
bodyLines.push(lines[j]);
j++;
}
if (!found) {
buf.push(lines[i]);
i++;
continue;
}
out.push({
type: "hide",
kind: open.kind,
points: open.points,
locked: open.locked,
body: bodyLines.join("\n"),
});
i = j + 1;
continue;
}
buf.push(lines[i]);
i++;
}
flushMd();
return out.length > 0 ? out : [{ type: "md", text: content }];
}
/** 去掉锁块与标记后,供 SEO / TOC / description 使用的公开纯文本片段 */
export function publicMarkdownForSeo(content: string): string {
const segs = splitHideSegments(content);
return segs
.filter((s): s is Extract<HideSegment, { type: "md" }> => s.type === "md")
.map((s) => s.text)
.concat(
segs
.filter(
(s): s is Extract<HideSegment, { type: "hide" }> =>
s.type === "hide" && !s.locked
)
.map((s) => s.body)
)
.join("\n\n")
.trim();
}
export function hideKindLabel(kind: HideKind): string {
switch (kind) {
case "login":
return "登录可见";
case "reply":
return "回复可见";
case "points":
return "积分可见";
case "password":
return "密码可见";
}
}