增加密码解锁与游客签名 cookie;发帖页对齐 1440 并默认双栏预览;修复 locked 解析、按钮对比度与回复聚焦。 Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
4.3 KiB
TypeScript
179 lines
4.3 KiB
TypeScript
/** 正文 :::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"]);
|
||
|
||
function isClose(line: string): boolean {
|
||
return line.trim() === ":::";
|
||
}
|
||
|
||
/** 与 Go parseOpenMarker 对齐::::hide <kind> [points|password] [locked] */
|
||
function parseOpen(trimmed: string): {
|
||
kind: HideKind;
|
||
points: number;
|
||
locked: boolean;
|
||
} | null {
|
||
if (!trimmed.startsWith(":::hide")) return null;
|
||
const parts = trimmed
|
||
.slice(":::hide".length)
|
||
.trim()
|
||
.split(/\s+/)
|
||
.filter(Boolean);
|
||
if (parts.length === 0) return null;
|
||
const kind = parts[0] as HideKind;
|
||
if (!HIDE_KINDS.has(kind)) return null;
|
||
|
||
let idx = 1;
|
||
let points = 0;
|
||
let locked = false;
|
||
|
||
if (kind === "points") {
|
||
if (idx >= parts.length) return null;
|
||
const n = Number(parts[idx]);
|
||
if (!Number.isFinite(n) || n < 1 || n > 100000) return null;
|
||
points = n;
|
||
idx++;
|
||
} else if (kind === "password") {
|
||
// 脱敏::::hide password locked;原文::::hide password <pwd> [locked]
|
||
if (idx < parts.length && parts[idx] !== "locked") {
|
||
idx++;
|
||
}
|
||
}
|
||
|
||
for (; idx < parts.length; idx++) {
|
||
if (parts[idx] === "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 "密码可见";
|
||
}
|
||
}
|