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.
- package/dist/assets/ChartRenderer-C5FkaI18.js +80 -0
- package/dist/assets/MermaidRenderer-CghpQL29.js +259 -0
- package/dist/assets/{MusicRenderer-DZhuX52M.js → MusicRenderer-Bsg1uCOO.js} +2 -2
- package/dist/assets/{SVGRenderer-CB5j7ekx.js → SVGRenderer-BkbsVjFt.js} +1 -1
- package/dist/assets/_basePickBy-BmphHeNv.js +1 -0
- package/dist/assets/{_baseUniq-BFwLbgVF.js → _baseUniq-DcPuVRoH.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-Dhht8baZ.js → architectureDiagram-2XIMDMQ5-ejVJFzXd.js} +3 -3
- package/dist/assets/blockDiagram-WCTKOSBZ-In1L1uKu.js +132 -0
- package/dist/assets/c4Diagram-IC4MRINW-DUOWrv1E.js +10 -0
- package/dist/assets/channel-Cgb_NwCj.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-FoAnG2DD.js → chunk-4BX2VUAB-CEKQJtxy.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Cj902k47.js → chunk-55IACEB6-CZtwN-ba.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-D8TmopkR.js → chunk-FMBD7UC4-DC7v8pYp.js} +1 -1
- package/dist/assets/chunk-JSJVCQXG-BImJsuxH.js +1 -0
- package/dist/assets/{chunk-QN33PNHL-DDhJT2OP.js → chunk-KX2RTZJC-Bn6iA2gB.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-YO8RmHLs.js → chunk-NQ4KR5QH-CuypZAVn.js} +4 -4
- package/dist/assets/{chunk-QZHKN3VN-CCSqXR9J.js → chunk-QZHKN3VN-BWpcmxTA.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-DYuNq0mZ.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-fyPj1cvz.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-fyPj1cvz.js +1 -0
- package/dist/assets/clone-Cw0uzdEo.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Dp7UKE-6.js → cose-bilkent-S5V4N54A-CrYky9tE.js} +1 -1
- package/dist/assets/dagre-KLK3FWXG-Bu-DLCK7.js +4 -0
- package/dist/assets/diagram-E7M64L7V-aAXWrxTM.js +24 -0
- package/dist/assets/diagram-IFDJBPK2-Dw4zd9rB.js +43 -0
- package/dist/assets/{diagram-S2PKOQOG-COTWuuiM.js → diagram-P4PSJMXO-45WsYDhT.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-DOTnUTaB.js +70 -0
- package/dist/assets/flowDiagram-PKNHOUZH-DG_tQtcp.js +162 -0
- package/dist/assets/{ganttDiagram-JELNMOA3-nAhYUBJC.js → ganttDiagram-A5KZAMGK-BzsGIuEd.js} +30 -5
- package/dist/assets/gitGraphDiagram-K3NZZRJ6-Cj6jckyn.js +65 -0
- package/dist/assets/{graph-CVfZ5ZRD.js → graph-DTIKcgiu.js} +1 -1
- package/dist/assets/index-C3vuLth5.js +620 -0
- package/dist/assets/{index-DuEkeKcS.js → index-C4cdR4BU.js} +1 -1
- package/dist/assets/index-DMCMxyfd.css +1 -0
- package/dist/assets/infoDiagram-LFFYTUFH-CCtedtHp.js +2 -0
- package/dist/assets/ishikawaDiagram-PHBUUO56-CLGCPNak.js +70 -0
- package/dist/assets/{journeyDiagram-XKPGCS4Q-DzPJjseD.js → journeyDiagram-4ABVD52K-BXpAIS18.js} +3 -3
- package/dist/assets/{kanban-definition-3W4ZIXB7-BMPHlnkL.js → kanban-definition-K7BYSVSG-Dz6JMJ6P.js} +5 -5
- package/dist/assets/{layout-Cdwf2_35.js → layout-1zKkDoXK.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-76g77fcj.js → mindmap-definition-YRQLILUH-Bk0RuY2l.js} +7 -7
- package/dist/assets/{pieDiagram-ADFJNKIX-_2bZQVhp.js → pieDiagram-SKSYHLDU-BRxRa9wV.js} +2 -2
- package/dist/assets/purify.es-DIZLy5JB.js +2 -0
- package/dist/assets/{quadrantDiagram-AYHSOK5B-DHidw6CG.js → quadrantDiagram-337W2JSQ-0xXACG3E.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-BosyfqW6.js → requirementDiagram-Z7DCOOCP-Cbizpsr2.js} +14 -5
- package/dist/assets/{sankeyDiagram-TZEHDZUN-kXQhuPRq.js → sankeyDiagram-WA2Y5GQK-C-2SmZmM.js} +1 -1
- package/dist/assets/sequenceDiagram-2WXFIKYE-BIt_HBTk.js +145 -0
- package/dist/assets/{stateDiagram-FKZM4ZOC-DUwTvhKc.js → stateDiagram-RAJIS63D-q_Cz5sxe.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DNYb0Gsx.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-tUdTwV48.js → timeline-definition-YZTLITO2-BwLu_3Bg.js} +1 -1
- package/dist/assets/treemap-KZPCXAKY-DUv4YaOt.js +162 -0
- package/dist/assets/vennDiagram-LZ73GAT5-CfA8AUEM.js +34 -0
- package/dist/assets/{xychartDiagram-PRI3JC2R-2ndTjhZS.js → xychartDiagram-JWTSCODW-ByOeXycF.js} +2 -2
- package/dist/index.html +2 -2
- package/package.json +1 -9
- package/dist/assets/ChartRenderer-BtX7_jv5.js +0 -80
- package/dist/assets/MermaidRenderer-DLpT9XPj.js +0 -260
- package/dist/assets/_basePickBy-KeSLCJM0.js +0 -1
- package/dist/assets/blockDiagram-VD42YOAC-C0uY9SKW.js +0 -122
- package/dist/assets/c4Diagram-YG6GDRKO-AGMRXqhN.js +0 -10
- package/dist/assets/channel-D-yx-ubr.js +0 -1
- package/dist/assets/chunk-B4BG7PRW-DZisX-Yn.js +0 -165
- package/dist/assets/chunk-TZMSLE5B-DtWrsAau.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-BOVDq9sH.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-BOVDq9sH.js +0 -1
- package/dist/assets/clone-DOmxAX3a.js +0 -1
- package/dist/assets/dagre-6UL2VRFP-NEctntTO.js +0 -4
- package/dist/assets/diagram-PSM6KHXK-Bdb-Z7Rq.js +0 -24
- package/dist/assets/diagram-QEK2KX5R-Cxi1cnQw.js +0 -43
- package/dist/assets/erDiagram-Q2GNP2WA-CgLq4-Sy.js +0 -60
- package/dist/assets/flowDiagram-NV44I4VS-CVdT6vYV.js +0 -162
- package/dist/assets/gitGraphDiagram-V2S2FVAM-cZq9sWZG.js +0 -65
- package/dist/assets/index-Baf0NBsK.css +0 -1
- package/dist/assets/index-CmDt8-sd.js +0 -621
- package/dist/assets/infoDiagram-HS3SLOUP-DFQtcUA2.js +0 -2
- package/dist/assets/purify.es-A66Cw1IH.js +0 -2
- package/dist/assets/sequenceDiagram-WL72ISMW-CQCG4z68.js +0 -145
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Ccelj_jo.js +0 -1
- package/dist/assets/treemap-GDKQZRPO-AkKmBZRv.js +0 -160
- package/markdown-renderer/LICENSE +0 -201
- package/markdown-renderer/src/CodeBlock.tsx +0 -332
- package/markdown-renderer/src/MarkdownComponents.tsx +0 -233
- package/markdown-renderer/src/MarkdownStyles.css +0 -732
- package/markdown-renderer/src/css.d.ts +0 -4
- package/markdown-renderer/src/index.ts +0 -32
- package/markdown-renderer/src/preprocessing.ts +0 -519
- package/markdown-renderer/src/renderers/ChartRenderer.tsx +0 -1464
- package/markdown-renderer/src/renderers/MermaidRenderer.tsx +0 -474
- package/markdown-renderer/src/renderers/MusicRenderer.tsx +0 -394
- package/markdown-renderer/src/renderers/SVGRenderer.tsx +0 -307
- 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
|
-
|