@schlessera/brain-ui-react 0.33.1 → 0.34.1
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/README.md +5 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +8 -149
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/activity-rollups.d.ts +5 -0
- package/dist/components/activity/activity-rollups.d.ts.map +1 -0
- package/dist/components/activity/activity-rollups.js +39 -0
- package/dist/components/activity/activity-rollups.js.map +1 -0
- package/dist/components/activity/activity-run-detail.d.ts +11 -0
- package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
- package/dist/components/activity/activity-run-detail.js +111 -0
- package/dist/components/activity/activity-run-detail.js.map +1 -0
- package/dist/components/activity/activity-run-list.d.ts +15 -0
- package/dist/components/activity/activity-run-list.d.ts.map +1 -0
- package/dist/components/activity/activity-run-list.js +25 -0
- package/dist/components/activity/activity-run-list.js.map +1 -0
- package/dist/components/chat/brain-markdown-code.d.ts +4 -0
- package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-code.js +47 -0
- package/dist/components/chat/brain-markdown-code.js.map +1 -0
- package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
- package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-entities.js +93 -0
- package/dist/components/chat/brain-markdown-entities.js.map +1 -0
- package/dist/components/chat/brain-markdown-links.d.ts +36 -0
- package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-links.js +176 -0
- package/dist/components/chat/brain-markdown-links.js.map +1 -0
- package/dist/components/chat/brain-markdown-share.d.ts +15 -0
- package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-share.js +40 -0
- package/dist/components/chat/brain-markdown-share.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +1 -34
- package/dist/components/chat/brain-markdown.d.ts.map +1 -1
- package/dist/components/chat/brain-markdown.js +9 -402
- package/dist/components/chat/brain-markdown.js.map +1 -1
- package/dist/components/chat/composer.d.ts.map +1 -1
- package/dist/components/chat/composer.js +14 -1
- package/dist/components/chat/composer.js.map +1 -1
- package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
- package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
- package/dist/components/chat/use-markdown-highlight.js +56 -0
- package/dist/components/chat/use-markdown-highlight.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
- package/dist/components/connectivity/connection-gate.js +41 -7
- package/dist/components/connectivity/connection-gate.js.map +1 -1
- package/dist/components/connectivity/connection-state.d.ts +12 -0
- package/dist/components/connectivity/connection-state.d.ts.map +1 -0
- package/dist/components/connectivity/connection-state.js +23 -0
- package/dist/components/connectivity/connection-state.js.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.js +4 -0
- package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
- package/dist/components/graph/graph-controls.js +4 -1
- package/dist/components/graph/graph-controls.js.map +1 -1
- package/dist/components/graph/graph-maintenance.d.ts +8 -0
- package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
- package/dist/components/graph/graph-maintenance.js +115 -0
- package/dist/components/graph/graph-maintenance.js.map +1 -0
- package/dist/components/graph/graph-page.d.ts.map +1 -1
- package/dist/components/graph/graph-page.js +11 -306
- package/dist/components/graph/graph-page.js.map +1 -1
- package/dist/components/graph/graph-scene.d.ts +8 -0
- package/dist/components/graph/graph-scene.d.ts.map +1 -0
- package/dist/components/graph/graph-scene.js +208 -0
- package/dist/components/graph/graph-scene.js.map +1 -0
- package/dist/components/graph/graph-spinner.d.ts +2 -0
- package/dist/components/graph/graph-spinner.d.ts.map +1 -0
- package/dist/components/graph/graph-spinner.js +6 -0
- package/dist/components/graph/graph-spinner.js.map +1 -0
- package/dist/hooks/use-vpn-status.d.ts +5 -0
- package/dist/hooks/use-vpn-status.d.ts.map +1 -1
- package/dist/hooks/use-vpn-status.js +61 -29
- package/dist/hooks/use-vpn-status.js.map +1 -1
- package/dist/hooks/use-websocket.d.ts +3 -2
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +61 -252
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
- package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/activity.js +10 -0
- package/dist/hooks/websocket-handlers/activity.js.map +1 -0
- package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
- package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/chat.js +175 -0
- package/dist/hooks/websocket-handlers/chat.js.map +1 -0
- package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
- package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/connection.js +27 -0
- package/dist/hooks/websocket-handlers/connection.js.map +1 -0
- package/dist/hooks/websocket-handlers/index.d.ts +4 -0
- package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/index.js +19 -0
- package/dist/hooks/websocket-handlers/index.js.map +1 -0
- package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
- package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/mask.js +14 -0
- package/dist/hooks/websocket-handlers/mask.js.map +1 -0
- package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
- package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/provider.js +31 -0
- package/dist/hooks/websocket-handlers/provider.js.map +1 -0
- package/dist/hooks/websocket-handlers/types.d.ts +24 -0
- package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/types.js +2 -0
- package/dist/hooks/websocket-handlers/types.js.map +1 -0
- package/dist/stores/activity-store-helpers.d.ts +50 -0
- package/dist/stores/activity-store-helpers.d.ts.map +1 -0
- package/dist/stores/activity-store-helpers.js +143 -0
- package/dist/stores/activity-store-helpers.js.map +1 -0
- package/dist/stores/activity-store-types.d.ts +48 -0
- package/dist/stores/activity-store-types.d.ts.map +1 -0
- package/dist/stores/activity-store-types.js +2 -0
- package/dist/stores/activity-store-types.js.map +1 -0
- package/dist/stores/activity-store.d.ts +2 -78
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +22 -142
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/stores/connection-store.d.ts +14 -2
- package/dist/stores/connection-store.d.ts.map +1 -1
- package/dist/stores/connection-store.js +17 -0
- package/dist/stores/connection-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +6 -477
- package/src/components/activity/activity-rollups.tsx +94 -0
- package/src/components/activity/activity-run-detail.tsx +301 -0
- package/src/components/activity/activity-run-list.tsx +97 -0
- package/src/components/chat/brain-markdown-code.tsx +60 -0
- package/src/components/chat/brain-markdown-entities.tsx +116 -0
- package/src/components/chat/brain-markdown-links.tsx +247 -0
- package/src/components/chat/brain-markdown-share.ts +47 -0
- package/src/components/chat/brain-markdown.tsx +27 -535
- package/src/components/chat/composer.tsx +14 -1
- package/src/components/chat/use-markdown-highlight.ts +63 -0
- package/src/components/connectivity/connection-gate.tsx +64 -13
- package/src/components/connectivity/connection-state.ts +37 -0
- package/src/components/graph/graph-canvas-lazy.tsx +4 -0
- package/src/components/graph/graph-controls.tsx +4 -1
- package/src/components/graph/graph-maintenance.tsx +304 -0
- package/src/components/graph/graph-page.tsx +11 -840
- package/src/components/graph/graph-scene.tsx +548 -0
- package/src/components/graph/graph-spinner.tsx +9 -0
- package/src/hooks/use-vpn-status.ts +64 -31
- package/src/hooks/use-websocket.ts +70 -273
- package/src/hooks/websocket-handlers/activity.ts +13 -0
- package/src/hooks/websocket-handlers/chat.ts +210 -0
- package/src/hooks/websocket-handlers/connection.ts +30 -0
- package/src/hooks/websocket-handlers/index.ts +59 -0
- package/src/hooks/websocket-handlers/mask.ts +17 -0
- package/src/hooks/websocket-handlers/provider.ts +35 -0
- package/src/hooks/websocket-handlers/types.ts +27 -0
- package/src/stores/activity-store-helpers.ts +164 -0
- package/src/stores/activity-store-types.ts +55 -0
- package/src/stores/activity-store.ts +21 -217
- package/src/stores/connection-store.ts +33 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { apiBase } from "../../lib/backend.js";
|
|
4
|
+
import {
|
|
5
|
+
isInternalRepoDir,
|
|
6
|
+
isInternalRepoPath,
|
|
7
|
+
useFileStore,
|
|
8
|
+
} from "../../stores/file-store.js";
|
|
9
|
+
import { useUIStore } from "../../stores/ui-store.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Turn a markdown image source into something the browser can actually load.
|
|
13
|
+
*
|
|
14
|
+
* Anything already absolute — a `data:` URI, an http(s) URL, or a path that is
|
|
15
|
+
* already an API route — passes through. A repo-relative path is rewritten to
|
|
16
|
+
* the files endpoint, which is where brain content is served from.
|
|
17
|
+
*/
|
|
18
|
+
export function repoImageSrc(src: string): string {
|
|
19
|
+
const trimmed = src.trim();
|
|
20
|
+
if (!trimmed) return src;
|
|
21
|
+
if (/^(data:|blob:|https?:\/\/)/i.test(trimmed)) return trimmed;
|
|
22
|
+
if (trimmed.startsWith("/api/")) return trimmed;
|
|
23
|
+
const rel = trimmed.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
24
|
+
return `${apiBase()}/files/content?path=${encodeURIComponent(rel)}&raw=1`;
|
|
25
|
+
}
|
|
26
|
+
/** Click target for a repo-relative file reference. Opens the file panel + viewer. */
|
|
27
|
+
export function FileLink({ path, children }: { path: string; children?: React.ReactNode }) {
|
|
28
|
+
const openFile = useFileStore((s) => s.openFile);
|
|
29
|
+
const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
|
|
30
|
+
return (
|
|
31
|
+
<a
|
|
32
|
+
href={`#/files/${path}`}
|
|
33
|
+
onClick={(e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setFilePanelOpen(true);
|
|
36
|
+
void openFile(path);
|
|
37
|
+
}}
|
|
38
|
+
className="brain-file-link"
|
|
39
|
+
title={path}
|
|
40
|
+
>
|
|
41
|
+
{children ?? path}
|
|
42
|
+
</a>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve an obsidian-style `[[target]]` to a repo-relative path using the
|
|
48
|
+
* wikilink map. Targets containing `/` are treated as paths directly; bare
|
|
49
|
+
* slugs are looked up by basename. An `#anchor` suffix is stripped before
|
|
50
|
+
* lookup.
|
|
51
|
+
*/
|
|
52
|
+
function resolveWikilinkTarget(
|
|
53
|
+
raw: string,
|
|
54
|
+
slugMap: Record<string, string>
|
|
55
|
+
): string | null {
|
|
56
|
+
const hashIdx = raw.indexOf("#");
|
|
57
|
+
const noAnchor = hashIdx >= 0 ? raw.slice(0, hashIdx).trim() : raw.trim();
|
|
58
|
+
if (!noAnchor) return null;
|
|
59
|
+
if (noAnchor.includes("/")) {
|
|
60
|
+
if (noAnchor.endsWith("/")) return null;
|
|
61
|
+
return /\.[a-z0-9]{1,8}$/i.test(noAnchor) ? noAnchor : `${noAnchor}.md`;
|
|
62
|
+
}
|
|
63
|
+
return slugMap[noAnchor.toLowerCase()] ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Click target for `[[slug]]` / `[[slug|Label]]`. */
|
|
67
|
+
export function WikiLink({
|
|
68
|
+
target,
|
|
69
|
+
label,
|
|
70
|
+
}: {
|
|
71
|
+
target: string;
|
|
72
|
+
label?: string;
|
|
73
|
+
}) {
|
|
74
|
+
const slugMap = useFileStore((s) => s.wikilinkMap);
|
|
75
|
+
const loaded = useFileStore((s) => s.wikilinkLoaded);
|
|
76
|
+
const openFile = useFileStore((s) => s.openFile);
|
|
77
|
+
const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
|
|
78
|
+
|
|
79
|
+
const resolved = resolveWikilinkTarget(target, slugMap);
|
|
80
|
+
const display = label ?? target;
|
|
81
|
+
|
|
82
|
+
if (!resolved) {
|
|
83
|
+
return (
|
|
84
|
+
<span
|
|
85
|
+
className="brain-wiki-link brain-wiki-link--unresolved"
|
|
86
|
+
title={loaded ? `Unresolved wikilink: ${target}` : "Resolving…"}
|
|
87
|
+
>
|
|
88
|
+
{display}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<a
|
|
95
|
+
href={`#/files/${resolved}`}
|
|
96
|
+
onClick={(e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
setFilePanelOpen(true);
|
|
99
|
+
void openFile(resolved);
|
|
100
|
+
}}
|
|
101
|
+
className="brain-file-link brain-wiki-link"
|
|
102
|
+
title={resolved}
|
|
103
|
+
>
|
|
104
|
+
{display}
|
|
105
|
+
</a>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Click target for a repo-relative directory reference. Opens the file
|
|
111
|
+
* panel, expands the tree down to the directory, and highlights it so
|
|
112
|
+
* the user sees its contents.
|
|
113
|
+
*/
|
|
114
|
+
export function DirLink({ path, children }: { path: string; children?: React.ReactNode }) {
|
|
115
|
+
const openDir = useFileStore((s) => s.openDir);
|
|
116
|
+
const setFilePanelOpen = useUIStore((s) => s.setFilePanelOpen);
|
|
117
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
118
|
+
const display = `${trimmed}/`;
|
|
119
|
+
return (
|
|
120
|
+
<a
|
|
121
|
+
href={`#/files/${display}`}
|
|
122
|
+
onClick={(e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
setFilePanelOpen(true);
|
|
125
|
+
void openDir(trimmed);
|
|
126
|
+
}}
|
|
127
|
+
className="brain-file-link brain-file-link--dir"
|
|
128
|
+
title={display}
|
|
129
|
+
>
|
|
130
|
+
{children ?? display}
|
|
131
|
+
</a>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Bare-path tokens in prose. Three passes:
|
|
136
|
+
// - files: "notes/projects/foo.md" — ends with .ext
|
|
137
|
+
// - dirs: "notes/projects/post-templates/" — ends with /
|
|
138
|
+
// - wikilinks: "[[slug]]" or "[[slug|Display]]" — obsidian-style
|
|
139
|
+
const BARE_FILE_RE =
|
|
140
|
+
/\b([a-z][a-z0-9_-]*(?:\/[a-z0-9._-]+)+\.[a-z0-9]{1,8})\b/gi;
|
|
141
|
+
const BARE_DIR_RE =
|
|
142
|
+
/\b([a-z][a-z0-9_-]*(?:\/[a-z0-9._-]+)+\/)(?=$|[\s,;:!?)\]])/gi;
|
|
143
|
+
const WIKILINK_RE = /\[\[([^\[\]\n|]+?)(?:\|([^\[\]\n]+?))?\]\]/g;
|
|
144
|
+
|
|
145
|
+
interface PathMatch {
|
|
146
|
+
start: number;
|
|
147
|
+
end: number;
|
|
148
|
+
kind: "file" | "dir" | "wikilink";
|
|
149
|
+
/** For file/dir kinds. */
|
|
150
|
+
path?: string;
|
|
151
|
+
/** Raw target inside the brackets (may include `#anchor`). For wikilink. */
|
|
152
|
+
target?: string;
|
|
153
|
+
/** Display label override (after the pipe). For wikilink. */
|
|
154
|
+
label?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scanBarePaths(text: string): PathMatch[] {
|
|
158
|
+
const out: PathMatch[] = [];
|
|
159
|
+
BARE_FILE_RE.lastIndex = 0;
|
|
160
|
+
let m: RegExpExecArray | null;
|
|
161
|
+
while ((m = BARE_FILE_RE.exec(text)) !== null) {
|
|
162
|
+
if (isInternalRepoPath(m[1])) {
|
|
163
|
+
out.push({
|
|
164
|
+
start: m.index,
|
|
165
|
+
end: BARE_FILE_RE.lastIndex,
|
|
166
|
+
path: m[1],
|
|
167
|
+
kind: "file",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
BARE_DIR_RE.lastIndex = 0;
|
|
172
|
+
while ((m = BARE_DIR_RE.exec(text)) !== null) {
|
|
173
|
+
if (isInternalRepoDir(m[1])) {
|
|
174
|
+
out.push({
|
|
175
|
+
start: m.index,
|
|
176
|
+
end: BARE_DIR_RE.lastIndex,
|
|
177
|
+
path: m[1],
|
|
178
|
+
kind: "dir",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
WIKILINK_RE.lastIndex = 0;
|
|
183
|
+
while ((m = WIKILINK_RE.exec(text)) !== null) {
|
|
184
|
+
out.push({
|
|
185
|
+
start: m.index,
|
|
186
|
+
end: WIKILINK_RE.lastIndex,
|
|
187
|
+
kind: "wikilink",
|
|
188
|
+
target: m[1].trim(),
|
|
189
|
+
label: m[2]?.trim(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
out.sort((a, b) => a.start - b.start);
|
|
193
|
+
|
|
194
|
+
// Drop overlapping matches — keep the first (earlier) when ranges collide
|
|
195
|
+
const merged: PathMatch[] = [];
|
|
196
|
+
let cursor = 0;
|
|
197
|
+
for (const cand of out) {
|
|
198
|
+
if (cand.start >= cursor) {
|
|
199
|
+
merged.push(cand);
|
|
200
|
+
cursor = cand.end;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return merged;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function renderBarePathsInText(text: string): React.ReactNode[] {
|
|
207
|
+
const matches = scanBarePaths(text);
|
|
208
|
+
if (matches.length === 0) return [text];
|
|
209
|
+
const nodes: React.ReactNode[] = [];
|
|
210
|
+
let last = 0;
|
|
211
|
+
let key = 0;
|
|
212
|
+
for (const m of matches) {
|
|
213
|
+
if (m.start > last) nodes.push(text.slice(last, m.start));
|
|
214
|
+
if (m.kind === "file" && m.path) {
|
|
215
|
+
nodes.push(
|
|
216
|
+
<FileLink key={`p${key++}`} path={m.path}>
|
|
217
|
+
{m.path}
|
|
218
|
+
</FileLink>
|
|
219
|
+
);
|
|
220
|
+
} else if (m.kind === "dir" && m.path) {
|
|
221
|
+
nodes.push(
|
|
222
|
+
<DirLink key={`p${key++}`} path={m.path.replace(/\/+$/, "")}>
|
|
223
|
+
{m.path}
|
|
224
|
+
</DirLink>
|
|
225
|
+
);
|
|
226
|
+
} else if (m.kind === "wikilink" && m.target) {
|
|
227
|
+
nodes.push(
|
|
228
|
+
<WikiLink key={`p${key++}`} target={m.target} label={m.label} />
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
last = m.end;
|
|
232
|
+
}
|
|
233
|
+
if (last < text.length) nodes.push(text.slice(last));
|
|
234
|
+
return nodes;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Public helper: turn any raw text blob into React nodes with bare-path
|
|
239
|
+
* tokens replaced by FileLink elements. Use this in non-markdown surfaces
|
|
240
|
+
* (tool outputs, streaming logs, plain user text).
|
|
241
|
+
*/
|
|
242
|
+
export function linkifyPaths(text: string): React.ReactNode {
|
|
243
|
+
if (!text) return text;
|
|
244
|
+
const parts = renderBarePathsInText(text);
|
|
245
|
+
if (parts.length === 1 && typeof parts[0] === "string") return parts[0];
|
|
246
|
+
return <>{parts}</>;
|
|
247
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ShareBlockFormat } from "./share-block.js";
|
|
2
|
+
|
|
3
|
+
type ShareSegment = { kind: "share"; format: ShareBlockFormat; title?: string; body: string };
|
|
4
|
+
type TextSegment = { kind: "text"; text: string };
|
|
5
|
+
export type Segment = TextSegment | ShareSegment;
|
|
6
|
+
|
|
7
|
+
const SHARE_BLOCK_RE = /<share\b([^>]*)>([\s\S]*?)<\/share>/gi;
|
|
8
|
+
const VALID_FORMATS: ReadonlySet<ShareBlockFormat> = new Set([
|
|
9
|
+
"image",
|
|
10
|
+
"pdf",
|
|
11
|
+
"text",
|
|
12
|
+
"markdown",
|
|
13
|
+
"richtext",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function parseShareAttrs(raw: string): { format: ShareBlockFormat; title?: string } {
|
|
17
|
+
const attrs: Record<string, string> = {};
|
|
18
|
+
const re = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
|
19
|
+
let m: RegExpExecArray | null;
|
|
20
|
+
while ((m = re.exec(raw)) !== null) {
|
|
21
|
+
attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
22
|
+
}
|
|
23
|
+
const format = (attrs.format ?? "image").toLowerCase() as ShareBlockFormat;
|
|
24
|
+
return {
|
|
25
|
+
format: VALID_FORMATS.has(format) ? format : "image",
|
|
26
|
+
title: attrs.title,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function splitShareBlocks(input: string): Segment[] {
|
|
31
|
+
const out: Segment[] = [];
|
|
32
|
+
let last = 0;
|
|
33
|
+
let m: RegExpExecArray | null;
|
|
34
|
+
SHARE_BLOCK_RE.lastIndex = 0;
|
|
35
|
+
while ((m = SHARE_BLOCK_RE.exec(input)) !== null) {
|
|
36
|
+
if (m.index > last) {
|
|
37
|
+
out.push({ kind: "text", text: input.slice(last, m.index) });
|
|
38
|
+
}
|
|
39
|
+
const { format, title } = parseShareAttrs(m[1] ?? "");
|
|
40
|
+
out.push({ kind: "share", format, title, body: m[2].trim() });
|
|
41
|
+
last = SHARE_BLOCK_RE.lastIndex;
|
|
42
|
+
}
|
|
43
|
+
if (last < input.length) {
|
|
44
|
+
out.push({ kind: "text", text: input.slice(last) });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|