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.
@@ -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
+ }