diff options
Diffstat (limited to 'subprojects/frontend/src/graph/export/exportDiagram.tsx')
-rw-r--r-- | subprojects/frontend/src/graph/export/exportDiagram.tsx | 122 |
1 files changed, 97 insertions, 25 deletions
diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx index 6abbcfdf..a0c3460c 100644 --- a/subprojects/frontend/src/graph/export/exportDiagram.tsx +++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx | |||
@@ -16,6 +16,7 @@ import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; | |||
16 | import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; | 16 | import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; |
17 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; | 17 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; |
18 | import type { Theme } from '@mui/material/styles'; | 18 | import type { Theme } from '@mui/material/styles'; |
19 | import { nanoid } from 'nanoid'; | ||
19 | 20 | ||
20 | import { darkTheme, lightTheme } from '../../theme/ThemeProvider'; | 21 | import { darkTheme, lightTheme } from '../../theme/ThemeProvider'; |
21 | import { copyBlob, saveBlob } from '../../utils/fileIO'; | 22 | import { copyBlob, saveBlob } from '../../utils/fileIO'; |
@@ -48,6 +49,36 @@ importSVG(labelSVG, 'icon-TRUE'); | |||
48 | importSVG(labelOutlinedSVG, 'icon-UNKNOWN'); | 49 | importSVG(labelOutlinedSVG, 'icon-UNKNOWN'); |
49 | importSVG(cancelSVG, 'icon-ERROR'); | 50 | importSVG(cancelSVG, 'icon-ERROR'); |
50 | 51 | ||
52 | function fixIDs(id: string, svgDocument: XMLDocument) { | ||
53 | const idMap = new Map<string, string>(); | ||
54 | let i = 0; | ||
55 | svgDocument.querySelectorAll('[id]').forEach((node) => { | ||
56 | const oldId = node.getAttribute('id'); | ||
57 | if (oldId === null) { | ||
58 | return; | ||
59 | } | ||
60 | if (oldId.endsWith(',clip')) { | ||
61 | const newId = `refinery-${id}-clip-${i}`; | ||
62 | i += 1; | ||
63 | idMap.set(`url(#${oldId})`, `url(#${newId})`); | ||
64 | node.setAttribute('id', newId); | ||
65 | } else { | ||
66 | node.setAttribute('id', ''); | ||
67 | } | ||
68 | }); | ||
69 | svgDocument.querySelectorAll('[clip-path]').forEach((node) => { | ||
70 | const oldPath = node.getAttribute('clip-path'); | ||
71 | if (oldPath === null) { | ||
72 | return; | ||
73 | } | ||
74 | const newPath = idMap.get(oldPath); | ||
75 | if (newPath === undefined) { | ||
76 | return; | ||
77 | } | ||
78 | node.setAttribute('clip-path', newPath); | ||
79 | }); | ||
80 | } | ||
81 | |||
51 | function addBackground( | 82 | function addBackground( |
52 | svgDocument: XMLDocument, | 83 | svgDocument: XMLDocument, |
53 | svg: SVGSVGElement, | 84 | svg: SVGSVGElement, |
@@ -142,40 +173,54 @@ async function fetchVariableFontCSS(): Promise<string> { | |||
142 | return variableFontCSS; | 173 | return variableFontCSS; |
143 | } | 174 | } |
144 | 175 | ||
176 | interface ThemeVariant { | ||
177 | selector: string; | ||
178 | theme: Theme; | ||
179 | } | ||
180 | |||
145 | function appendStyles( | 181 | function appendStyles( |
182 | id: string, | ||
146 | svgDocument: XMLDocument, | 183 | svgDocument: XMLDocument, |
147 | svg: SVGSVGElement, | 184 | svg: SVGSVGElement, |
148 | theme: Theme, | 185 | themes: ThemeVariant[], |
149 | colorNodes: boolean, | 186 | colorNodes: boolean, |
150 | hexTypeHashes: string[], | 187 | hexTypeHashes: string[], |
151 | fontsCSS: string, | 188 | fontsCSS: string, |
152 | ): void { | 189 | ): void { |
153 | const cache = createCache({ | 190 | const className = `refinery-${id}`; |
154 | key: 'refinery', | 191 | svg.classList.add(className); |
155 | container: svg, | ||
156 | prepend: true, | ||
157 | }); | ||
158 | // @ts-expect-error `CSSObject` types don't match up between `@mui/material` and | ||
159 | // `@emotion/serialize`, but they are compatible in practice. | ||
160 | const styles = serializeStyles([createGraphTheme], cache.registered, { | ||
161 | theme, | ||
162 | colorNodes, | ||
163 | hexTypeHashes, | ||
164 | noEmbedIcons: true, | ||
165 | }); | ||
166 | const rules: string[] = [fontsCSS]; | 192 | const rules: string[] = [fontsCSS]; |
167 | const sheet = { | 193 | themes.forEach(({ selector, theme }) => { |
168 | insert(rule) { | 194 | const cache = createCache({ |
169 | rules.push(rule); | 195 | key: 'refinery', |
170 | }, | 196 | container: svg, |
171 | } as StyleSheet; | 197 | prepend: true, |
172 | cache.insert('', styles, sheet, false); | 198 | }); |
199 | // @ts-expect-error `CSSObject` types don't match up between `@mui/material` and | ||
200 | // `@emotion/serialize`, but they are compatible in practice. | ||
201 | const styles = serializeStyles([createGraphTheme], cache.registered, { | ||
202 | theme, | ||
203 | colorNodes, | ||
204 | hexTypeHashes, | ||
205 | noEmbedIcons: true, | ||
206 | }); | ||
207 | const sheet = { | ||
208 | insert(rule) { | ||
209 | rules.push(rule); | ||
210 | }, | ||
211 | } as StyleSheet; | ||
212 | cache.insert(`${selector} .${className}`, styles, sheet, false); | ||
213 | }); | ||
173 | const styleElement = svgDocument.createElementNS(SVG_NS, 'style'); | 214 | const styleElement = svgDocument.createElementNS(SVG_NS, 'style'); |
174 | svg.prepend(styleElement); | 215 | svg.prepend(styleElement); |
175 | styleElement.innerHTML = rules.join(''); | 216 | styleElement.innerHTML = rules.join(''); |
176 | } | 217 | } |
177 | 218 | ||
178 | function fixForeignObjects(svgDocument: XMLDocument, svg: SVGSVGElement): void { | 219 | function fixForeignObjects( |
220 | id: string, | ||
221 | svgDocument: XMLDocument, | ||
222 | svg: SVGSVGElement, | ||
223 | ): void { | ||
179 | const foreignObjects: SVGForeignObjectElement[] = []; | 224 | const foreignObjects: SVGForeignObjectElement[] = []; |
180 | svg | 225 | svg |
181 | .querySelectorAll('foreignObject') | 226 | .querySelectorAll('foreignObject') |
@@ -197,7 +242,7 @@ function fixForeignObjects(svgDocument: XMLDocument, svg: SVGSVGElement): void { | |||
197 | object.children[0]?.classList?.forEach((className) => { | 242 | object.children[0]?.classList?.forEach((className) => { |
198 | useElement.classList.add(className); | 243 | useElement.classList.add(className); |
199 | if (ICONS.has(className)) { | 244 | if (ICONS.has(className)) { |
200 | useElement.setAttribute('href', `#${className}`); | 245 | useElement.setAttribute('href', `#refinery-${id}-${className}`); |
201 | } | 246 | } |
202 | }); | 247 | }); |
203 | object.replaceWith(useElement); | 248 | object.replaceWith(useElement); |
@@ -206,6 +251,7 @@ function fixForeignObjects(svgDocument: XMLDocument, svg: SVGSVGElement): void { | |||
206 | svg.prepend(defs); | 251 | svg.prepend(defs); |
207 | ICONS.forEach((value) => { | 252 | ICONS.forEach((value) => { |
208 | const importedValue = svgDocument.importNode(value, true); | 253 | const importedValue = svgDocument.importNode(value, true); |
254 | importedValue.id = `refinery-${id}-${importedValue.id}`; | ||
209 | defs.appendChild(importedValue); | 255 | defs.appendChild(importedValue); |
210 | }); | 256 | }); |
211 | } | 257 | } |
@@ -322,12 +368,37 @@ export default async function exportDiagram( | |||
322 | svgDocument.replaceChild(copyOfSVG, originalRoot); | 368 | svgDocument.replaceChild(copyOfSVG, originalRoot); |
323 | } | 369 | } |
324 | 370 | ||
325 | const theme = settings.theme === 'light' ? lightTheme : darkTheme; | 371 | const id = nanoid(); |
372 | fixIDs(id, svgDocument); | ||
373 | |||
374 | let theme: Theme; | ||
375 | let themes: ThemeVariant[]; | ||
376 | if (settings.theme === 'dynamic') { | ||
377 | theme = lightTheme; | ||
378 | themes = [ | ||
379 | { | ||
380 | selector: '', | ||
381 | theme: lightTheme, | ||
382 | }, | ||
383 | { | ||
384 | selector: '[data-theme="dark"]', | ||
385 | theme: darkTheme, | ||
386 | }, | ||
387 | ]; | ||
388 | } else { | ||
389 | theme = settings.theme === 'light' ? lightTheme : darkTheme; | ||
390 | themes = [ | ||
391 | { | ||
392 | selector: '', | ||
393 | theme, | ||
394 | }, | ||
395 | ]; | ||
396 | } | ||
326 | if (!settings.transparent) { | 397 | if (!settings.transparent) { |
327 | addBackground(svgDocument, copyOfSVG, theme); | 398 | addBackground(svgDocument, copyOfSVG, theme); |
328 | } | 399 | } |
329 | 400 | ||
330 | fixForeignObjects(svgDocument, copyOfSVG); | 401 | fixForeignObjects(id, svgDocument, copyOfSVG); |
331 | 402 | ||
332 | const { colorNodes } = graph; | 403 | const { colorNodes } = graph; |
333 | let fontsCSS = ''; | 404 | let fontsCSS = ''; |
@@ -339,9 +410,10 @@ export default async function exportDiagram( | |||
339 | fontsCSS = await fetchFontCSS(); | 410 | fontsCSS = await fetchFontCSS(); |
340 | } | 411 | } |
341 | appendStyles( | 412 | appendStyles( |
413 | id, | ||
342 | svgDocument, | 414 | svgDocument, |
343 | copyOfSVG, | 415 | copyOfSVG, |
344 | theme, | 416 | themes, |
345 | colorNodes, | 417 | colorNodes, |
346 | graph.hexTypeHashes, | 418 | graph.hexTypeHashes, |
347 | fontsCSS, | 419 | fontsCSS, |