aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/export/exportDiagram.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/export/exportDiagram.tsx')
-rw-r--r--subprojects/frontend/src/graph/export/exportDiagram.tsx122
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';
16import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; 16import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
17import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; 17import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
18import type { Theme } from '@mui/material/styles'; 18import type { Theme } from '@mui/material/styles';
19import { nanoid } from 'nanoid';
19 20
20import { darkTheme, lightTheme } from '../../theme/ThemeProvider'; 21import { darkTheme, lightTheme } from '../../theme/ThemeProvider';
21import { copyBlob, saveBlob } from '../../utils/fileIO'; 22import { copyBlob, saveBlob } from '../../utils/fileIO';
@@ -48,6 +49,36 @@ importSVG(labelSVG, 'icon-TRUE');
48importSVG(labelOutlinedSVG, 'icon-UNKNOWN'); 49importSVG(labelOutlinedSVG, 'icon-UNKNOWN');
49importSVG(cancelSVG, 'icon-ERROR'); 50importSVG(cancelSVG, 'icon-ERROR');
50 51
52function 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
51function addBackground( 82function 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
176interface ThemeVariant {
177 selector: string;
178 theme: Theme;
179}
180
145function appendStyles( 181function 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
178function fixForeignObjects(svgDocument: XMLDocument, svg: SVGSVGElement): void { 219function 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,