Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
解决 artifact 预览组件冲突:mermaid 渲染(dhk-1) 与 markdown 锚点跳转(master) 取并集;package-lock.json 按 master 删除(项目用 pnpm)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
b2132dd668
104 files changed
+8775
-12038
No files matched your search
@@ -1,10 +1,196 @@
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** True for hrefs that point to an anchor within the current document (`#section`). */
|
||||
export function isInPageAnchor(href: string | undefined | null): href is string {
|
||||
return !!href && href.startsWith("#") && !href.startsWith("#reference-");
|
||||
}
|
||||
|
||||
function escapeAttrValue(value: string): string {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to an in-page anchor by id without mutating `window.location.hash`.
|
||||
*
|
||||
* The app runs under a HashRouter, so letting an `<a href="#...">` click change
|
||||
* the hash would be interpreted as a route change and 404. We resolve the target
|
||||
* element ourselves and scroll to it instead. Scoped to the nearest
|
||||
* `[data-markdown-anchor-root]` so headings in other messages don't collide.
|
||||
*/
|
||||
export function scrollToInPageAnchor(anchor: HTMLElement, href: string | undefined | null): void {
|
||||
if (!isInPageAnchor(href)) return;
|
||||
const id = decodeLinkPart(href.slice(1)).trim();
|
||||
if (!id) return;
|
||||
const scope = anchor.closest<HTMLElement>("[data-markdown-anchor-root]");
|
||||
const selector = `[id="${escapeAttrValue(id)}"]`;
|
||||
const target =
|
||||
(scope?.querySelector<HTMLElement>(selector) ?? null) ??
|
||||
document.getElementById(id) ??
|
||||
document.querySelector<HTMLElement>(selector);
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
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, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a small runtime script into generated HTML so that clicking an
|
||||
* in-page anchor (`<a href="#section">`) scrolls within the iframe instead of
|
||||
* navigating it. The preview iframe loads from a sandboxed `blob:` URL, where a
|
||||
* fragment navigation blanks the frame; intercepting the click avoids that.
|
||||
*/
|
||||
export function injectInPageAnchorHandler(html: string): string {
|
||||
const script = `<script>(function(){
|
||||
function decode(v){try{return decodeURIComponent(v);}catch(e){return v;}}
|
||||
function bySelector(id){
|
||||
if(!document.querySelector)return null;
|
||||
var esc=window.CSS&&CSS.escape?CSS.escape(id):id.replace(/["\\\\]/g,"\\\\$&");
|
||||
try{return document.querySelector("#"+esc+",[name=\\""+esc+"\\"]");}catch(e){return null;}
|
||||
}
|
||||
document.addEventListener("click",function(e){
|
||||
var el=e.target;
|
||||
while(el&&el.tagName!=="A"){el=el.parentElement;}
|
||||
if(!el)return;
|
||||
var href=el.getAttribute("href");
|
||||
if(!href||href.charAt(0)!=="#"||href.length<2)return;
|
||||
e.preventDefault();
|
||||
var id=decode(href.slice(1));
|
||||
var raw=href.slice(1);
|
||||
var target=
|
||||
document.getElementById(id)||
|
||||
document.getElementById(raw)||
|
||||
document.getElementsByName(id)[0]||
|
||||
document.getElementsByName(raw)[0]||
|
||||
bySelector(id)||
|
||||
bySelector(raw);
|
||||
if(target){target.scrollIntoView({behavior:"smooth",block:"start"});}
|
||||
},true);
|
||||
})();</script>`;
|
||||
if (/<\/body>/i.test(html)) {
|
||||
return html.replace(/<\/body>/i, `${script}</body>`);
|
||||
}
|
||||
return html + script;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user