Files
jiang13-bbs/frontend/lib/hideBlocks.ts
freefire d784b0ea7a feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 05:27:33 +08:00

193 lines
4.6 KiB
TypeScript
Raw Permalink 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<string>(["login", "reply", "points", "password"]);
const HIDE_OPEN_RE = /^\[hide(?:\s+(.+))?\]$/i;
function isHideKind(s: string): s is HideKind {
return HIDE_KINDS.has(s);
}
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();
if (!isHideKind(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 "密码可见";
}
}