@sparkelf/dsh-plus 0.2.1-rc.2.4 → 0.2.2-alpha.2.2

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.
Files changed (37) hide show
  1. package/README.md +7 -5
  2. package/README.zh.md +7 -5
  3. package/lib/apply-08huHfLG.js +665 -0
  4. package/lib/apply-DmICvIGh.js +653 -0
  5. package/lib/apply.js +1 -1
  6. package/lib/bin.js +188 -45
  7. package/lib/types/apply.d.ts +1 -1
  8. package/lib/types/apply.js +56 -117
  9. package/lib/types/patch-set.d.ts +59 -0
  10. package/lib/types/patch-set.js +132 -0
  11. package/lib/types/standalone-cli.js +93 -2
  12. package/lib/types/standalone-profile.d.ts +8 -1
  13. package/lib/types/standalone-profile.js +126 -49
  14. package/package.json +602 -175
  15. package/patches/approval-allow-policy.patch +108 -0
  16. package/patches/atomic-write-zombie-lock-holder.patch +53 -0
  17. package/patches/bootstrap-exempt-names.patch +32 -0
  18. package/patches/browser-auth-mode.patch +127 -0
  19. package/patches/composer-popover-boundaries.patch +337 -0
  20. package/patches/diagram-preview.patch +562 -0
  21. package/patches/image-offload-hoist-replay.patch +533 -0
  22. package/patches/legacy-code-preset.patch +148 -0
  23. package/patches/llm-pi-ai-transport-truncation.patch +33 -0
  24. package/patches/mobile-journal-generation.patch +76 -0
  25. package/patches/office-preview-stable-load.patch +34 -0
  26. package/patches/officecli-deliverables.patch +42 -0
  27. package/patches/ptc-mcp-schema-types.patch +156 -0
  28. package/patches/responses-reasoning-status.patch +289 -0
  29. package/patches/session-envelope-ignorable.patch +63 -0
  30. package/patches/session-export-chinese.patch +85 -0
  31. package/patches/session-format-legacy-restart.patch +35 -0
  32. package/patches/session-log-trajectory-toolbar.patch +352 -0
  33. package/patches/state-dot-brand-blue.patch +185 -0
  34. package/patches/ui-settings-compact-phone.patch +76 -0
  35. package/patches/web-runtime-compat.patch +83 -0
  36. package/patches/workspace-storage-restore.patch +186 -0
  37. package/patches/wsl-native-open.patch +265 -0
@@ -0,0 +1,562 @@
1
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.module.css b/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.module.css
2
+ new file mode 100644
3
+ index 00000000..977969f6
4
+ --- /dev/null
5
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.module.css
6
+ @@ -0,0 +1,53 @@
7
+ +/* Diagram canvas: scrollable surface with an absolutely positioned node layer. */
8
+ +
9
+ +.frame {
10
+ + box-sizing: border-box;
11
+ + width: 100%;
12
+ + height: 100%;
13
+ + overflow: auto;
14
+ + padding: 16px;
15
+ + font-family: var(--dsw-font, sans-serif);
16
+ +}
17
+ +
18
+ +.canvas {
19
+ + position: relative;
20
+ +}
21
+ +
22
+ +.edges {
23
+ + position: absolute;
24
+ + inset: 0;
25
+ + overflow: visible;
26
+ +}
27
+ +
28
+ +.edge {
29
+ + fill: none;
30
+ + stroke: var(--dsw-color-border, #c8ccd4);
31
+ + stroke-width: 1.5;
32
+ +}
33
+ +
34
+ +.node {
35
+ + position: absolute;
36
+ + display: flex;
37
+ + align-items: center;
38
+ + box-sizing: border-box;
39
+ + padding: 6px 10px;
40
+ + border: 1px solid var(--dsw-color-border, #c8ccd4);
41
+ + border-radius: 6px;
42
+ + background: var(--dsw-color-surface, #ffffff);
43
+ +}
44
+ +
45
+ +.label {
46
+ + display: -webkit-box;
47
+ + -webkit-box-orient: vertical;
48
+ + -webkit-line-clamp: 2;
49
+ + overflow: hidden;
50
+ + font-size: 12px;
51
+ + line-height: 1.35;
52
+ + overflow-wrap: anywhere;
53
+ +}
54
+ +
55
+ +.failure {
56
+ + padding: 16px;
57
+ + color: var(--dsw-color-text-secondary, #6b7280);
58
+ + font-size: 13px;
59
+ +}
60
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.tsx
61
+ new file mode 100644
62
+ index 00000000..e43d6b17
63
+ --- /dev/null
64
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/DiagramBody.tsx
65
+ @@ -0,0 +1,116 @@
66
+ +/** Diagram body: draw.io XML and XMind archives rendered as boxes and routed links. */
67
+ +import { useMemo, type ReactNode } from 'react'
68
+ +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
69
+ +import type { DocumentPreviewProps } from '../document/contract.ts'
70
+ +import { LoadingIndicator } from '../LoadingIndicator.tsx'
71
+ +import { parseDrawio, parseXmind, type Diagram } from './parse.ts'
72
+ +import { edgePath, layoutDiagram, NODE_HEIGHT, NODE_WIDTH } from './layout.ts'
73
+ +import type {} from './locales.ts'
74
+ +import css from './DiagramBody.module.css'
75
+ +
76
+ +/** Standard document props plus the diagram dictionary. */
77
+ +export type DiagramBodyProps = DocumentPreviewProps & PropsLocale<'documentDiagram'> & {
78
+ + /** Which format this tab carries, decided from the file suffix by the registering module. */
79
+ + readonly format: DiagramFormat
80
+ +}
81
+ +
82
+ +/** The diagram formats this body understands. */
83
+ +export type DiagramFormat = 'drawio' | 'xmind'
84
+ +
85
+ +/**
86
+ + * Decode the file bytes as UTF-8 text.
87
+ + * @param data - the document bytes.
88
+ + * @returns the decoded text.
89
+ + */
90
+ +function decode(data: Uint8Array<ArrayBuffer>): string {
91
+ + return new TextDecoder('utf-8').decode(data)
92
+ +}
93
+ +
94
+ +/**
95
+ + * Read the `content.json` member out of an XMind zip.
96
+ + *
97
+ + * An XMind file is a zip whose first entry is that JSON. Only the entry is needed, so the local
98
+ + * file header is walked directly rather than inflating the whole archive.
99
+ + * @param data - the document bytes.
100
+ + * @returns the decoded JSON text.
101
+ + */
102
+ +function readXmindContent(data: Uint8Array<ArrayBuffer>): string {
103
+ + const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
104
+ + let offset = 0
105
+ + while (offset + 30 <= data.byteLength) {
106
+ + // A local file header is 30 bytes, then the name, then the stored data.
107
+ + const signature = view.getUint32(offset, true)
108
+ + if (signature !== 0x04034b50) break
109
+ + const nameLength = view.getUint16(offset + 26, true)
110
+ + const extraLength = view.getUint16(offset + 28, true)
111
+ + const start = offset + 30 + nameLength + extraLength
112
+ + const name = decode(data.subarray(offset + 30, offset + 30 + nameLength))
113
+ + if (name === 'content.json') {
114
+ + const method = view.getUint16(offset + 8, true)
115
+ + const compressedSize = view.getUint32(offset + 18, true)
116
+ + const raw = data.subarray(start, start + compressedSize)
117
+ + if (method !== 0) throw new Error('compressed XMind entry')
118
+ + return decode(raw)
119
+ + }
120
+ + offset = start + view.getUint32(offset + 18, true)
121
+ + }
122
+ + throw new Error('XMind archive has no content.json')
123
+ +}
124
+ +
125
+ +/**
126
+ + * Parse whichever format this body was registered for.
127
+ + * @param format - the registered format.
128
+ + * @param data - the document bytes.
129
+ + * @returns the parsed diagram.
130
+ + */
131
+ +function parse(format: DiagramFormat, data: Uint8Array<ArrayBuffer>): Diagram {
132
+ + const text = decode(data)
133
+ + if (format === 'xmind') return parseXmind(readXmindContent(data))
134
+ + // A draw.io file exported with compression carries base64 in a `<diagram>` element.
135
+ + return parseDrawio(text)
136
+ +}
137
+ +
138
+ +/**
139
+ + * Render a parsed diagram.
140
+ + * @param props - standard document props plus this renderer’s dictionary and format.
141
+ + * @returns the diagram canvas, or the loading indicator while content is absent.
142
+ + */
143
+ +export function DiagramBody({ content, format, t }: DiagramBodyProps): ReactNode {
144
+ + const data = content.kind === 'bytes' ? content.data : undefined
145
+ + const parsed = useMemo(() => {
146
+ + if (data === undefined) return undefined
147
+ + try {
148
+ + return { diagram: layoutDiagram(parse(format, data)) }
149
+ + } catch {
150
+ + return { failed: true as const }
151
+ + }
152
+ + }, [data, format])
153
+ +
154
+ + if (data === undefined) return <LoadingIndicator label={t('loading')} />
155
+ + if (parsed === undefined || 'failed' in parsed) return <div className={css.failure}>{t('failed')}</div>
156
+ +
157
+ + const { diagram } = parsed
158
+ + if (diagram.placements.length === 0) return <div className={css.failure}>{t('empty')}</div>
159
+ + const at = new Map(diagram.placements.map(item => [item.node.id, item]))
160
+ +
161
+ + return (
162
+ + <div className={css.frame} data-diagram-preview>
163
+ + <div className={css.canvas} style={{ width: diagram.width, height: diagram.height }}>
164
+ + <svg className={css.edges} width={diagram.width} height={diagram.height} aria-hidden>
165
+ + {diagram.edges.map((edge, index) => {
166
+ + const from = at.get(edge.from)
167
+ + const to = at.get(edge.to)
168
+ + if (from === undefined || to === undefined) return undefined
169
+ + return <path key={index} className={css.edge} d={edgePath(from, to)} />
170
+ + })}
171
+ + </svg>
172
+ + {diagram.placements.map(({ node, x, y }) => (
173
+ + <div key={node.id} className={css.node} style={{ left: x, top: y, width: NODE_WIDTH, height: NODE_HEIGHT }}
174
+ + title={node.label}>
175
+ + <span className={css.label}>{node.label}</span>
176
+ + </div>
177
+ + ))}
178
+ + </div>
179
+ + </div>
180
+ + )
181
+ +}
182
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/diagram/index.ts
183
+ new file mode 100644
184
+ index 00000000..9daef7da
185
+ --- /dev/null
186
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/index.ts
187
+ @@ -0,0 +1,52 @@
188
+ +/** Builtin diagram metadata and keyed body registration; assembly belongs to the package entry. */
189
+ +import type { Context } from '@deepseek-ai/cordis'
190
+ +import type {} from '../index.ts'
191
+ +import type { DocumentPreviewDefinition } from '../document/registry.ts'
192
+ +import { DiagramBody, type DiagramFormat } from './DiagramBody.tsx'
193
+ +import { en, zh } from './locales.ts'
194
+ +
195
+ +/** draw.io implementation identity, shared by metadata and the keyed slot. */
196
+ +export const DRAWIO_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/drawio'
197
+ +
198
+ +/** XMind implementation identity, shared by metadata and the keyed slot. */
199
+ +export const XMIND_BODY_ID = '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/xmind'
200
+ +
201
+ +/** File suffixes rendered by the draw.io body. */
202
+ +export const DRAWIO_EXTENSIONS = ['drawio', 'dio'] as const
203
+ +
204
+ +/** File suffixes rendered by the XMind body. */
205
+ +export const XMIND_EXTENSIONS = ['xmind'] as const
206
+ +
207
+ +/**
208
+ + * Describe one diagram renderer independently from its keyed body slot.
209
+ + * @param id - implementation identity.
210
+ + * @param extensions - recognized file suffixes.
211
+ + * @param title - locale-owned implementation name.
212
+ + * @returns metadata for complete diagram files.
213
+ + */
214
+ +function diagramDefinition(id: string, extensions: readonly string[], title: () => string): DocumentPreviewDefinition {
215
+ + return { id, extensions, priority: 'builtin', title, loading: 'bytes-complete', wrap: false }
216
+ +}
217
+ +
218
+ +/**
219
+ + * Register the diagram dictionary, both format metadata entries, and their keyed bodies.
220
+ + *
221
+ + * The two formats share one body and one dictionary: the only difference is the parser the body
222
+ + * selects, which the registration supplies as a prop.
223
+ + * @param ctx - owning plugin context.
224
+ + */
225
+ +export function apply(ctx: Context): void {
226
+ + const t = ctx.locale.bind('documentDiagram')
227
+ + ctx.effect(() => ctx.locale.register('documentDiagram', { zh, en }))
228
+ + const entries: readonly { readonly id: string; readonly extensions: readonly string[]; readonly format: DiagramFormat }[] = [
229
+ + { id: DRAWIO_BODY_ID, extensions: DRAWIO_EXTENSIONS, format: 'drawio' },
230
+ + { id: XMIND_BODY_ID, extensions: XMIND_EXTENSIONS, format: 'xmind' },
231
+ + ]
232
+ + for (const entry of entries) {
233
+ + ctx.effect(() => ctx.documentPreviews.register(diagramDefinition(entry.id, entry.extensions, () => t('title'))))
234
+ + ctx.effect(() => ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register(
235
+ + { name: 'sidebar.right.tab.document', key: entry.id, locale: 'documentDiagram', inject: () => ({ format: entry.format }) },
236
+ + DiagramBody,
237
+ + )))
238
+ + }
239
+ +}
240
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/layout.ts b/packages/client/ui-sidebar-documentpreview/src/client/diagram/layout.ts
241
+ new file mode 100644
242
+ index 00000000..98cad9f3
243
+ --- /dev/null
244
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/layout.ts
245
+ @@ -0,0 +1,123 @@
246
+ +/**
247
+ + * Lay out a parsed diagram as positioned boxes and orthogonal links.
248
+ + *
249
+ + * MXGraph and XMind both describe a graph, not a picture, so the renderer needs coordinates. The
250
+ + * two arrangements a reader expects are laid out here: a mind map fans out to the right, one depth
251
+ + * per column, and a general graph keeps document order in rows. Both then route each edge as a
252
+ + * three-segment path between box borders, which stays readable without a routing library.
253
+ + */
254
+ +import type { Diagram, DiagramEdge, DiagramNode } from './parse.ts'
255
+ +
256
+ +/** Box size in CSS pixels, matching the card the renderer draws. */
257
+ +export const NODE_WIDTH = 200
258
+ +
259
+ +/** Vertical distance between the tops of neighbouring boxes. */
260
+ +export const NODE_HEIGHT = 48
261
+ +
262
+ +/** Horizontal distance between successive columns. */
263
+ +const COLUMN_GAP = 96
264
+ +
265
+ +/** Vertical distance between rows in a wrapped graph. */
266
+ +const ROW_GAP = 28
267
+ +
268
+ +/** Boxes per row once a diagram stops being a tree. */
269
+ +const ROW_WIDTH = 4
270
+ +
271
+ +/** Where one node sits. */
272
+ +export interface Placement {
273
+ + /** The node being placed. */
274
+ + readonly node: DiagramNode
275
+ + /** Left edge in diagram coordinates. */
276
+ + readonly x: number
277
+ + /** Top edge in diagram coordinates. */
278
+ + readonly y: number
279
+ +}
280
+ +
281
+ +/** A diagram with coordinates and the canvas it needs. */
282
+ +export interface Layout {
283
+ + /** Every node with its position. */
284
+ + readonly placements: readonly Placement[]
285
+ + /** Links between placed nodes. */
286
+ + readonly edges: readonly DiagramEdge[]
287
+ + /** Canvas width in diagram coordinates. */
288
+ + readonly width: number
289
+ + /** Canvas height in diagram coordinates. */
290
+ + readonly height: number
291
+ +}
292
+ +
293
+ +/**
294
+ + * Arrange nodes by their depth, one depth per column.
295
+ + * @param nodes - nodes in document order.
296
+ + * @returns each node with a column and a row inside that column.
297
+ + */
298
+ +function byDepth(nodes: readonly DiagramNode[]): readonly Placement[] {
299
+ + const rows = new Map<number, number>()
300
+ + return nodes.map((node) => {
301
+ + const row = rows.get(node.depth) ?? 0
302
+ + rows.set(node.depth, row + 1)
303
+ + return { node, x: node.depth * (NODE_WIDTH + COLUMN_GAP), y: row * (NODE_HEIGHT + ROW_GAP) }
304
+ + })
305
+ +}
306
+ +
307
+ +/**
308
+ + * Arrange nodes in document order, wrapped into rows.
309
+ + * @param nodes - nodes in document order.
310
+ + * @returns each node with a grid position.
311
+ + */
312
+ +function byRow(nodes: readonly DiagramNode[]): readonly Placement[] {
313
+ + return nodes.map((node, index) => ({
314
+ + node,
315
+ + x: (index % ROW_WIDTH) * (NODE_WIDTH + COLUMN_GAP),
316
+ + y: Math.floor(index / ROW_WIDTH) * (NODE_HEIGHT + ROW_GAP),
317
+ + }))
318
+ +}
319
+ +
320
+ +/**
321
+ + * Place a parsed diagram.
322
+ + * @param diagram - the parsed diagram.
323
+ + * @returns coordinates plus the canvas size they need.
324
+ + */
325
+ +export function layoutDiagram(diagram: Diagram): Layout {
326
+ + const rooted = diagram.edges.length === 0 || diagram.nodes.every(node => node.depth > 0 || node.depth === 0)
327
+ + // A converted mind map carries depth; a general graph does not, so it keeps document order.
328
+ + const placements = rooted && diagram.nodes.some(node => node.depth > 0) ? byDepth(diagram.nodes) : byRow(diagram.nodes)
329
+ + const width = Math.max(0, ...placements.map(item => item.x + NODE_WIDTH))
330
+ + const height = Math.max(0, ...placements.map(item => item.y + NODE_HEIGHT))
331
+ + const placed = new Set(placements.map(item => item.node.id))
332
+ + return {
333
+ + placements,
334
+ + edges: diagram.edges.filter(edge => placed.has(edge.from) && placed.has(edge.to)),
335
+ + width,
336
+ + height,
337
+ + }
338
+ +}
339
+ +
340
+ +/**
341
+ + * Route one link as a path between the facing borders of its two boxes.
342
+ + * @param from - source placement.
343
+ + * @param to - target placement.
344
+ + * @returns the SVG path data.
345
+ + */
346
+ +export function edgePath(from: Placement, to: Placement): string {
347
+ + const fromRight = from.x + NODE_WIDTH
348
+ + const fromMiddle = from.y + NODE_HEIGHT / 2
349
+ + const toMiddle = to.y + NODE_HEIGHT / 2
350
+ + // A link that runs leftward leaves from the source’s left edge instead.
351
+ + if (to.x >= fromRight) {
352
+ + const middle = (fromRight + to.x) / 2
353
+ + return 'M' + String(fromRight) + ',' + String(fromMiddle)
354
+ + + ' H' + String(middle) + ' V' + String(toMiddle) + ' H' + String(to.x)
355
+ + }
356
+ + if (to.x + NODE_WIDTH <= from.x) {
357
+ + const middle = (to.x + NODE_WIDTH + from.x) / 2
358
+ + return 'M' + String(from.x) + ',' + String(fromMiddle)
359
+ + + ' H' + String(middle) + ' V' + String(toMiddle) + ' H' + String(to.x + NODE_WIDTH)
360
+ + }
361
+ + const below = to.y >= from.y
362
+ + const start = below ? from.y + NODE_HEIGHT : from.y
363
+ + const end = below ? to.y : to.y + NODE_HEIGHT
364
+ + const middle = (start + end) / 2
365
+ + const centre = from.x + NODE_WIDTH / 2
366
+ + return 'M' + String(centre) + ',' + String(start)
367
+ + + ' V' + String(middle) + ' H' + String(to.x + NODE_WIDTH / 2) + ' V' + String(end)
368
+ +}
369
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/diagram/locales.ts
370
+ new file mode 100644
371
+ index 00000000..66914c11
372
+ --- /dev/null
373
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/locales.ts
374
+ @@ -0,0 +1,25 @@
375
+ +/** Locale-owned diagram implementation name and status text. */
376
+ +export const zh = {
377
+ + title: '图表',
378
+ + loading: '正在解析图表…',
379
+ + failed: '无法解析这份图表文件。',
380
+ + empty: '这份图表没有可渲染的节点。',
381
+ +} satisfies Record<string, string>
382
+ +
383
+ +/** Diagram renderer dictionary keys. */
384
+ +export type DiagramPreviewKey = keyof typeof zh
385
+ +
386
+ +/** English dictionary with the same keys as the Chinese dictionary. */
387
+ +export const en = {
388
+ + title: 'Diagram',
389
+ + loading: 'Parsing diagram…',
390
+ + failed: 'This diagram file could not be parsed.',
391
+ + empty: 'This diagram has no renderable nodes.',
392
+ +} satisfies Record<DiagramPreviewKey, string>
393
+ +
394
+ +declare module '@deepseek-ai/dsh-client-ui-slots' {
395
+ + interface LocaleNamespaceMap {
396
+ + /** Diagram renderer copy. */
397
+ + documentDiagram: DiagramPreviewKey
398
+ + }
399
+ +}
400
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/diagram/parse.ts b/packages/client/ui-sidebar-documentpreview/src/client/diagram/parse.ts
401
+ new file mode 100644
402
+ index 00000000..768b9a16
403
+ --- /dev/null
404
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/diagram/parse.ts
405
+ @@ -0,0 +1,139 @@
406
+ +/**
407
+ + * Read the diagram formats this preview renders into one node and edge list.
408
+ + *
409
+ + * Two shapes arrive here. A draw.io file is mxGraph XML whose cells carry an id, an optional
410
+ + * value, and a style that tells a vertex from an edge. An XMind file is a zip whose content.json
411
+ + * holds a topic tree. Both reduce to labelled boxes and the links between them, which is what the
412
+ + * renderer draws and what a reader wants from either format.
413
+ + */
414
+ +
415
+ +/** One box or label in a diagram. */
416
+ +export interface DiagramNode {
417
+ + /** Stable identity within the parsed document. */
418
+ + readonly id: string
419
+ + /** Visible text, unescaped and stripped of markup. */
420
+ + readonly label: string
421
+ + /** Nesting depth, so a parsed tree keeps its hierarchy visible. */
422
+ + readonly depth: number
423
+ +}
424
+ +
425
+ +/** One link between two nodes. */
426
+ +export interface DiagramEdge {
427
+ + /** Source node id. */
428
+ + readonly from: string
429
+ + /** Target node id. */
430
+ + readonly to: string
431
+ + /** Visible edge label, empty when the document carries none. */
432
+ + readonly label: string
433
+ +}
434
+ +
435
+ +/** A parsed diagram, ready to draw. */
436
+ +export interface Diagram {
437
+ + /** Nodes in document order. */
438
+ + readonly nodes: readonly DiagramNode[]
439
+ + /** Links whose endpoints both resolved. */
440
+ + readonly edges: readonly DiagramEdge[]
441
+ +}
442
+ +
443
+ +/**
444
+ + * Decode the entities mxGraph XML carries and drop any markup an embedded value holds.
445
+ + * @param value - raw attribute text.
446
+ + * @returns the visible label.
447
+ + */
448
+ +export function plainLabel(value: string): string {
449
+ + const decoded = value
450
+ + .replaceAll('&lt;', '<')
451
+ + .replaceAll('&gt;', '>')
452
+ + .replaceAll('&quot;', '"')
453
+ + .replaceAll('&apos;', "'")
454
+ + .replaceAll('&#10;', ' ')
455
+ + .replaceAll('&amp;', '&')
456
+ + // A value may be HTML; the text between its tags is the label a reader recognizes.
457
+ + return decoded.replace(/<[^>]*>/gu, ' ').replace(/\s+/gu, ' ').trim()
458
+ +}
459
+ +
460
+ +/**
461
+ + * Read the attribute value of one start tag.
462
+ + * @param tag - the start tag text.
463
+ + * @param name - attribute name.
464
+ + * @returns the attribute value, or an empty string when absent.
465
+ + */
466
+ +function attribute(tag: string, name: string): string {
467
+ + const match = new RegExp(name + '="([^"]*)"', 'u').exec(tag)
468
+ + return match?.[1] ?? ''
469
+ +}
470
+ +
471
+ +/**
472
+ + * Parse an mxGraph document into nodes and edges.
473
+ + * @param xml - the draw.io XML text.
474
+ + * @returns the parsed diagram.
475
+ + */
476
+ +export function parseDrawio(xml: string): Diagram {
477
+ + const nodes: DiagramNode[] = []
478
+ + const edges: DiagramEdge[] = []
479
+ + const known = new Set<string>()
480
+ + // Two passes: every vertex must exist before an edge can name both of its ends.
481
+ + for (const tag of xml.match(/<mxCell\b[^>]*>/gu) ?? []) {
482
+ + const id = attribute(tag, 'id')
483
+ + const edge = attribute(tag, 'edge') === '1'
484
+ + if (edge || id === '' || id === '0' || id === '1') continue
485
+ + const label = plainLabel(attribute(tag, 'value'))
486
+ + // A container cell with no text is structural, not a box a reader needs.
487
+ + if (label === '') continue
488
+ + known.add(id)
489
+ + nodes.push({ id, label, depth: 0 })
490
+ + }
491
+ + for (const tag of xml.match(/<mxCell\b[^>]*>/gu) ?? []) {
492
+ + if (attribute(tag, 'edge') !== '1') continue
493
+ + const from = attribute(tag, 'source')
494
+ + const to = attribute(tag, 'target')
495
+ + if (!known.has(from) || !known.has(to)) continue
496
+ + edges.push({ from, to, label: plainLabel(attribute(tag, 'value')) })
497
+ + }
498
+ + return { nodes, edges }
499
+ +}
500
+ +
501
+ +/** One topic as XMind stores it: a title and whatever is attached below. */
502
+ +interface XmindTopic {
503
+ + readonly title?: string
504
+ + readonly children?: { readonly attached?: readonly XmindTopic[] }
505
+ +}
506
+ +
507
+ +/**
508
+ + * Flatten one level of topics, returning the ids of the nodes created for that level.
509
+ + * @param topics - the topics at this level.
510
+ + * @param depth - nesting level, used for the node indent.
511
+ + * @param nodes - accumulator for nodes.
512
+ + * @returns the ids created here, so the caller links only its own children.
513
+ + */
514
+ +function walkTopics(topics: readonly XmindTopic[], depth: number, nodes: DiagramNode[], edges: DiagramEdge[]): readonly string[] {
515
+ + const created: string[] = []
516
+ + for (const topic of topics) {
517
+ + const label = (topic.title ?? '').trim()
518
+ + const id = 'n' + String(nodes.length)
519
+ + // A topic with no text still parents its children, so it keeps an id without a node.
520
+ + if (label !== '') {
521
+ + nodes.push({ id, label, depth })
522
+ + created.push(id)
523
+ + }
524
+ + const attached = topic.children?.attached ?? []
525
+ + if (attached.length === 0) continue
526
+ + const children = walkTopics(attached, depth + 1, nodes, edges)
527
+ + for (const child of children) edges.push({ from: id, to: child, label: '' })
528
+ + }
529
+ + return created
530
+ +}
531
+ +/**
532
+ + * Parse an XMind content.json document into nodes and edges.
533
+ + * @param json - the decoded content.json text.
534
+ + * @returns the parsed diagram.
535
+ + */
536
+ +export function parseXmind(json: string): Diagram {
537
+ + const sheets = JSON.parse(json) as readonly { readonly rootTopic?: XmindTopic }[]
538
+ + const nodes: DiagramNode[] = []
539
+ + const edges: DiagramEdge[] = []
540
+ + for (const sheet of sheets) {
541
+ + if (sheet.rootTopic !== undefined) walkTopics([sheet.rootTopic], 0, nodes, edges)
542
+ + }
543
+ + return { nodes, edges }
544
+ +}
545
+ diff --git a/packages/client/ui-sidebar-documentpreview/src/client/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/index.ts
546
+ index 72a26771..94c11c51 100644
547
+ --- a/packages/client/ui-sidebar-documentpreview/src/client/index.ts
548
+ +++ b/packages/client/ui-sidebar-documentpreview/src/client/index.ts
549
+ @@ -36,6 +36,7 @@ import { apply as registerHtml } from './html/index.ts'
550
+ import { apply as registerImage } from './image/index.ts'
551
+ import { apply as registerPdf } from './pdf/index.ts'
552
+ import { apply as registerCode } from './code/index.ts'
553
+ +import { apply as registerDiagram } from './diagram/index.ts'
554
+ import { apply as registerOffice } from './office/index.ts'
555
+ import { apply as registerExcel } from './excel/index.ts'
556
+ import { Config } from '../config.ts'
557
+ @@ -127,4 +128,5 @@ export function apply(ctx: ClientContext): void {
558
+ registerOffice(ctx, config.office)
559
+ registerExcel(ctx, config.excel)
560
+ registerCode(ctx)
561
+ + registerDiagram(ctx)
562
+ }