Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/formats/html/mermaid/mermaid-init.js
12923 views
1
// mermaid-init.js
2
// Initializes the quarto-mermaid JS runtime
3
//
4
// Copyright (C) 2022 Posit Software, PBC
5
6
/**
7
* String.prototype.replaceAll() polyfill
8
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
9
* @author Chris Ferdinandi
10
* @license MIT
11
*/
12
if (!String.prototype.replaceAll) {
13
String.prototype.replaceAll = function (str, newStr) {
14
// If a regex pattern
15
if (
16
Object.prototype.toString.call(str).toLowerCase() === "[object regexp]"
17
) {
18
return this.replace(str, newStr);
19
}
20
21
// If a string
22
return this.replace(new RegExp(str, "g"), newStr);
23
};
24
}
25
26
const mermaidOpts = {
27
startOnLoad: false,
28
};
29
// this CSS is adapted from
30
// mkdocs-material
31
// Copyright (c) 2016-2022 Martin Donath <[email protected]>
32
33
const defaultCSS =
34
".label text{fill:var(--mermaid-fg-color)}.node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--mermaid-node-bg-color);stroke:var(--mermaid-node-fg-color)}marker{fill:var(--mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--mermaid-label-bg-color);color:var(--mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--mermaid-edge-color);stroke:none}.cluster rect{fill:var(--mermaid-fg-color--lightest);stroke:var(--mermaid-fg-color--lighter)}.cluster span{color:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}defs #flowchart-circleEnd,defs #flowchart-circleStart,defs #flowchart-crossEnd,defs #flowchart-crossStart,defs #flowchart-pointEnd,defs #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--mermaid-node-bg-color);stroke:var(--mermaid-node-fg-color)}g.classGroup text{fill:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.classLabel .box{fill:var(--mermaid-label-bg-color);background-color:var(--mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.node .divider{stroke:var(--mermaid-node-fg-color)}.relation{stroke:var(--mermaid-edge-color)}.cardinality{fill:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--mermaid-edge-color)!important;stroke:var(--mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--mermaid-label-bg-color)!important;stroke:var(--mermaid-edge-color)!important}g.stateGroup rect{fill:var(--mermaid-node-bg-color);stroke:var(--mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--mermaid-label-fg-color)!important;font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}g.stateGroup .composit{fill:var(--mermaid-label-bg-color)}.nodeLabel{color:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--mermaid-label-bg-color)}.transition{stroke:var(--mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--mermaid-bg-color)}.statediagram-cluster rect{fill:var(--mermaid-node-bg-color);stroke:var(--mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--mermaid-fg-color--lightest);stroke:var(--mermaid-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--mermaid-edge-color)}.entityBox{fill:var(--mermaid-label-bg-color);stroke:var(--mermaid-node-fg-color)}.entityLabel{fill:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}.relationshipLabelBox{fill:var(--mermaid-label-bg-color);fill-opacity:1;background-color:var(--mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--mermaid-label-fg-color)}.relationshipLine{stroke:var(--mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--mermaid-edge-color)!important}.actor,defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--mermaid-label-bg-color)}.actor{stroke:var(--mermaid-node-fg-color)}text.actor>tspan{fill:var(--mermaid-label-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}line{stroke:var(--mermaid-fg-color--lighter)}.messageLine0,.messageLine1{stroke:var(--mermaid-edge-color)}.loopText>tspan,.messageText,.noteText>tspan{fill:var(--mermaid-edge-color);stroke:none;font-family:var(--mermaid-font-family)!important;font-weight:var(--mermaid-font-weight)!important}.noteText>tspan{fill:#000}#arrowhead path{fill:var(--mermaid-edge-color);stroke:none}.loopLine{stroke:var(--mermaid-node-fg-color)}.labelBox,.loopLine{fill:var(--mermaid-node-bg-color)}.labelBox{stroke:none}.labelText,.labelText>span{fill:var(--mermaid-node-fg-color);font-family:var(--mermaid-font-family);font-weight:var(--mermaid-font-weight)}";
35
36
const mermaidThemeEl = document.querySelector('meta[name="mermaid-theme"]');
37
if (mermaidThemeEl) {
38
mermaidOpts.theme = mermaidThemeEl.content;
39
} else {
40
mermaidOpts.themeCSS = defaultCSS;
41
}
42
43
mermaid.initialize(mermaidOpts);
44
45
const _quartoMermaid = {
46
// NB: there's effectively a copy of this function
47
// in `core/svg.ts`.
48
// if you change something here, you must keep it consistent there as well.
49
setSvgSize(svg) {
50
const { widthInPoints, heightInPoints, explicitHeight, explicitWidth } =
51
this.resolveSize(svg);
52
53
if (explicitWidth && explicitHeight) {
54
svg.setAttribute("width", widthInPoints);
55
svg.setAttribute("height", heightInPoints);
56
svg.style.maxWidth = null; // remove mermaid's default max-width
57
} else {
58
if (explicitWidth) {
59
svg.style.maxWidth = `${widthInPoints}px`;
60
}
61
if (explicitHeight) {
62
svg.style.maxHeight = `${heightInPoints}px`;
63
}
64
}
65
},
66
67
// NB: there's effectively a copy of this function
68
// in `core/svg.ts`.
69
// if you change something here, you must keep it consistent there as well.
70
makeResponsive(svg) {
71
const width = svg.getAttribute("width");
72
if (width === null) {
73
throw new Error("Couldn't find SVG width");
74
}
75
const numWidth = Number(width.slice(0, -2));
76
77
if (numWidth > 650) {
78
changed = true;
79
svg.setAttribute("width", "100%");
80
svg.removeAttribute("height");
81
}
82
},
83
84
// NB: there's effectively a copy of this function
85
// in `core/svg.ts`.
86
// if you change something here, you must keep it consistent there as well.
87
fixupAlignment(svg, align) {
88
let style = svg.getAttribute("style") || "";
89
90
switch (align) {
91
case "left":
92
style = `${style}; display: block; margin: auto auto auto 0`;
93
break;
94
case "right":
95
style = `${style}; display: block; margin: auto 0 auto auto`;
96
break;
97
case "center":
98
style = `${style}; display: block; margin: auto auto auto auto`;
99
break;
100
}
101
svg.setAttribute("style", style);
102
},
103
104
resolveOptions(svgEl) {
105
return svgEl.parentElement.parentElement.parentElement.parentElement
106
.dataset;
107
},
108
109
// NB: there's effectively a copy of this function
110
// in our mermaid runtime in `core/svg.ts`.
111
// if you change something here, you must keep it consistent there as well.
112
resolveSize(svgEl) {
113
const inInches = (size) => {
114
if (size.endsWith("in")) {
115
return Number(size.slice(0, -2));
116
}
117
if (size.endsWith("pt") || size.endsWith("px")) {
118
// assume 96 dpi for now
119
return Number(size.slice(0, -2)) / 96;
120
}
121
return Number(size);
122
};
123
124
// these are figWidth and figHeight on purpose,
125
// because data attributes are translated to camelCase by the DOM API
126
const kFigWidth = "figWidth",
127
kFigHeight = "figHeight";
128
const options = this.resolveOptions(svgEl);
129
let width = svgEl.getAttribute("width");
130
let height = svgEl.getAttribute("height");
131
const getViewBox = () => {
132
const vb = svgEl.attributes.getNamedItem("viewBox").value; // do it the roundabout way so that viewBox isn't dropped by deno_dom and text/html
133
if (!vb) return undefined;
134
const lst = vb.trim().split(" ").map(Number);
135
if (lst.length !== 4) return undefined;
136
if (lst.some(isNaN)) return undefined;
137
return lst;
138
};
139
if (!width || !height) {
140
// attempt to resolve figure dimensions via viewBox
141
const viewBox = getViewBox();
142
if (viewBox !== undefined) {
143
const [_mx, _my, vbWidth, vbHeight] = viewBox;
144
width = `${vbWidth}px`;
145
height = `${vbHeight}px`;
146
} else {
147
throw new Error(
148
"Mermaid generated an SVG without a viewbox attribute. Without knowing the diagram dimensions, quarto cannot convert it to a PNG"
149
);
150
}
151
}
152
153
let svgWidthInInches, svgHeightInInches;
154
155
if (
156
(width.slice(0, -2) === "pt" && height.slice(0, -2) === "pt") ||
157
(width.slice(0, -2) === "px" && height.slice(0, -2) === "px") ||
158
(!isNaN(Number(width)) && !isNaN(Number(height)))
159
) {
160
// we assume 96 dpi which is generally what seems to be used.
161
svgWidthInInches = Number(width.slice(0, -2)) / 96;
162
svgHeightInInches = Number(height.slice(0, -2)) / 96;
163
}
164
const viewBox = getViewBox();
165
if (viewBox !== undefined) {
166
// assume width and height come from viewbox.
167
const [_mx, _my, vbWidth, vbHeight] = viewBox;
168
svgWidthInInches = vbWidth / 96;
169
svgHeightInInches = vbHeight / 96;
170
} else {
171
throw new Error(
172
"Internal Error: Couldn't resolve width and height of SVG"
173
);
174
}
175
const svgWidthOverHeight = svgWidthInInches / svgHeightInInches;
176
let widthInInches, heightInInches;
177
178
if (options[kFigWidth] && options[kFigHeight]) {
179
// both were prescribed, so just go with them
180
widthInInches = inInches(String(options[kFigWidth]));
181
heightInInches = inInches(String(options[kFigHeight]));
182
} else if (options[kFigWidth]) {
183
// we were only given width, use that and adjust height based on aspect ratio;
184
widthInInches = inInches(String(options[kFigWidth]));
185
heightInInches = widthInInches / svgWidthOverHeight;
186
} else if (options[kFigHeight]) {
187
// we were only given height, use that and adjust width based on aspect ratio;
188
heightInInches = inInches(String(options[kFigHeight]));
189
widthInInches = heightInInches * svgWidthOverHeight;
190
} else {
191
// we were not given either, use svg's prescribed height
192
heightInInches = svgHeightInInches;
193
widthInInches = svgWidthInInches;
194
}
195
196
return {
197
widthInInches,
198
heightInInches,
199
widthInPoints: Math.round(widthInInches * 96),
200
heightInPoints: Math.round(heightInInches * 96),
201
explicitWidth: options?.[kFigWidth] !== undefined,
202
explicitHeight: options?.[kFigHeight] !== undefined,
203
};
204
},
205
206
postProcess(svg) {
207
const options = this.resolveOptions(svg);
208
if (
209
options.responsive &&
210
options["figWidth"] === undefined &&
211
options["figHeight"] === undefined
212
) {
213
this.makeResponsive(svg);
214
} else {
215
this.setSvgSize(svg);
216
}
217
if (options["reveal"]) {
218
this.fixupAlignment(svg, options["figAlign"] || "center");
219
}
220
221
// forward align attributes to the correct parent dif
222
// so that the svg figure is aligned correctly
223
const div = svg.parentElement.parentElement.parentElement;
224
const align = div.parentElement.parentElement.dataset.layoutAlign;
225
if (align) {
226
div.classList.remove("quarto-figure-left");
227
div.classList.remove("quarto-figure-center");
228
div.classList.remove("quarto-figure-right");
229
div.classList.add(`quarto-figure-${align}`);
230
}
231
},
232
};
233
234
// deno-lint-ignore no-window-prefix
235
window.addEventListener(
236
"load",
237
async function () {
238
let i = 0;
239
// we need pre because of whitespace preservation
240
for (const el of Array.from(document.querySelectorAll("pre.mermaid-js"))) {
241
// &nbsp; doesn't appear to be treated as whitespace by mermaid
242
// so we replace it with a space.
243
const text = el.textContent.replaceAll("&nbsp;", " ");
244
const { svg: output } = await mermaid.mermaidAPI.render(
245
`mermaid-${++i}`,
246
text,
247
el
248
);
249
el.innerHTML = output;
250
if (el.dataset.label) {
251
// patch mermaid's emitted style
252
const svg = el.firstChild;
253
const style = svg.querySelector("style");
254
style.innerHTML = style.innerHTML.replaceAll(
255
`#${svg.id}`,
256
`#${el.dataset.label}-mermaid`
257
);
258
svg.id = el.dataset.label + "-mermaid";
259
delete el.dataset.label;
260
}
261
262
const svg = el.querySelector("svg");
263
const parent = el.parentElement;
264
parent.removeChild(el);
265
parent.appendChild(svg);
266
svg.classList.add("mermaid-js");
267
}
268
for (const svgEl of Array.from(
269
document.querySelectorAll("svg.mermaid-js")
270
)) {
271
_quartoMermaid.postProcess(svgEl);
272
}
273
},
274
false
275
);
276
277