feat: improve markdown rendering, artifact file preview, and public scheduled task page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-04 16:52:30 +08:00
1 parent 94c02ded2a
commit 87f764bfa9
5 files changed
+419 -74

No files matched your search

+125 -8
View File
@@ -1,10 +1,127 @@
export function extractTitleFromMarkdown(markdown: string) {
if (markdown.startsWith("# ")) {
let title = markdown.split("\n")[0]!.trim();
if (title.startsWith("# ")) {
title = title.slice(2).trim();
export function extractTitleFromMarkdown(markdown: string) {
if (markdown.startsWith("# ")) {
let title = markdown.split("\n")[0]!.trim();
if (title.startsWith("# ")) {
title = title.slice(2).trim();
}
return title;
}
return undefined;
}
}
return undefined;
}
export type MarkdownHeadingAnchor = {
level: number;
text: string;
id: string;
};
export type MarkdownAnchorResolver = {
headings: MarkdownHeadingAnchor[];
getHeadingId: (level: number, text: string) => string | undefined;
resolveHref: (href: string | undefined, label?: string) => string | null | undefined;
};
const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
const PROTOCOL_RE = /^[a-z][a-z\d+.-]*:/i;
function stripInlineMarkdown(value: string): string {
return value
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/[`*_~]/g, "")
.replace(/<[^>]+>/g, "")
.trim();
}
export function markdownSlug(value: string): string {
const cleaned = stripInlineMarkdown(value)
.normalize("NFKC")
.toLowerCase()
.replace(/[^\p{L}\p{N}\s._-]+/gu, "")
.trim()
.replace(/[\s._-]+/g, "-")
.replace(/^-+|-+$/g, "");
return cleaned || "section";
}
function decodeLinkPart(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function normalizeLookupKey(value: string): string {
return markdownSlug(decodeLinkPart(value).replace(/^#+/, ""));
}
function isExternalOrSpecialHref(href: string): boolean {
if (href.startsWith("//")) return true;
if (PROTOCOL_RE.test(href)) return true;
return href.startsWith("/") || href.startsWith("?");
}
function candidateTextFromHref(href: string): string {
const withoutQuery = href.split(/[?#]/)[0] ?? href;
const lastSegment = withoutQuery.split("/").filter(Boolean).pop() ?? withoutQuery;
return decodeLinkPart(lastSegment).replace(/\.(md|markdown)$/i, "");
}
export function createMarkdownAnchorResolver(markdown: string): MarkdownAnchorResolver {
const headings: MarkdownHeadingAnchor[] = [];
const idsByLookup = new Map<string, string>();
const seen = new Map<string, number>();
let inFence = false;
for (const rawLine of markdown.split(/\r?\n/)) {
const line = rawLine.trim();
if (/^(```|~~~)/.test(line)) {
inFence = !inFence;
continue;
}
if (inFence) continue;
const match = HEADING_RE.exec(rawLine);
if (!match) continue;
const level = match[1]!.length;
const text = stripInlineMarkdown(match[2]!);
const baseId = markdownSlug(text);
const count = seen.get(baseId) ?? 0;
seen.set(baseId, count + 1);
const id = count === 0 ? baseId : `${baseId}-${count}`;
headings.push({ level, text, id });
idsByLookup.set(normalizeLookupKey(text), id);
idsByLookup.set(normalizeLookupKey(id), id);
}
return {
headings,
getHeadingId: (level: number, text: string) => {
void level;
return idsByLookup.get(normalizeLookupKey(text));
},
resolveHref: (href: string | undefined, label?: string) => {
if (!href) return href;
const trimmed = href.trim();
if (!trimmed || isExternalOrSpecialHref(trimmed)) return href;
const hash = trimmed.includes("#") ? trimmed.slice(trimmed.indexOf("#") + 1) : "";
const candidates = [
hash,
trimmed.startsWith("#") ? trimmed.slice(1) : "",
candidateTextFromHref(trimmed),
label ?? "",
].filter(Boolean);
for (const candidate of candidates) {
const id = idsByLookup.get(normalizeLookupKey(candidate));
if (id) return `#${id}`;
}
return trimmed.startsWith("#") ? null : null;
},
};
}