Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/handlers/dot.ts
3583 views
1
/*
2
* graphviz.ts
3
*
4
* Copyright (C) 2022 Posit Software, PBC
5
*/
6
7
import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts";
8
import { baseHandler, install } from "./base.ts";
9
import { resourcePath } from "../resources.ts";
10
import { join, toFileUrl } from "../../deno_ral/path.ts";
11
import {
12
isIpynbOutput,
13
isJavascriptCompatible,
14
isLatexOutput,
15
isRevealjsOutput,
16
isTypstOutput,
17
} from "../../config/format.ts";
18
import { QuartoMdCell } from "../lib/break-quarto-md.ts";
19
import { mappedConcat, mappedIndexToLineCol } from "../lib/mapped-text.ts";
20
21
import { lineOffsets } from "../lib/text.ts";
22
import {
23
kFigAlign,
24
kFigHeight,
25
kFigResponsive,
26
kFigWidth,
27
kIpynbProduceSourceNotebook,
28
} from "../../config/constants.ts";
29
import {
30
fixupAlignment,
31
makeResponsive,
32
resolveSize,
33
setSvgSize,
34
} from "../svg.ts";
35
import { Element, parseHtml } from "../deno-dom.ts";
36
37
const dotHandler: LanguageHandler = {
38
...baseHandler,
39
40
type: "cell",
41
stage: "post-engine",
42
43
languageName: "dot",
44
45
defaultOptions: {
46
echo: false,
47
eval: true,
48
include: true,
49
"graph-layout": "dot",
50
},
51
52
comment: "//",
53
54
async cell(
55
handlerContext: LanguageCellHandlerContext,
56
cell: QuartoMdCell,
57
options: Record<string, unknown>,
58
) {
59
const cellContent = handlerContext.cellContent(cell);
60
61
const graphvizModule = await import(
62
toFileUrl(resourcePath(join("js", "graphviz-wasm.js"))).href
63
);
64
let svg;
65
const oldConsoleLog = console.log;
66
const oldConsoleWarn = console.warn;
67
console.log = () => {};
68
console.warn = () => {};
69
try {
70
svg = await graphvizModule.graphviz().layout(
71
cellContent.value,
72
"svg",
73
options["graph-layout"],
74
);
75
console.log = oldConsoleLog;
76
console.warn = oldConsoleWarn;
77
} catch (e) {
78
if (!(e instanceof Error)) throw e;
79
console.log = oldConsoleLog;
80
console.warn = oldConsoleWarn;
81
const m = (e.message as string).match(
82
/(.*)syntax error in line (\d+)(.*)/,
83
);
84
if (m) {
85
const number = Number(m[2]) - 1;
86
const locF = mappedIndexToLineCol(cellContent);
87
const offsets = Array.from(lineOffsets(cellContent.value));
88
const offset = offsets[number];
89
const mapResult = cellContent.map(offset, true);
90
const { line } = locF(offset);
91
e.message = (e.message as string).replace(
92
m[0],
93
`${m[1]}syntax error in file ${
94
mapResult!.originalString.fileName
95
}, line ${line + 1}${m[3]}`,
96
);
97
throw e;
98
} else {
99
throw e;
100
}
101
}
102
103
const makeFigLink = (
104
sourceName: string,
105
width?: number,
106
height?: number,
107
includeCaption?: boolean,
108
) => {
109
const figEnvSpecifier =
110
isLatexOutput(handlerContext.options.format.pandoc)
111
? ` fig-env='${cell.options?.["fig-env"] || "figure"}'`
112
: "";
113
const heightOffset = isTypstOutput(handlerContext.options.format.pandoc)
114
? 0.1
115
: 0.0;
116
let posSpecifier = "";
117
if (
118
isLatexOutput(handlerContext.options.format.pandoc) &&
119
cell.options?.["fig-pos"] !== false
120
) {
121
const v = Array.isArray(cell.options?.["fig-pos"])
122
? cell.options?.["fig-pos"].join("")
123
: cell.options?.["fig-pos"];
124
posSpecifier = ` fig-pos='${v || "H"}'`;
125
}
126
const idSpecifier = (cell.options?.label && includeCaption)
127
? ` #${cell.options?.label}`
128
: "";
129
const widthSpecifier = width
130
? `width="${Math.round(width * 100) / 100}in"`
131
: "";
132
const heightSpecifier = height
133
? ` height="${(Math.round(height * 100) / 100) + heightOffset}in"`
134
: "";
135
const captionSpecifier = includeCaption
136
? (cell.options?.["fig-cap"] || "")
137
: "";
138
139
return `\n![${captionSpecifier}](${sourceName}){${widthSpecifier}${heightSpecifier}${posSpecifier}${figEnvSpecifier}${idSpecifier}}\n`;
140
};
141
142
const fixupRevealAlignment = (svg: Element) => {
143
if (isRevealjsOutput(handlerContext.options.context.format.pandoc)) {
144
const align = (options?.[kFigAlign] as string) ?? "center";
145
fixupAlignment(svg, align);
146
}
147
};
148
149
if (
150
isJavascriptCompatible(handlerContext.options.format) &&
151
!isIpynbOutput(handlerContext.options.format.pandoc)
152
) {
153
const responsive = options?.[kFigResponsive] ??
154
handlerContext.options.context.format.metadata
155
?.[kFigResponsive];
156
157
svg = (await parseHtml(svg)).querySelector("svg")!.outerHTML;
158
if (
159
responsive && options[kFigWidth] === undefined &&
160
options[kFigHeight] === undefined
161
) {
162
svg = await makeResponsive(svg, fixupRevealAlignment);
163
} else {
164
svg = await setSvgSize(svg, options, fixupRevealAlignment);
165
}
166
167
svg = mappedConcat(["```{=html}\n", svg, "\n```\n"]);
168
return this.build(handlerContext, cell, svg, options);
169
} else {
170
const {
171
filenames: [sourceName],
172
} = await handlerContext.createPngsFromHtml({
173
prefix: "dot-figure-",
174
selector: "svg",
175
count: 1,
176
deviceScaleFactor: Number(options.deviceScaleFactor) || 4,
177
html: `<!DOCTYPE html><html><body>${svg}</body></html>`,
178
});
179
180
const {
181
widthInInches,
182
heightInInches,
183
} = await resolveSize(svg, options);
184
185
const isIpynbSourceOutput =
186
isIpynbOutput(handlerContext.options.context.format.pandoc) &&
187
handlerContext.options.context.format
188
.render[kIpynbProduceSourceNotebook];
189
190
if (isIpynbSourceOutput) {
191
// If we're producing a source notebook, we know that we're just
192
// producing an image, so present the output as an image in
193
// a markdown cell (and allow the figure attributes through)
194
const figLink = makeFigLink(
195
sourceName,
196
widthInInches,
197
heightInInches,
198
true,
199
);
200
return mappedConcat(["\n:::{.cell .markdown}", figLink, ":::\n"]);
201
} else {
202
return this.build(
203
handlerContext,
204
cell,
205
mappedConcat([
206
makeFigLink(
207
sourceName,
208
widthInInches,
209
heightInInches,
210
),
211
// `\n![](${sourceName}){width="${widthInInches}in" height="${heightInInches}in" fig-pos='H'}\n`,
212
]),
213
options,
214
);
215
}
216
}
217
},
218
};
219
220
install(dotHandler);
221
222