Files
jiang13-bbs/frontend/lib/toc.ts
freefire 0b60c4299b feat: 公文三栏单页布局,并修复时间线目录跳转与样式
公告/单页对齐帖子详情三栏壳;时间线按日锚点可跳转,条目改为日志行并用暖金日期区分 feat 色。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 21:15:15 +08:00

103 lines
3.0 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.
import type { TocItem } from "@/components/PostToc";
const TIMELINE_DAY_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s+(.+)$/;
const HEADING_RE = /^(#{2,3})\s+(.+?)\s*#*\s*$/;
const HIDE_OPEN_RE = /^\[hide(?:\s+.+)?\]$/i;
/** 时间线日分组锚点 id(与 MdTimeline 一致) */
export function timelineDayId(date: string): string {
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) return `tl-${date}`;
return `tl-${date.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown"}`;
}
/**
* 从正文提取目录:
* - 块外 ## / ### → h-{n}(供 headingRenderer 按序挂 id)
* - [timeline] 内按日期去重 → tl-YYYY-MM-DD(日分组锚点)
* - 代码围栏 / [hide] 块内标题不计入
*/
export function extractToc(content: string): TocItem[] {
const items: TocItem[] = [];
let inCode = false;
let inTimeline = false;
let inHide = false;
let headingCount = 0;
/** 当前 timeline 块内出现过的日期(保持文件顺序,收块时再按日倒序发出) */
let timelineDates: string[] = [];
const timelineSeen = new Set<string>();
const flushTimelineDates = () => {
if (timelineDates.length === 0) return;
const sorted = [...timelineDates].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
for (const date of sorted) {
items.push({ level: 2, text: date, id: timelineDayId(date) });
}
timelineDates = [];
timelineSeen.clear();
};
for (const raw of content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n")) {
const line = raw.trim();
if (line.startsWith("```")) {
inCode = !inCode;
continue;
}
if (inCode) continue;
const lower = line.toLowerCase();
if (!inHide && !inTimeline && lower === "[timeline]") {
inTimeline = true;
continue;
}
if (inTimeline && lower === "[/timeline]") {
inTimeline = false;
flushTimelineDates();
continue;
}
if (inTimeline) {
const tm = TIMELINE_DAY_RE.exec(line);
if (tm && !timelineSeen.has(tm[1])) {
timelineSeen.add(tm[1]);
timelineDates.push(tm[1]);
}
continue;
}
if (!inHide && HIDE_OPEN_RE.test(line)) {
inHide = true;
continue;
}
if (inHide && lower === "[/hide]") {
inHide = false;
continue;
}
if (inHide) continue;
const m = HEADING_RE.exec(line);
if (m) {
items.push({
level: m[1].length as 2 | 3,
text: m[2],
id: `h-${headingCount++}`,
});
}
}
// 未闭合的 timeline 也尽量发出已收集日期
flushTimelineDates();
return items;
}
/** 仅正文 Markdown 标题(排除时间线日锚点),供 headingRenderer 按序挂 id */
export function markdownHeadingToc(toc: TocItem[]): TocItem[] {
return toc.filter((t) => /^h-\d+$/.test(t.id));
}
/** 粗略阅读时长:约 400 字/分钟,至少 1 分钟 */
export function estimateReadingMinutes(text: string, fallbackLen = 0): number {
const len = text.trim().length > 0 ? text.trim().length : fallbackLen;
return Math.max(1, Math.ceil(len / 400));
}