orbitchat 3.3.8 → 3.5.0

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 (90) hide show
  1. package/dist/assets/ChartRenderer-C5FkaI18.js +80 -0
  2. package/dist/assets/MermaidRenderer-CghpQL29.js +259 -0
  3. package/dist/assets/{MusicRenderer-DZhuX52M.js → MusicRenderer-Bsg1uCOO.js} +2 -2
  4. package/dist/assets/{SVGRenderer-CB5j7ekx.js → SVGRenderer-BkbsVjFt.js} +1 -1
  5. package/dist/assets/_basePickBy-BmphHeNv.js +1 -0
  6. package/dist/assets/{_baseUniq-BFwLbgVF.js → _baseUniq-DcPuVRoH.js} +1 -1
  7. package/dist/assets/{architectureDiagram-VXUJARFQ-Dhht8baZ.js → architectureDiagram-2XIMDMQ5-ejVJFzXd.js} +3 -3
  8. package/dist/assets/blockDiagram-WCTKOSBZ-In1L1uKu.js +132 -0
  9. package/dist/assets/c4Diagram-IC4MRINW-DUOWrv1E.js +10 -0
  10. package/dist/assets/channel-Cgb_NwCj.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-FoAnG2DD.js → chunk-4BX2VUAB-CEKQJtxy.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Cj902k47.js → chunk-55IACEB6-CZtwN-ba.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-D8TmopkR.js → chunk-FMBD7UC4-DC7v8pYp.js} +1 -1
  14. package/dist/assets/chunk-JSJVCQXG-BImJsuxH.js +1 -0
  15. package/dist/assets/{chunk-QN33PNHL-DDhJT2OP.js → chunk-KX2RTZJC-Bn6iA2gB.js} +1 -1
  16. package/dist/assets/{chunk-DI55MBZ5-YO8RmHLs.js → chunk-NQ4KR5QH-CuypZAVn.js} +4 -4
  17. package/dist/assets/{chunk-QZHKN3VN-CCSqXR9J.js → chunk-QZHKN3VN-BWpcmxTA.js} +1 -1
  18. package/dist/assets/chunk-WL4C6EOR-DYuNq0mZ.js +189 -0
  19. package/dist/assets/classDiagram-VBA2DB6C-fyPj1cvz.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-fyPj1cvz.js +1 -0
  21. package/dist/assets/clone-Cw0uzdEo.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-Dp7UKE-6.js → cose-bilkent-S5V4N54A-CrYky9tE.js} +1 -1
  23. package/dist/assets/dagre-KLK3FWXG-Bu-DLCK7.js +4 -0
  24. package/dist/assets/diagram-E7M64L7V-aAXWrxTM.js +24 -0
  25. package/dist/assets/diagram-IFDJBPK2-Dw4zd9rB.js +43 -0
  26. package/dist/assets/{diagram-S2PKOQOG-COTWuuiM.js → diagram-P4PSJMXO-45WsYDhT.js} +1 -1
  27. package/dist/assets/erDiagram-INFDFZHY-DOTnUTaB.js +70 -0
  28. package/dist/assets/flowDiagram-PKNHOUZH-DG_tQtcp.js +162 -0
  29. package/dist/assets/{ganttDiagram-JELNMOA3-nAhYUBJC.js → ganttDiagram-A5KZAMGK-BzsGIuEd.js} +30 -5
  30. package/dist/assets/gitGraphDiagram-K3NZZRJ6-Cj6jckyn.js +65 -0
  31. package/dist/assets/{graph-CVfZ5ZRD.js → graph-DTIKcgiu.js} +1 -1
  32. package/dist/assets/index-C3vuLth5.js +620 -0
  33. package/dist/assets/{index-DuEkeKcS.js → index-C4cdR4BU.js} +1 -1
  34. package/dist/assets/index-DMCMxyfd.css +1 -0
  35. package/dist/assets/infoDiagram-LFFYTUFH-CCtedtHp.js +2 -0
  36. package/dist/assets/ishikawaDiagram-PHBUUO56-CLGCPNak.js +70 -0
  37. package/dist/assets/{journeyDiagram-XKPGCS4Q-DzPJjseD.js → journeyDiagram-4ABVD52K-BXpAIS18.js} +3 -3
  38. package/dist/assets/{kanban-definition-3W4ZIXB7-BMPHlnkL.js → kanban-definition-K7BYSVSG-Dz6JMJ6P.js} +5 -5
  39. package/dist/assets/{layout-Cdwf2_35.js → layout-1zKkDoXK.js} +1 -1
  40. package/dist/assets/{mindmap-definition-VGOIOE7T-76g77fcj.js → mindmap-definition-YRQLILUH-Bk0RuY2l.js} +7 -7
  41. package/dist/assets/{pieDiagram-ADFJNKIX-_2bZQVhp.js → pieDiagram-SKSYHLDU-BRxRa9wV.js} +2 -2
  42. package/dist/assets/purify.es-DIZLy5JB.js +2 -0
  43. package/dist/assets/{quadrantDiagram-AYHSOK5B-DHidw6CG.js → quadrantDiagram-337W2JSQ-0xXACG3E.js} +1 -1
  44. package/dist/assets/{requirementDiagram-UZGBJVZJ-BosyfqW6.js → requirementDiagram-Z7DCOOCP-Cbizpsr2.js} +14 -5
  45. package/dist/assets/{sankeyDiagram-TZEHDZUN-kXQhuPRq.js → sankeyDiagram-WA2Y5GQK-C-2SmZmM.js} +1 -1
  46. package/dist/assets/sequenceDiagram-2WXFIKYE-BIt_HBTk.js +145 -0
  47. package/dist/assets/{stateDiagram-FKZM4ZOC-DUwTvhKc.js → stateDiagram-RAJIS63D-q_Cz5sxe.js} +1 -1
  48. package/dist/assets/stateDiagram-v2-FVOUBMTO-DNYb0Gsx.js +1 -0
  49. package/dist/assets/{timeline-definition-IT6M3QCI-tUdTwV48.js → timeline-definition-YZTLITO2-BwLu_3Bg.js} +1 -1
  50. package/dist/assets/treemap-KZPCXAKY-DUv4YaOt.js +162 -0
  51. package/dist/assets/vennDiagram-LZ73GAT5-CfA8AUEM.js +34 -0
  52. package/dist/assets/{xychartDiagram-PRI3JC2R-2ndTjhZS.js → xychartDiagram-JWTSCODW-ByOeXycF.js} +2 -2
  53. package/dist/index.html +2 -2
  54. package/package.json +1 -9
  55. package/dist/assets/ChartRenderer-BtX7_jv5.js +0 -80
  56. package/dist/assets/MermaidRenderer-DLpT9XPj.js +0 -260
  57. package/dist/assets/_basePickBy-KeSLCJM0.js +0 -1
  58. package/dist/assets/blockDiagram-VD42YOAC-C0uY9SKW.js +0 -122
  59. package/dist/assets/c4Diagram-YG6GDRKO-AGMRXqhN.js +0 -10
  60. package/dist/assets/channel-D-yx-ubr.js +0 -1
  61. package/dist/assets/chunk-B4BG7PRW-DZisX-Yn.js +0 -165
  62. package/dist/assets/chunk-TZMSLE5B-DtWrsAau.js +0 -1
  63. package/dist/assets/classDiagram-2ON5EDUG-BOVDq9sH.js +0 -1
  64. package/dist/assets/classDiagram-v2-WZHVMYZB-BOVDq9sH.js +0 -1
  65. package/dist/assets/clone-DOmxAX3a.js +0 -1
  66. package/dist/assets/dagre-6UL2VRFP-NEctntTO.js +0 -4
  67. package/dist/assets/diagram-PSM6KHXK-Bdb-Z7Rq.js +0 -24
  68. package/dist/assets/diagram-QEK2KX5R-Cxi1cnQw.js +0 -43
  69. package/dist/assets/erDiagram-Q2GNP2WA-CgLq4-Sy.js +0 -60
  70. package/dist/assets/flowDiagram-NV44I4VS-CVdT6vYV.js +0 -162
  71. package/dist/assets/gitGraphDiagram-V2S2FVAM-cZq9sWZG.js +0 -65
  72. package/dist/assets/index-Baf0NBsK.css +0 -1
  73. package/dist/assets/index-CmDt8-sd.js +0 -621
  74. package/dist/assets/infoDiagram-HS3SLOUP-DFQtcUA2.js +0 -2
  75. package/dist/assets/purify.es-A66Cw1IH.js +0 -2
  76. package/dist/assets/sequenceDiagram-WL72ISMW-CQCG4z68.js +0 -145
  77. package/dist/assets/stateDiagram-v2-4FDKWEC3-Ccelj_jo.js +0 -1
  78. package/dist/assets/treemap-GDKQZRPO-AkKmBZRv.js +0 -160
  79. package/markdown-renderer/LICENSE +0 -201
  80. package/markdown-renderer/src/CodeBlock.tsx +0 -332
  81. package/markdown-renderer/src/MarkdownComponents.tsx +0 -233
  82. package/markdown-renderer/src/MarkdownStyles.css +0 -732
  83. package/markdown-renderer/src/css.d.ts +0 -4
  84. package/markdown-renderer/src/index.ts +0 -32
  85. package/markdown-renderer/src/preprocessing.ts +0 -519
  86. package/markdown-renderer/src/renderers/ChartRenderer.tsx +0 -1464
  87. package/markdown-renderer/src/renderers/MermaidRenderer.tsx +0 -474
  88. package/markdown-renderer/src/renderers/MusicRenderer.tsx +0 -394
  89. package/markdown-renderer/src/renderers/SVGRenderer.tsx +0 -307
  90. package/markdown-renderer/src/types.ts +0 -174
@@ -1,394 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import type { MusicRendererProps } from '../types';
3
-
4
- type AbcJsLike = {
5
- renderAbc: (target: HTMLElement | string, code: string, options?: Record<string, unknown>) => unknown;
6
- };
7
-
8
- type WindowWithAbcjs = {
9
- ABCJS?: AbcJsLike;
10
- };
11
-
12
- // Dynamic import for abcjs to handle both ESM and CommonJS
13
- let abcjs: AbcJsLike | null = null;
14
- const loadAbcjs = async () => {
15
- if (typeof window === 'undefined') {
16
- throw new Error('abcjs requires a browser environment');
17
- }
18
-
19
- if (abcjs) return abcjs;
20
-
21
- try {
22
- // Import abcjs (CommonJS module, will be default export in ESM)
23
- const abcjsModule = await import('abcjs');
24
-
25
- // CommonJS modules are typically the default export when imported as ESM
26
- const abcjsLib = abcjsModule.default || abcjsModule;
27
-
28
- if (!abcjsLib) {
29
- throw new Error('abcjs module is empty');
30
- }
31
-
32
- if (typeof abcjsLib.renderAbc !== 'function') {
33
- throw new Error(`renderAbc is not a function. Available methods: ${Object.keys(abcjsLib).join(', ')}`);
34
- }
35
-
36
- abcjs = abcjsLib;
37
- return abcjs;
38
- } catch (err) {
39
- // Fallback: try to load from window if available
40
- const windowWithAbcjs = window as WindowWithAbcjs;
41
- if (typeof window !== 'undefined' && windowWithAbcjs.ABCJS) {
42
- abcjs = windowWithAbcjs.ABCJS;
43
- return abcjs;
44
- }
45
- const errorMessage = err instanceof Error ? err.message : 'Failed to load abcjs';
46
- throw new Error(`Failed to load abcjs: ${errorMessage}`);
47
- }
48
- };
49
-
50
- /**
51
- * Detects if the code is ABC notation
52
- */
53
- const isAbcNotation = (code: string): boolean => {
54
- const trimmed = code.trim();
55
- // ABC notation typically starts with headers like X:, T:, M:, L:, K:
56
- return /^[XMTLK]:/m.test(trimmed) || /^X:\d+/m.test(trimmed);
57
- };
58
-
59
- /**
60
- * Check if ABC notation appears incomplete (streaming)
61
- */
62
- const isLikelyIncomplete = (code: string): boolean => {
63
- const trimmed = code.trim();
64
-
65
- // Must have at least X: header to start
66
- if (!trimmed.includes('X:')) {
67
- return true;
68
- }
69
-
70
- // Check if we have the minimum required headers
71
- // ABC notation needs at least X: and K: (key signature) to render
72
- const hasKey = /^K:/m.test(trimmed);
73
- if (!hasKey) {
74
- return true;
75
- }
76
-
77
- // Check if the last line looks incomplete (ends mid-header or mid-note)
78
- const lines = trimmed.split('\n');
79
- const lastLine = lines[lines.length - 1].trim();
80
-
81
- // Incomplete header (has colon but nothing after, or just a letter)
82
- if (lastLine.match(/^[A-Z]:?\s*$/) && lastLine.length < 3) {
83
- return true;
84
- }
85
-
86
- // Line ends with a bar that suggests more content coming
87
- if (lastLine.endsWith('|') && !lastLine.endsWith('|]') && !lastLine.endsWith('||')) {
88
- // Could be incomplete, but also could be valid - check if very short
89
- const noteContent = lastLine.replace(/\|/g, '').trim();
90
- if (noteContent.length < 2) {
91
- return true;
92
- }
93
- }
94
-
95
- return false;
96
- };
97
-
98
- export const MusicRenderer: React.FC<MusicRendererProps> = ({ code }) => {
99
- const containerRef = useRef<HTMLDivElement>(null);
100
- const [error, setError] = useState<string | null>(null);
101
- const [isLoading, setIsLoading] = useState(true);
102
- const [isStreaming, setIsStreaming] = useState(false);
103
- const [isAbc, setIsAbc] = useState(false);
104
- const [showErrorDetails, setShowErrorDetails] = useState(false);
105
- const lastCodeRef = useRef<string>('');
106
- const lastUpdateTimeRef = useRef<number>(0);
107
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
108
-
109
- // First effect: Detect ABC notation and streaming state
110
- useEffect(() => {
111
- const trimmed = code.trim();
112
- if (!trimmed) {
113
- setIsLoading(false);
114
- setIsAbc(false);
115
- setIsStreaming(false);
116
- return;
117
- }
118
-
119
- const now = Date.now();
120
- const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
121
- const codeChanged = code !== lastCodeRef.current;
122
-
123
- lastCodeRef.current = code;
124
- lastUpdateTimeRef.current = now;
125
-
126
- // Detect streaming
127
- const incomplete = isLikelyIncomplete(trimmed);
128
- const rapidUpdate = codeChanged && timeSinceLastUpdate < 500 && timeSinceLastUpdate > 0;
129
- const likelyStreaming = incomplete || rapidUpdate;
130
-
131
- if (incomplete) {
132
- setIsStreaming(true);
133
- setIsLoading(true);
134
- setError(null);
135
- return;
136
- }
137
-
138
- if (isAbcNotation(code)) {
139
- setIsAbc(true);
140
- setError(null);
141
- if (likelyStreaming) {
142
- setIsStreaming(true);
143
- }
144
- } else {
145
- setIsAbc(false);
146
- if (likelyStreaming) {
147
- setIsStreaming(true);
148
- setError(null);
149
- } else {
150
- setError('Unable to detect ABC notation. Expected ABC notation starting with headers like X:, T:, M:, L:, or K:');
151
- setIsLoading(false);
152
- setIsStreaming(false);
153
- }
154
- }
155
- }, [code]);
156
-
157
- // Second effect: Render ABC notation after container is mounted
158
- useEffect(() => {
159
- if (!isAbc || !code.trim()) {
160
- return;
161
- }
162
-
163
- // If streaming, debounce the render
164
- if (isStreaming) {
165
- if (debounceTimerRef.current) {
166
- clearTimeout(debounceTimerRef.current);
167
- }
168
-
169
- debounceTimerRef.current = setTimeout(() => {
170
- setIsStreaming(false);
171
- }, 400);
172
- }
173
-
174
- const renderAbc = async () => {
175
- try {
176
- setIsLoading(true);
177
-
178
- // Wait for container to be available (with retries)
179
- let retries = 0;
180
- const maxRetries = 10;
181
- while (!containerRef.current && retries < maxRetries) {
182
- await new Promise(resolve => setTimeout(resolve, 50));
183
- retries++;
184
- }
185
-
186
- if (!containerRef.current) {
187
- throw new Error('Container element not found after waiting');
188
- }
189
-
190
- const abcjsLib = await loadAbcjs();
191
-
192
- // Clear previous content
193
- containerRef.current.innerHTML = '';
194
-
195
- // Render ABC notation
196
- abcjsLib.renderAbc(containerRef.current, code, {
197
- responsive: 'resize',
198
- staffwidth: 740,
199
- paddingleft: 0,
200
- paddingright: 0,
201
- paddingtop: 15,
202
- paddingbottom: 15,
203
- scale: 1.0,
204
- });
205
-
206
- setError(null);
207
- setIsLoading(false);
208
- } catch (err) {
209
- const errorMessage = err instanceof Error ? err.message : 'Failed to render ABC notation';
210
- setError(errorMessage);
211
- setIsLoading(false);
212
- }
213
- };
214
-
215
- renderAbc();
216
-
217
- return () => {
218
- if (debounceTimerRef.current) {
219
- clearTimeout(debounceTimerRef.current);
220
- }
221
- };
222
- }, [code, isAbc, isStreaming]);
223
-
224
- if (error) {
225
- return (
226
- <div className="graph-error">
227
- <div className="graph-error-header">
228
- <div className="graph-error-icon">⚠️</div>
229
- <div className="graph-error-content">
230
- <div className="graph-error-title">ABC Notation Rendering Error</div>
231
- <div className="graph-error-message">{error}</div>
232
- </div>
233
- </div>
234
- <button
235
- className="graph-error-toggle"
236
- onClick={() => setShowErrorDetails(!showErrorDetails)}
237
- type="button"
238
- >
239
- {showErrorDetails ? 'Hide' : 'Show'} Details
240
- </button>
241
- {showErrorDetails && (
242
- <details className="graph-error-details" open>
243
- <summary style={{ cursor: 'pointer', marginBottom: '8px', fontWeight: 500 }}>
244
- ABC Notation Code
245
- </summary>
246
- <pre style={{
247
- marginTop: '8px',
248
- fontSize: '0.8em',
249
- opacity: 0.8,
250
- padding: '8px',
251
- background: 'rgba(0, 0, 0, 0.05)',
252
- borderRadius: '4px',
253
- overflow: 'auto',
254
- maxHeight: '200px'
255
- }}>
256
- <code>{code}</code>
257
- </pre>
258
- </details>
259
- )}
260
- </div>
261
- );
262
- }
263
-
264
- // Show loading/streaming state
265
- if ((isLoading || isStreaming) && !isAbc) {
266
- return (
267
- <div className="graph-container music-container abc-container">
268
- <div style={{
269
- display: 'flex',
270
- flexDirection: 'column',
271
- alignItems: 'center',
272
- justifyContent: 'center',
273
- padding: '30px 20px',
274
- color: 'var(--md-text-secondary, #6b7280)',
275
- minHeight: '120px',
276
- }}>
277
- <svg
278
- style={{
279
- animation: 'spin 1s linear infinite',
280
- marginBottom: '10px',
281
- width: '28px',
282
- height: '28px',
283
- }}
284
- viewBox="0 0 24 24"
285
- fill="none"
286
- >
287
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
288
- </svg>
289
- <span style={{ fontWeight: 500, fontSize: '14px' }}>
290
- {isStreaming ? 'Receiving music notation...' : 'Loading music notation...'}
291
- </span>
292
- <style>{`
293
- @keyframes spin {
294
- from { transform: rotate(0deg); }
295
- to { transform: rotate(360deg); }
296
- }
297
- `}</style>
298
- </div>
299
- </div>
300
- );
301
- }
302
-
303
- // Render ABC notation - always render container so ref is available
304
- if (isAbc) {
305
- return (
306
- <div
307
- className="graph-container music-container abc-container"
308
- style={{
309
- padding: '16px',
310
- position: 'relative',
311
- }}
312
- >
313
- {isStreaming && (
314
- <div
315
- style={{
316
- position: 'absolute',
317
- top: '8px',
318
- right: '8px',
319
- display: 'flex',
320
- alignItems: 'center',
321
- padding: '4px 8px',
322
- backgroundColor: 'rgba(59, 130, 246, 0.1)',
323
- borderRadius: '4px',
324
- fontSize: '12px',
325
- color: '#3b82f6',
326
- zIndex: 10,
327
- }}
328
- >
329
- <svg
330
- style={{
331
- animation: 'spin 1s linear infinite',
332
- marginRight: '4px',
333
- width: '12px',
334
- height: '12px'
335
- }}
336
- viewBox="0 0 24 24"
337
- fill="none"
338
- >
339
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
340
- </svg>
341
- Updating...
342
- <style>{`
343
- @keyframes spin {
344
- from { transform: rotate(0deg); }
345
- to { transform: rotate(360deg); }
346
- }
347
- `}</style>
348
- </div>
349
- )}
350
- {isLoading && !isStreaming && (
351
- <div style={{
352
- display: 'flex',
353
- flexDirection: 'column',
354
- alignItems: 'center',
355
- justifyContent: 'center',
356
- padding: '20px',
357
- color: 'var(--md-text-secondary, #6b7280)',
358
- minHeight: '80px',
359
- }}>
360
- <svg
361
- style={{
362
- animation: 'spin 1s linear infinite',
363
- marginBottom: '8px',
364
- width: '24px',
365
- height: '24px',
366
- }}
367
- viewBox="0 0 24 24"
368
- fill="none"
369
- >
370
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
371
- </svg>
372
- <span style={{ fontSize: '13px' }}>Rendering notation...</span>
373
- <style>{`
374
- @keyframes spin {
375
- from { transform: rotate(0deg); }
376
- to { transform: rotate(360deg); }
377
- }
378
- `}</style>
379
- </div>
380
- )}
381
- <div
382
- ref={containerRef}
383
- style={{
384
- display: 'flex',
385
- justifyContent: 'center',
386
- overflow: 'auto',
387
- }}
388
- />
389
- </div>
390
- );
391
- }
392
-
393
- return null;
394
- };
@@ -1,307 +0,0 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import DOMPurify from 'dompurify';
3
- import type { SVGRendererProps } from '../types';
4
-
5
- // Comprehensive list of allowed SVG tags
6
- const ALLOWED_SVG_TAGS = [
7
- // Structure
8
- 'svg', 'g', 'defs', 'symbol', 'use', 'switch', 'desc', 'title', 'metadata',
9
- // Shapes
10
- 'path', 'circle', 'ellipse', 'rect', 'line', 'polyline', 'polygon',
11
- // Text
12
- 'text', 'tspan', 'textPath',
13
- // Gradients & Patterns
14
- 'linearGradient', 'radialGradient', 'stop', 'pattern',
15
- // Filters
16
- 'filter', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite',
17
- 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight',
18
- 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur',
19
- 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight',
20
- 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',
21
- // Clipping & Masking
22
- 'clipPath', 'mask',
23
- // Markers
24
- 'marker',
25
- // Animation (safe subset)
26
- 'animate', 'animateTransform', 'animateMotion', 'set', 'mpath',
27
- // Images
28
- 'image', 'foreignObject',
29
- ];
30
-
31
- // Comprehensive list of allowed SVG attributes
32
- const ALLOWED_SVG_ATTRS = [
33
- // Core attributes
34
- 'id', 'class', 'style', 'lang', 'tabindex',
35
- // Positioning & sizing
36
- 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
37
- 'width', 'height', 'viewBox', 'preserveAspectRatio',
38
- // Paths
39
- 'd', 'pathLength',
40
- // Transforms
41
- 'transform', 'transform-origin',
42
- // Presentation attributes
43
- 'fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity',
44
- 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset',
45
- 'stroke-miterlimit', 'opacity', 'visibility', 'display',
46
- // Colors
47
- 'color', 'color-interpolation', 'color-interpolation-filters',
48
- // Text
49
- 'font-family', 'font-size', 'font-style', 'font-weight', 'font-variant',
50
- 'text-anchor', 'dominant-baseline', 'alignment-baseline', 'baseline-shift',
51
- 'letter-spacing', 'word-spacing', 'text-decoration', 'writing-mode',
52
- 'dx', 'dy', 'rotate', 'textLength', 'lengthAdjust',
53
- // Gradients & patterns
54
- 'gradientUnits', 'gradientTransform', 'spreadMethod', 'offset', 'stop-color', 'stop-opacity',
55
- 'patternUnits', 'patternContentUnits', 'patternTransform',
56
- // Filters
57
- 'filterUnits', 'primitiveUnits', 'in', 'in2', 'result', 'stdDeviation',
58
- 'type', 'values', 'mode', 'operator', 'k1', 'k2', 'k3', 'k4',
59
- 'surfaceScale', 'diffuseConstant', 'specularConstant', 'specularExponent',
60
- 'kernelMatrix', 'order', 'divisor', 'bias', 'targetX', 'targetY',
61
- 'edgeMode', 'kernelUnitLength', 'preserveAlpha', 'baseFrequency',
62
- 'numOctaves', 'seed', 'stitchTiles', 'scale', 'xChannelSelector', 'yChannelSelector',
63
- // Clipping & masking
64
- 'clipPathUnits', 'clip-path', 'clip-rule', 'mask', 'maskUnits', 'maskContentUnits',
65
- // Markers
66
- 'markerUnits', 'markerWidth', 'markerHeight', 'orient', 'refX', 'refY',
67
- 'marker-start', 'marker-mid', 'marker-end',
68
- // Links
69
- 'href', 'xlink:href',
70
- // Misc
71
- 'xmlns', 'xmlns:xlink', 'version', 'points', 'overflow', 'vector-effect',
72
- // Animation attributes
73
- 'attributeName', 'attributeType', 'begin', 'dur', 'end', 'min', 'max',
74
- 'restart', 'repeatCount', 'repeatDur', 'calcMode', 'keyTimes', 'keySplines',
75
- 'from', 'to', 'by', 'additive', 'accumulate',
76
- ];
77
-
78
- // Check if SVG content appears incomplete (streaming)
79
- const isLikelyIncomplete = (code: string): boolean => {
80
- const trimmed = code.trim();
81
-
82
- // Check for unclosed SVG tag
83
- if (trimmed.includes('<svg') && !trimmed.includes('</svg>')) {
84
- return true;
85
- }
86
-
87
- // Check for unbalanced tags (simple heuristic)
88
- const openTags = (trimmed.match(/<[a-zA-Z][^/>]*>/g) || []).length;
89
- const closeTags = (trimmed.match(/<\/[a-zA-Z][^>]*>/g) || []).length;
90
- const selfClosing = (trimmed.match(/<[a-zA-Z][^>]*\/>/g) || []).length;
91
-
92
- if (openTags > closeTags + selfClosing) {
93
- return true;
94
- }
95
-
96
- // Check for incomplete attribute
97
- if (trimmed.match(/\s+\w+\s*=\s*["'][^"']*$/)) {
98
- return true;
99
- }
100
-
101
- // Check for unclosed tag
102
- if (trimmed.match(/<[a-zA-Z][^>]*$/)) {
103
- return true;
104
- }
105
-
106
- return false;
107
- };
108
-
109
- // Ensure SVG has proper attributes for responsive display
110
- const ensureResponsiveSvg = (svgContent: string): string => {
111
- // Parse to check/modify SVG attributes
112
- const parser = new DOMParser();
113
- const doc = parser.parseFromString(svgContent, 'image/svg+xml');
114
- const svg = doc.querySelector('svg');
115
-
116
- if (!svg) return svgContent;
117
-
118
- // If SVG has width/height but no viewBox, create one
119
- const width = svg.getAttribute('width');
120
- const height = svg.getAttribute('height');
121
- const viewBox = svg.getAttribute('viewBox');
122
-
123
- if (width && height && !viewBox) {
124
- const w = parseFloat(width) || 100;
125
- const h = parseFloat(height) || 100;
126
- svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
127
- }
128
-
129
- // Ensure responsive sizing
130
- if (!svg.style.maxWidth) {
131
- svg.style.maxWidth = '100%';
132
- }
133
- if (!svg.style.height) {
134
- svg.style.height = 'auto';
135
- }
136
-
137
- return svg.outerHTML;
138
- };
139
-
140
- export const SVGRenderer: React.FC<SVGRendererProps> = ({ code }) => {
141
- const [sanitizedSvg, setSanitizedSvg] = useState<string | null>(null);
142
- const [error, setError] = useState<string | null>(null);
143
- const [isStreaming, setIsStreaming] = useState(false);
144
- const [showErrorDetails, setShowErrorDetails] = useState(false);
145
- const lastCodeRef = useRef<string>('');
146
- const lastUpdateTimeRef = useRef<number>(0);
147
-
148
- useEffect(() => {
149
- const trimmed = code.trim();
150
- if (!trimmed) {
151
- setSanitizedSvg(null);
152
- setError(null);
153
- return;
154
- }
155
-
156
- const now = Date.now();
157
- const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
158
- const codeChanged = code !== lastCodeRef.current;
159
-
160
- lastCodeRef.current = code;
161
- lastUpdateTimeRef.current = now;
162
-
163
- // Detect streaming
164
- const incomplete = isLikelyIncomplete(trimmed);
165
- const rapidUpdate = codeChanged && timeSinceLastUpdate < 500 && timeSinceLastUpdate > 0;
166
- const likelyStreaming = incomplete || rapidUpdate;
167
-
168
- if (incomplete) {
169
- setIsStreaming(true);
170
- // Don't try to render incomplete SVG
171
- return;
172
- }
173
-
174
- try {
175
- // Sanitize the SVG content with comprehensive allowlist
176
- const sanitized = DOMPurify.sanitize(code, {
177
- USE_PROFILES: { svg: true, svgFilters: true },
178
- ADD_TAGS: ALLOWED_SVG_TAGS,
179
- ADD_ATTR: ALLOWED_SVG_ATTRS,
180
- ALLOW_DATA_ATTR: false,
181
- FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
182
- FORBID_ATTR: ['onload', 'onerror', 'onclick', 'onmouseover'],
183
- });
184
-
185
- if (!sanitized || sanitized.trim().length === 0) {
186
- throw new Error('SVG sanitization resulted in empty content');
187
- }
188
-
189
- // Check if the result is actually an SVG
190
- if (!sanitized.includes('<svg')) {
191
- throw new Error('Content does not appear to be valid SVG');
192
- }
193
-
194
- // Make SVG responsive
195
- const responsiveSvg = ensureResponsiveSvg(sanitized);
196
-
197
- setSanitizedSvg(responsiveSvg);
198
- setError(null);
199
- setIsStreaming(false);
200
- } catch (err) {
201
- if (likelyStreaming) {
202
- setIsStreaming(true);
203
- setError(null);
204
- } else {
205
- setError(err instanceof Error ? err.message : 'Failed to process SVG');
206
- setIsStreaming(false);
207
- }
208
- }
209
- }, [code]);
210
-
211
- if (error) {
212
- return (
213
- <div className="graph-error">
214
- <div className="graph-error-header">
215
- <div className="graph-error-icon">⚠️</div>
216
- <div className="graph-error-content">
217
- <div className="graph-error-title">SVG Rendering Error</div>
218
- <div className="graph-error-message">{error}</div>
219
- </div>
220
- </div>
221
- <button
222
- className="graph-error-toggle"
223
- onClick={() => setShowErrorDetails(!showErrorDetails)}
224
- type="button"
225
- >
226
- {showErrorDetails ? 'Hide' : 'Show'} Details
227
- </button>
228
- {showErrorDetails && (
229
- <pre style={{
230
- marginTop: '8px',
231
- fontSize: '0.8em',
232
- opacity: 0.8,
233
- padding: '8px',
234
- background: 'rgba(0, 0, 0, 0.05)',
235
- borderRadius: '4px',
236
- overflow: 'auto',
237
- maxHeight: '200px',
238
- }}>
239
- <code>{code}</code>
240
- </pre>
241
- )}
242
- </div>
243
- );
244
- }
245
-
246
- if (!sanitizedSvg || isStreaming) {
247
- return (
248
- <div className="graph-container svg-container">
249
- <div style={{
250
- display: 'flex',
251
- flexDirection: 'column',
252
- alignItems: 'center',
253
- justifyContent: 'center',
254
- padding: '30px 20px',
255
- color: 'var(--md-text-secondary, #6b7280)',
256
- minHeight: '120px',
257
- }}>
258
- <svg
259
- style={{
260
- animation: 'spin 1s linear infinite',
261
- marginBottom: '10px',
262
- width: '28px',
263
- height: '28px',
264
- }}
265
- viewBox="0 0 24 24"
266
- fill="none"
267
- >
268
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="32" strokeLinecap="round" />
269
- </svg>
270
- <span style={{ fontWeight: 500, fontSize: '14px' }}>
271
- {isStreaming ? 'Receiving SVG data...' : 'Processing SVG...'}
272
- </span>
273
- <style>{`
274
- @keyframes spin {
275
- from { transform: rotate(0deg); }
276
- to { transform: rotate(360deg); }
277
- }
278
- `}</style>
279
- </div>
280
- </div>
281
- );
282
- }
283
-
284
- return (
285
- <div
286
- className="graph-container svg-container"
287
- style={{
288
- padding: '16px',
289
- display: 'flex',
290
- justifyContent: 'center',
291
- alignItems: 'center',
292
- }}
293
- >
294
- <div
295
- dangerouslySetInnerHTML={{ __html: sanitizedSvg }}
296
- style={{
297
- display: 'flex',
298
- justifyContent: 'center',
299
- alignItems: 'center',
300
- maxWidth: '100%',
301
- overflow: 'auto',
302
- }}
303
- />
304
- </div>
305
- );
306
- };
307
-