feat(compose): 正文内嵌隐藏块并升级 Markdown 编辑器
去掉整帖可见性表单,改为 :::hide 按块脱敏;工具栏补齐代码块、链接/图片对话框与粘贴拖拽上传。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
154
frontend/lib/hideBlocks.ts
Normal file
154
frontend/lib/hideBlocks.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/** 正文 :::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 "积分可见";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user