anentrypoint-design 0.0.368 → 0.0.370
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/chat.css +128 -0
- package/dist/247420.css +454 -1
- package/dist/247420.js +27 -27
- package/package.json +2 -8
- package/src/components/chat-message-parts.js +42 -4
- package/src/components/chat-minimap.js +309 -0
- package/src/components/chat.js +2 -1
- package/src/components/editor-primitives.js +41 -14
- package/src/components/files-modals.js +44 -4
- package/src/components/git-status.js +22 -6
- package/src/components/interaction-primitives.js +27 -0
- package/src/components/models-config.js +229 -0
- package/src/components/plugins-config.js +154 -0
- package/src/components/sessions.js +138 -38
- package/src/components/skills-config.js +188 -0
- package/src/components/voice.js +41 -0
- package/src/components.js +12 -2
- package/src/css/app-shell/chat-polish.css +25 -0
- package/src/css/app-shell/files.css +18 -0
- package/src/css/app-shell/models-config.css +93 -0
- package/src/css/app-shell/plugins-config.css +139 -0
- package/src/css/app-shell/skills-config.css +27 -0
- package/src/file-mention.js +142 -0
- package/src/index.js +6 -0
- package/src/math.js +158 -0
- package/src/mermaid.js +105 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Pure helpers for a chat input's "@" file-mention autocomplete: detecting an
|
|
2
|
+
// in-progress @token before the caret, ranking a flat file/dir index against
|
|
3
|
+
// the typed query, and building the replacement text on completion.
|
|
4
|
+
// Framework-independent — no DOM, no webjsx — wire into any composer's
|
|
5
|
+
// oninput/onkeydown handlers.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect an "@" file token immediately before the cursor. The "@" must sit at
|
|
9
|
+
* the start of the text or be preceded by whitespace, so emails like
|
|
10
|
+
* foo@bar never trigger. Supports the quoted form @"my dir/fi so
|
|
11
|
+
* space-containing paths can be typed into.
|
|
12
|
+
* @param {string} textBeforeCursor
|
|
13
|
+
* @returns {{start:number, query:string, quoted:boolean}|null}
|
|
14
|
+
*/
|
|
15
|
+
export function extractAtQuery(textBeforeCursor) {
|
|
16
|
+
const quoted = /(?:^|\s)@"([^"\n]*)$/.exec(textBeforeCursor)
|
|
17
|
+
if (quoted) {
|
|
18
|
+
return { start: textBeforeCursor.length - (quoted[1].length + 2), query: quoted[1], quoted: true }
|
|
19
|
+
}
|
|
20
|
+
const plain = /(?:^|\s)@([^\s"]*)$/.exec(textBeforeCursor)
|
|
21
|
+
if (plain) {
|
|
22
|
+
return { start: textBeforeCursor.length - (plain[1].length + 1), query: plain[1], quoted: false }
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pathDepth(p) {
|
|
28
|
+
let depth = 0
|
|
29
|
+
for (let i = 0; i < p.length; i++) if (p[i] === '/') depth++
|
|
30
|
+
return depth
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a { path, isDir } entry list from a flat file-path list, deriving
|
|
35
|
+
* directory entries by walking each path's "/" segments. Base order is
|
|
36
|
+
* shallow-first then alphabetical (what an empty "@" query should show).
|
|
37
|
+
* @param {string[]} files
|
|
38
|
+
* @returns {{path:string, isDir:boolean}[]}
|
|
39
|
+
*/
|
|
40
|
+
export function buildEntriesFromFiles(files) {
|
|
41
|
+
const dirs = new Set()
|
|
42
|
+
for (const f of files) {
|
|
43
|
+
let idx = f.indexOf('/')
|
|
44
|
+
while (idx !== -1) {
|
|
45
|
+
dirs.add(f.slice(0, idx))
|
|
46
|
+
idx = f.indexOf('/', idx + 1)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const entries = []
|
|
50
|
+
for (const d of dirs) entries.push({ path: d, isDir: true })
|
|
51
|
+
for (const f of files) {
|
|
52
|
+
if (!f) continue
|
|
53
|
+
entries.push({ path: f, isDir: false })
|
|
54
|
+
}
|
|
55
|
+
entries.sort((a, b) => pathDepth(a.path) - pathDepth(b.path) || a.path.localeCompare(b.path))
|
|
56
|
+
return entries
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isSubsequence(needle, haystack) {
|
|
60
|
+
if (!needle) return true
|
|
61
|
+
let i = 0
|
|
62
|
+
for (let j = 0; j < haystack.length && i < needle.length; j++) {
|
|
63
|
+
if (haystack[j] === needle[i]) i++
|
|
64
|
+
}
|
|
65
|
+
return i === needle.length
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Score ladder: exact 100 / prefix 80 / substring 50 / path-substring 30,
|
|
70
|
+
* directories get +10, plus a low-weight subsequence fallback (10) so a
|
|
71
|
+
* loose query like "chinp" still finds components/ChatInput.tsx. Queries
|
|
72
|
+
* containing "/" rank against the full relative path (drill-down support).
|
|
73
|
+
*/
|
|
74
|
+
function scoreEntry(entry, lowerQuery) {
|
|
75
|
+
const lowerPath = entry.path.toLowerCase()
|
|
76
|
+
let score = 0
|
|
77
|
+
if (lowerQuery.includes('/')) {
|
|
78
|
+
if (lowerPath === lowerQuery) score = 100
|
|
79
|
+
else if (lowerPath.startsWith(lowerQuery)) score = 80
|
|
80
|
+
else if (lowerPath.includes(lowerQuery)) score = 50
|
|
81
|
+
else if (isSubsequence(lowerQuery, lowerPath)) score = 10
|
|
82
|
+
} else {
|
|
83
|
+
const slash = lowerPath.lastIndexOf('/')
|
|
84
|
+
const lowerName = slash === -1 ? lowerPath : lowerPath.slice(slash + 1)
|
|
85
|
+
if (lowerName === lowerQuery) score = 100
|
|
86
|
+
else if (lowerName.startsWith(lowerQuery)) score = 80
|
|
87
|
+
else if (lowerName.includes(lowerQuery)) score = 50
|
|
88
|
+
else if (lowerPath.includes(lowerQuery)) score = 30
|
|
89
|
+
else if (isSubsequence(lowerQuery, lowerPath)) score = 10
|
|
90
|
+
}
|
|
91
|
+
if (entry.isDir && score > 0) score += 10
|
|
92
|
+
return score
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const AT_RESULT_LIMIT = 20
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Rank+filter a file index against a typed query, capped at `limit`.
|
|
99
|
+
* @param {{path:string, isDir:boolean}[]} entries
|
|
100
|
+
* @param {string} query
|
|
101
|
+
* @param {number} [limit]
|
|
102
|
+
*/
|
|
103
|
+
export function filterFileEntries(entries, query, limit = AT_RESULT_LIMIT) {
|
|
104
|
+
const lowerQuery = query.toLowerCase()
|
|
105
|
+
if (!lowerQuery) return entries.slice(0, limit)
|
|
106
|
+
const scored = []
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const score = scoreEntry(entry, lowerQuery)
|
|
109
|
+
if (score > 0) scored.push({ entry, score })
|
|
110
|
+
}
|
|
111
|
+
scored.sort((a, b) => b.score - a.score || pathDepth(a.entry.path) - pathDepth(b.entry.path) || a.entry.path.localeCompare(b.entry.path))
|
|
112
|
+
return scored.slice(0, limit).map(s => s.entry)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Replacement text for the @token when a suggestion is confirmed. Files
|
|
117
|
+
* close the token ("@path ", quoted if it has spaces), caret after the
|
|
118
|
+
* trailing space. Directories stay open for drill-down ("@dir/"), no
|
|
119
|
+
* trailing space — quoted directories close instead (@"my dir/") with the
|
|
120
|
+
* caret placed before the closing quote.
|
|
121
|
+
* @returns {{text:string, cursorOffset:number}}
|
|
122
|
+
*/
|
|
123
|
+
export function buildAtInsertText(entryPath, isDir, forceQuotes = false) {
|
|
124
|
+
const p = isDir ? `${entryPath}/` : entryPath
|
|
125
|
+
const needsQuotes = forceQuotes || p.includes(' ')
|
|
126
|
+
if (isDir) {
|
|
127
|
+
const text = needsQuotes ? `@"${p}"` : `@${p}`
|
|
128
|
+
return { text, cursorOffset: needsQuotes ? text.length - 1 : text.length }
|
|
129
|
+
}
|
|
130
|
+
const text = needsQuotes ? `@"${p}" ` : `@${p} `
|
|
131
|
+
return { text, cursorOffset: text.length }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Closed one-shot @mention (e.g. an explorer's "@" button) — directories close too. */
|
|
135
|
+
export function buildAtMentionText(entryPath, isDir) {
|
|
136
|
+
const p = isDir ? `${entryPath}/` : entryPath
|
|
137
|
+
return p.includes(' ') ? `@"${p}" ` : `@${p} `
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildFileAtMentionsText(entryPaths) {
|
|
141
|
+
return entryPaths.map(entryPath => buildAtMentionText(entryPath, false)).join('')
|
|
142
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { renderMarkdown, ensureReady as ensureMarkdownReady, sanitizeHtml, isDeg
|
|
|
13
13
|
import { escapeHtml, escapeJson } from './html-escape.js';
|
|
14
14
|
import { uid, shortUid } from './uid.js';
|
|
15
15
|
import { ensurePrism, highlightAllUnder, configurePrismCdn, getPrismCdnConfig } from './highlight.js';
|
|
16
|
+
import { ensureMermaid, renderMermaid, renderMermaidBlocksUnder, configureMermaidCdn, getMermaidCdnConfig } from './mermaid.js';
|
|
17
|
+
import { ensureKatex, renderMathBlocksUnder, configureKatexCdn, getKatexCdnConfig } from './math.js';
|
|
16
18
|
import { renderPageHtml } from './page-html.js';
|
|
17
19
|
import { HeroFromPageData } from './components/content.js';
|
|
18
20
|
import { ThemeToggle } from './components/theme-toggle.js';
|
|
@@ -85,6 +87,8 @@ export {
|
|
|
85
87
|
renderMarkdown, ensureMarkdownReady, sanitizeHtml, isMarkdownDegraded,
|
|
86
88
|
configureMarkdownCdn, getMarkdownCdnConfig,
|
|
87
89
|
ensurePrism, highlightAllUnder, configurePrismCdn, getPrismCdnConfig,
|
|
90
|
+
ensureMermaid, renderMermaid, renderMermaidBlocksUnder, configureMermaidCdn, getMermaidCdnConfig,
|
|
91
|
+
ensureKatex, renderMathBlocksUnder, configureKatexCdn, getKatexCdnConfig,
|
|
88
92
|
registerChatElement, DsChat,
|
|
89
93
|
registerFreddieChatElement, FreddieChat,
|
|
90
94
|
renderPageHtml, HeroFromPageData,
|
|
@@ -99,6 +103,8 @@ export {
|
|
|
99
103
|
};
|
|
100
104
|
export { applyTheme, getTheme, resolvedTheme, onThemeChange, initTheme,
|
|
101
105
|
applyAccent, getAccent, applyDensity, getDensity } from './theme.js';
|
|
106
|
+
export { extractAtQuery, buildEntriesFromFiles, filterFileEntries, buildAtInsertText,
|
|
107
|
+
buildAtMentionText, buildFileAtMentionsText } from './file-mention.js';
|
|
102
108
|
export const h = webjsx.createElement;
|
|
103
109
|
export const applyDiff = webjsx.applyDiff;
|
|
104
110
|
|
package/src/math.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Inline/display math (KaTeX) rendering — lazy-loads KaTeX CSS+JS from CDN on
|
|
2
|
+
// first call. No-op safe: absent/failed load leaves $...$ / $$...$$ as plain
|
|
3
|
+
// literal text (already escaped by the markdown sanitizer), never broken markup.
|
|
4
|
+
// Mirrors highlight.js/mermaid.js's lazy-CDN-module pattern.
|
|
5
|
+
|
|
6
|
+
let _katex = null;
|
|
7
|
+
let _ready = null;
|
|
8
|
+
let _cssInjected = false;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_KATEX_JS_URL = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.mjs';
|
|
11
|
+
const DEFAULT_KATEX_CSS_URL = 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css';
|
|
12
|
+
let _jsUrl = DEFAULT_KATEX_JS_URL;
|
|
13
|
+
let _cssUrl = DEFAULT_KATEX_CSS_URL;
|
|
14
|
+
|
|
15
|
+
export function configureKatexCdn({ jsUrl, cssUrl } = {}) {
|
|
16
|
+
_jsUrl = jsUrl || DEFAULT_KATEX_JS_URL;
|
|
17
|
+
_cssUrl = cssUrl || DEFAULT_KATEX_CSS_URL;
|
|
18
|
+
_katex = null;
|
|
19
|
+
_ready = null;
|
|
20
|
+
_cssInjected = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getKatexCdnConfig() {
|
|
24
|
+
return { jsUrl: _jsUrl, cssUrl: _cssUrl };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function injectCss() {
|
|
28
|
+
if (_cssInjected || typeof document === 'undefined') return;
|
|
29
|
+
if (document.querySelector('link[data-katex]')) { _cssInjected = true; return; }
|
|
30
|
+
const link = document.createElement('link');
|
|
31
|
+
link.rel = 'stylesheet';
|
|
32
|
+
link.href = _cssUrl;
|
|
33
|
+
link.setAttribute('data-katex', '');
|
|
34
|
+
document.head.appendChild(link);
|
|
35
|
+
_cssInjected = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function ensureKatex() {
|
|
39
|
+
if (_katex) return _katex;
|
|
40
|
+
if (_ready) return _ready;
|
|
41
|
+
_ready = (async () => {
|
|
42
|
+
try {
|
|
43
|
+
injectCss();
|
|
44
|
+
const mod = await import(_jsUrl);
|
|
45
|
+
const katex = mod.default || mod;
|
|
46
|
+
if (!katex || typeof katex.renderToString !== 'function') throw new Error('katex module missing renderToString()');
|
|
47
|
+
_katex = katex;
|
|
48
|
+
return _katex;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn('[247420] katex loader failed:', err);
|
|
51
|
+
_katex = null;
|
|
52
|
+
_ready = null;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
return _ready;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Render one math source to sanitized-by-construction KaTeX HTML (katex
|
|
60
|
+
// escapes its own output; throwOnError:false degrades a malformed expression
|
|
61
|
+
// to KaTeX's own inline error span rather than throwing).
|
|
62
|
+
async function renderOne(src, displayMode) {
|
|
63
|
+
const katex = await ensureKatex();
|
|
64
|
+
if (!katex) return null;
|
|
65
|
+
try {
|
|
66
|
+
return katex.renderToString(src, { throwOnError: false, strict: false, displayMode });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.warn('[247420] katex render failed:', err);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Replace $$...$$ (display) and $...$ (inline) math spans inside already-
|
|
74
|
+
// rendered markdown HTML text nodes under `root`. Runs AFTER markdown/
|
|
75
|
+
// DOMPurify have produced the DOM (never operates on raw markdown source, so
|
|
76
|
+
// it cannot introduce unsanitized HTML — katex's own output is inserted via
|
|
77
|
+
// innerHTML on a fresh span, same trust boundary as DOMPurify's own output).
|
|
78
|
+
// Skips code/pre content so math delimiters inside fenced or inline code are
|
|
79
|
+
// left alone. Idempotent (data-math-wired guard on root).
|
|
80
|
+
export async function renderMathBlocksUnder(root) {
|
|
81
|
+
if (!root || root.dataset?.mathWired === '1') return;
|
|
82
|
+
if (!root || !root.querySelectorAll) return;
|
|
83
|
+
const text = root.innerHTML || '';
|
|
84
|
+
if (!text.includes('$')) return;
|
|
85
|
+
const katex = await ensureKatex();
|
|
86
|
+
if (!katex) return;
|
|
87
|
+
root.dataset.mathWired = '1';
|
|
88
|
+
|
|
89
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
90
|
+
acceptNode(node) {
|
|
91
|
+
const p = node.parentElement;
|
|
92
|
+
if (p && (p.closest('code') || p.closest('pre'))) return NodeFilter.FILTER_REJECT;
|
|
93
|
+
return /\$/.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const nodes = [];
|
|
97
|
+
let n;
|
|
98
|
+
while ((n = walker.nextNode())) nodes.push(n);
|
|
99
|
+
|
|
100
|
+
const DISPLAY_RE = /\$\$([^$]+)\$\$/g;
|
|
101
|
+
const INLINE_RE = /\$([^$\n]+)\$/g;
|
|
102
|
+
|
|
103
|
+
for (const node of nodes) {
|
|
104
|
+
const src = node.nodeValue;
|
|
105
|
+
if (!DISPLAY_RE.test(src) && !INLINE_RE.test(src)) continue;
|
|
106
|
+
DISPLAY_RE.lastIndex = 0;
|
|
107
|
+
INLINE_RE.lastIndex = 0;
|
|
108
|
+
const frag = document.createDocumentFragment();
|
|
109
|
+
let cursor = 0;
|
|
110
|
+
// Two-pass: first split out display ($$..$$), each remaining plain
|
|
111
|
+
// segment is then split for inline ($..$).
|
|
112
|
+
const pieces = [];
|
|
113
|
+
let m;
|
|
114
|
+
while ((m = DISPLAY_RE.exec(src)) !== null) {
|
|
115
|
+
if (m.index > cursor) pieces.push({ text: src.slice(cursor, m.index) });
|
|
116
|
+
pieces.push({ math: m[1], display: true });
|
|
117
|
+
cursor = m.index + m[0].length;
|
|
118
|
+
}
|
|
119
|
+
if (cursor < src.length) pieces.push({ text: src.slice(cursor) });
|
|
120
|
+
|
|
121
|
+
for (const piece of pieces) {
|
|
122
|
+
if (piece.math != null) {
|
|
123
|
+
const html = await renderOne(piece.math.trim(), true);
|
|
124
|
+
if (html) {
|
|
125
|
+
const span = document.createElement('span');
|
|
126
|
+
span.className = 'ds-math-display';
|
|
127
|
+
span.innerHTML = html;
|
|
128
|
+
frag.appendChild(span);
|
|
129
|
+
} else {
|
|
130
|
+
frag.appendChild(document.createTextNode('$$' + piece.math + '$$'));
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
let sub = piece.text;
|
|
135
|
+
let last = 0;
|
|
136
|
+
INLINE_RE.lastIndex = 0;
|
|
137
|
+
let im;
|
|
138
|
+
let any = false;
|
|
139
|
+
while ((im = INLINE_RE.exec(sub)) !== null) {
|
|
140
|
+
any = true;
|
|
141
|
+
if (im.index > last) frag.appendChild(document.createTextNode(sub.slice(last, im.index)));
|
|
142
|
+
const html = await renderOne(im[1].trim(), false);
|
|
143
|
+
if (html) {
|
|
144
|
+
const span = document.createElement('span');
|
|
145
|
+
span.className = 'ds-math-inline';
|
|
146
|
+
span.innerHTML = html;
|
|
147
|
+
frag.appendChild(span);
|
|
148
|
+
} else {
|
|
149
|
+
frag.appendChild(document.createTextNode('$' + im[1] + '$'));
|
|
150
|
+
}
|
|
151
|
+
last = im.index + im[0].length;
|
|
152
|
+
}
|
|
153
|
+
if (!any) frag.appendChild(document.createTextNode(sub));
|
|
154
|
+
else if (last < sub.length) frag.appendChild(document.createTextNode(sub.slice(last)));
|
|
155
|
+
}
|
|
156
|
+
node.parentNode.replaceChild(frag, node);
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/mermaid.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Mermaid diagram rendering — lazy-loads mermaid.js from CDN on first call.
|
|
2
|
+
// No-op safe: absent/failed load leaves the fenced ```mermaid block as plain
|
|
3
|
+
// (already Prism-highlighted) code, never a blank or broken area.
|
|
4
|
+
// Mirrors highlight.js's lazy-CDN-module pattern (module cache + configurable
|
|
5
|
+
// base URL + fail-soft).
|
|
6
|
+
|
|
7
|
+
let _mermaid = null;
|
|
8
|
+
let _ready = null;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MERMAID_URL = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
11
|
+
let _mermaidUrl = DEFAULT_MERMAID_URL;
|
|
12
|
+
|
|
13
|
+
// Optional override (self-host / mirror / CSP-allowlisted proxy), same
|
|
14
|
+
// contract as configurePrismCdn/configureMarkdownCdn: additive, forces a
|
|
15
|
+
// fresh load on next use.
|
|
16
|
+
export function configureMermaidCdn({ url } = {}) {
|
|
17
|
+
_mermaidUrl = url || DEFAULT_MERMAID_URL;
|
|
18
|
+
_mermaid = null;
|
|
19
|
+
_ready = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getMermaidCdnConfig() {
|
|
23
|
+
return { url: _mermaidUrl };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function ensureMermaid() {
|
|
27
|
+
if (_mermaid) return _mermaid;
|
|
28
|
+
if (_ready) return _ready;
|
|
29
|
+
_ready = (async () => {
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import(_mermaidUrl);
|
|
32
|
+
const mermaid = mod.default || mod;
|
|
33
|
+
if (!mermaid || typeof mermaid.render !== 'function') throw new Error('mermaid module missing render()');
|
|
34
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true });
|
|
35
|
+
_mermaid = mermaid;
|
|
36
|
+
return _mermaid;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn('[247420] mermaid loader failed:', err);
|
|
39
|
+
_mermaid = null;
|
|
40
|
+
_ready = null;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return _ready;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let _seq = 0;
|
|
48
|
+
// Render one mermaid source string to sanitized-by-construction SVG markup
|
|
49
|
+
// (mermaid's own securityLevel:'strict' escapes text nodes; no user HTML is
|
|
50
|
+
// interpolated). Returns null on any failure so the caller can keep showing
|
|
51
|
+
// the source block instead of an empty/broken pane.
|
|
52
|
+
export async function renderMermaid(code) {
|
|
53
|
+
const mermaid = await ensureMermaid();
|
|
54
|
+
if (!mermaid) return null;
|
|
55
|
+
try {
|
|
56
|
+
const id = 'ds-mermaid-' + Date.now().toString(36) + '-' + (_seq++);
|
|
57
|
+
const ok = mermaid.parse ? await mermaid.parse(code, { suppressErrors: true }) : true;
|
|
58
|
+
if (!ok) return null;
|
|
59
|
+
const { svg } = await mermaid.render(id, code);
|
|
60
|
+
return svg;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.warn('[247420] mermaid render failed:', err);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find every ```mermaid fenced block already rendered as
|
|
68
|
+
// <pre><code class="language-mermaid">...</code></pre> (or lang-mermaid, the
|
|
69
|
+
// alternate class chat-message-parts.js's CodeNode uses) inside `root`, and
|
|
70
|
+
// replace its content with a rendered SVG + a source/preview toggle button.
|
|
71
|
+
// Idempotent (data-mermaid-wired guard). Fail-soft per block: an individual
|
|
72
|
+
// diagram that fails to parse/render is left as its original code block.
|
|
73
|
+
export async function renderMermaidBlocksUnder(root) {
|
|
74
|
+
if (!root || !root.querySelectorAll) return;
|
|
75
|
+
const blocks = root.querySelectorAll('code.language-mermaid, code.lang-mermaid');
|
|
76
|
+
for (const codeEl of blocks) {
|
|
77
|
+
const pre = codeEl.closest('pre');
|
|
78
|
+
if (!pre || pre.dataset.mermaidWired === '1') continue;
|
|
79
|
+
pre.dataset.mermaidWired = '1';
|
|
80
|
+
const code = codeEl.textContent || '';
|
|
81
|
+
const svg = await renderMermaid(code);
|
|
82
|
+
if (!svg) { delete pre.dataset.mermaidWired; continue; }
|
|
83
|
+
const wrap = document.createElement('div');
|
|
84
|
+
wrap.className = 'ds-mermaid-block';
|
|
85
|
+
const toggle = document.createElement('button');
|
|
86
|
+
toggle.type = 'button';
|
|
87
|
+
toggle.className = 'ds-mermaid-toggle';
|
|
88
|
+
toggle.textContent = 'source';
|
|
89
|
+
const diagram = document.createElement('div');
|
|
90
|
+
diagram.className = 'ds-mermaid-diagram';
|
|
91
|
+
diagram.innerHTML = svg;
|
|
92
|
+
let showingSource = false;
|
|
93
|
+
toggle.addEventListener('click', () => {
|
|
94
|
+
showingSource = !showingSource;
|
|
95
|
+
diagram.style.display = showingSource ? 'none' : '';
|
|
96
|
+
pre.style.display = showingSource ? '' : 'none';
|
|
97
|
+
toggle.textContent = showingSource ? 'diagram' : 'source';
|
|
98
|
+
});
|
|
99
|
+
wrap.appendChild(diagram);
|
|
100
|
+
wrap.appendChild(toggle);
|
|
101
|
+
pre.style.display = 'none';
|
|
102
|
+
pre.parentNode.insertBefore(wrap, pre);
|
|
103
|
+
wrap.appendChild(pre);
|
|
104
|
+
}
|
|
105
|
+
}
|