Files
jiang13-bbs/frontend/lib/hideBlocks.ts
freefire 01e2fd05ca feat(compose): 正文内嵌隐藏块并升级 Markdown 编辑器
去掉整帖可见性表单,改为 :::hide 按块脱敏;工具栏补齐代码块、链接/图片对话框与粘贴拖拽上传。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 04:58:20 +08:00

155 lines
3.8 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 块解析(与 backend/markdown/hide.go 契约一致) */
export type HideKind = "login" | "reply" | "points";
export type HideSegment =
| { type: "md"; text: string }
| {
type: "hide";
kind: HideKind;
points: number;
locked: boolean;
body: string;
};
const OPEN_RE = /^:::hide\s+(login|reply|points)(?:\s+(\d+))?(?:\s+(locked))?\s*$/;
function isClose(line: string): boolean {
return line.trim() === ":::";
}
function parseOpen(trimmed: string): {
kind: HideKind;
points: number;
locked: boolean;
} | null {
const m = OPEN_RE.exec(trimmed);
if (!m) return null;
const kind = m[1] as HideKind;
const points = m[2] ? Number(m[2]) : 0;
const locked = m[3] === "locked";
if (kind === "points" && (!Number.isFinite(points) || points < 1)) {
return null;
}
return { kind, points: kind === "points" ? points : 0, 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 "积分可见";
}
}