feat: 代码片段功能与 Markdown 内嵌片段渲染、文档截图等
This commit is contained in:
@@ -7,14 +7,21 @@
|
||||
* default: /post/123 /u/5 /announcement/1 /p/about /leaderboard /library /library/{slug} /library/{slug}/read?c={书内章号}
|
||||
* html: /post-123.html /user-5.html /announcement-1.html /p-about.html /leaderboard.html /library.html /library-{slug}.html (章节:/library-{slug}.html?sec={书内章号})
|
||||
* discuz: /thread-123.html /space-uid-5.html /notice-1.html /page-about.html /rank.html (书库两风格同 html 模板)
|
||||
* 书库章节走详情路径 + ?sec= 查询参数(章内子目录锚点 #e-{sec}-{章内序号});旧版 -read-{n} 路径
|
||||
* 不再生成,但正则保留可解析(middleware 改写回标准读页),旧链接不失效。
|
||||
* 代码片段:default /snippets /snippets/{id};html/discuz 两风格统一 /snippets.html /snippet-{id}.html
|
||||
*/
|
||||
|
||||
export type UrlStyle = "default" | "html" | "discuz";
|
||||
export type EntityKind = "post" | "user" | "announcement";
|
||||
/** 全部可反解的实体类型;page/libraryDoc/libraryRead 的 id 是 slug(字符串),leaderboard/library 是固定单页无 id */
|
||||
export type PageKind = EntityKind | "page" | "leaderboard" | "library" | "libraryDoc" | "libraryRead";
|
||||
/** 全部可反解的实体类型;page/libraryDoc/libraryRead 的 id 是 slug(字符串),leaderboard/library/snippets 是固定单页无 id */
|
||||
export type PageKind =
|
||||
| EntityKind
|
||||
| "page"
|
||||
| "leaderboard"
|
||||
| "library"
|
||||
| "libraryDoc"
|
||||
| "libraryRead"
|
||||
| "snippets"
|
||||
| "snippet";
|
||||
|
||||
export const URL_STYLES: readonly UrlStyle[] = ["default", "html", "discuz"];
|
||||
|
||||
@@ -64,6 +71,18 @@ const LIBRARY_TEMPLATES: Record<Exclude<UrlStyle, "default">, { list: string; do
|
||||
},
|
||||
};
|
||||
|
||||
/** 代码片段伪静态模板:两风格统一(同书库,非 Discuz 原生概念) */
|
||||
const SNIPPET_TEMPLATES: Record<Exclude<UrlStyle, "default">, { list: string; detail: (id: number) => string }> = {
|
||||
html: {
|
||||
list: "/snippets.html",
|
||||
detail: (id) => `/snippet-${id}.html`,
|
||||
},
|
||||
discuz: {
|
||||
list: "/snippets.html",
|
||||
detail: (id) => `/snippet-${id}.html`,
|
||||
},
|
||||
};
|
||||
|
||||
/** id 非法(≤0/非有限数)时返回空串,调用方按异常路径处理 */
|
||||
function guardId(id: number): number | null {
|
||||
return Number.isFinite(id) && id > 0 ? id : null;
|
||||
@@ -155,6 +174,19 @@ export function libraryDocReadPath(style: UrlStyle, slug: string, chapterNo?: nu
|
||||
return no ? `${base}?c=${no}` : base;
|
||||
}
|
||||
|
||||
/** 片段列表路径:default 标准路径,html/discuz 伪静态(两风格统一模板) */
|
||||
export function snippetsPath(style: UrlStyle): string {
|
||||
return style === "default" ? "/snippets" : SNIPPET_TEMPLATES[style].list;
|
||||
}
|
||||
|
||||
/** 片段详情路径;id 非法(≤0/非有限数)时返回空串 */
|
||||
export function snippetPath(style: UrlStyle, id: number): string {
|
||||
const n = guardId(id);
|
||||
if (n == null) return "";
|
||||
if (style === "default") return `/snippets/${n}`;
|
||||
return SNIPPET_TEMPLATES[style].detail(n);
|
||||
}
|
||||
|
||||
/** 实体对应风格下的标准/pretty 路径(内部与 middleware 共用) */
|
||||
export function entityPath(style: UrlStyle, kind: EntityKind, id: number): string {
|
||||
if (kind === "post") return postPath(style, id);
|
||||
@@ -173,7 +205,9 @@ export type PrettyMatch =
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "libraryDoc"; slug: string }
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "libraryRead"; slug: string; id: number }
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "library" }
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "leaderboard" };
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "leaderboard" }
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "snippets" }
|
||||
| { style: Exclude<UrlStyle, "default">; kind: "snippet"; id: number };
|
||||
|
||||
// 精确锚定:id 仅纯数字,天然排除 /post/123/edit 这类子路径与伪造路径;
|
||||
// page/libraryDoc 的 slug 捕获保持 URL 原样(encoded),rewrite 落点直接复用。
|
||||
@@ -188,6 +222,8 @@ const PRETTY_PATTERNS: { re: RegExp; style: Exclude<UrlStyle, "default">; kind:
|
||||
{ re: /^\/library\.html$/, style: "html", kind: "library" },
|
||||
{ re: /^\/library-(.+)-read-(\d+)\.html$/, style: "html", kind: "libraryRead" },
|
||||
{ re: /^\/library-([^/]+)\.html$/, style: "html", kind: "libraryDoc" },
|
||||
{ re: /^\/snippets\.html$/, style: "html", kind: "snippets" },
|
||||
{ re: /^\/snippet-(\d+)\.html$/, style: "html", kind: "snippet" },
|
||||
{ re: /^\/thread-(\d+)\.html$/, style: "discuz", kind: "post" },
|
||||
{ re: /^\/space-uid-(\d+)\.html$/, style: "discuz", kind: "user" },
|
||||
{ re: /^\/notice-(\d+)\.html$/, style: "discuz", kind: "announcement" },
|
||||
@@ -196,6 +232,8 @@ const PRETTY_PATTERNS: { re: RegExp; style: Exclude<UrlStyle, "default">; kind:
|
||||
{ re: /^\/library\.html$/, style: "discuz", kind: "library" },
|
||||
{ re: /^\/library-(.+)-read-(\d+)\.html$/, style: "discuz", kind: "libraryRead" },
|
||||
{ re: /^\/library-([^/]+)\.html$/, style: "discuz", kind: "libraryDoc" },
|
||||
{ re: /^\/snippets\.html$/, style: "discuz", kind: "snippets" },
|
||||
{ re: /^\/snippet-(\d+)\.html$/, style: "discuz", kind: "snippet" },
|
||||
];
|
||||
|
||||
const STANDARD_PATTERNS: { re: RegExp; kind: PageKind }[] = [
|
||||
@@ -208,6 +246,9 @@ const STANDARD_PATTERNS: { re: RegExp; kind: PageKind }[] = [
|
||||
{ re: /^\/library\/?$/, kind: "library" },
|
||||
{ re: /^\/library\/([^/]+)\/read$/, kind: "libraryRead" },
|
||||
{ re: /^\/library\/([^/]+)$/, kind: "libraryDoc" },
|
||||
// (\d+)$ 纯数字锚定:/snippets/new 与 /snippets/123/edit 不会命中详情模式
|
||||
{ re: /^\/snippets\/?$/, kind: "snippets" },
|
||||
{ re: /^\/snippets\/(\d+)$/, kind: "snippet" },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -230,7 +271,7 @@ export function matchPrettyPath(
|
||||
hit = { style: p.style, kind: p.kind, slug: m[1] };
|
||||
} else if (p.kind === "libraryRead") {
|
||||
hit = { style: p.style, kind: p.kind, slug: m[1], id: Number(m[2]) };
|
||||
} else if (p.kind === "library" || p.kind === "leaderboard") {
|
||||
} else if (p.kind === "library" || p.kind === "leaderboard" || p.kind === "snippets") {
|
||||
hit = { style: p.style, kind: p.kind };
|
||||
} else {
|
||||
hit = { style: p.style, kind: p.kind, id: Number(m[1]) };
|
||||
@@ -249,7 +290,7 @@ export function matchStandardPath(pathname: string): StandardMatch | null {
|
||||
if (p.kind === "page" || p.kind === "libraryDoc" || p.kind === "libraryRead") {
|
||||
return { kind: p.kind, slug: m[1] };
|
||||
}
|
||||
if (p.kind === "library" || p.kind === "leaderboard") return { kind: p.kind };
|
||||
if (p.kind === "library" || p.kind === "leaderboard" || p.kind === "snippets") return { kind: p.kind };
|
||||
return { kind: p.kind, id: Number(m[1]) };
|
||||
}
|
||||
return null;
|
||||
@@ -261,7 +302,9 @@ export type StandardMatch =
|
||||
| { kind: "libraryDoc"; slug: string }
|
||||
| { kind: "libraryRead"; slug: string }
|
||||
| { kind: "library" }
|
||||
| { kind: "leaderboard" };
|
||||
| { kind: "leaderboard" }
|
||||
| { kind: "snippets" }
|
||||
| { kind: "snippet"; id: number };
|
||||
|
||||
/**
|
||||
* 反解任意实体路径(标准 + 全部伪静态风格),供 isActive 前缀判断、面包屑等
|
||||
@@ -281,6 +324,7 @@ export function parseEntityPath(pathname: string): StandardMatch | null {
|
||||
if (pretty.kind === "libraryRead") return { kind: "libraryRead", slug: decodeSlug(pretty.slug) };
|
||||
if (pretty.kind === "leaderboard") return { kind: "leaderboard" };
|
||||
if (pretty.kind === "library") return { kind: "library" };
|
||||
if (pretty.kind === "snippets") return { kind: "snippets" };
|
||||
return { kind: pretty.kind, id: pretty.id };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user