import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts";
import { baseHandler, install } from "./base.ts";
import { resourcePath } from "../resources.ts";
import { join, toFileUrl } from "../../deno_ral/path.ts";
import {
isIpynbOutput,
isJavascriptCompatible,
isLatexOutput,
isRevealjsOutput,
isTypstOutput,
} from "../../config/format.ts";
import { QuartoMdCell } from "../lib/break-quarto-md.ts";
import { mappedConcat, mappedIndexToLineCol } from "../lib/mapped-text.ts";
import { lineOffsets } from "../lib/text.ts";
import {
kFigAlign,
kFigHeight,
kFigResponsive,
kFigWidth,
kIpynbProduceSourceNotebook,
} from "../../config/constants.ts";
import {
fixupAlignment,
makeResponsive,
resolveSize,
setSvgSize,
} from "../svg.ts";
import { Element, parseHtml } from "../deno-dom.ts";
const dotHandler: LanguageHandler = {
...baseHandler,
type: "cell",
stage: "post-engine",
languageName: "dot",
defaultOptions: {
echo: false,
eval: true,
include: true,
"graph-layout": "dot",
},
comment: "//",
async cell(
handlerContext: LanguageCellHandlerContext,
cell: QuartoMdCell,
options: Record<string, unknown>,
) {
const cellContent = handlerContext.cellContent(cell);
const graphvizModule = await import(
toFileUrl(resourcePath(join("js", "graphviz-wasm.js"))).href
);
let svg;
const oldConsoleLog = console.log;
const oldConsoleWarn = console.warn;
console.log = () => {};
console.warn = () => {};
try {
svg = await graphvizModule.graphviz().layout(
cellContent.value,
"svg",
options["graph-layout"],
);
console.log = oldConsoleLog;
console.warn = oldConsoleWarn;
} catch (e) {
if (!(e instanceof Error)) throw e;
console.log = oldConsoleLog;
console.warn = oldConsoleWarn;
const m = (e.message as string).match(
/(.*)syntax error in line (\d+)(.*)/,
);
if (m) {
const number = Number(m[2]) - 1;
const locF = mappedIndexToLineCol(cellContent);
const offsets = Array.from(lineOffsets(cellContent.value));
const offset = offsets[number];
const mapResult = cellContent.map(offset, true);
const { line } = locF(offset);
e.message = (e.message as string).replace(
m[0],
`${m[1]}syntax error in file ${
mapResult!.originalString.fileName
}, line ${line + 1}${m[3]}`,
);
throw e;
} else {
throw e;
}
}
const makeFigLink = (
sourceName: string,
width?: number,
height?: number,
includeCaption?: boolean,
) => {
const figEnvSpecifier =
isLatexOutput(handlerContext.options.format.pandoc)
? ` fig-env='${cell.options?.["fig-env"] || "figure"}'`
: "";
const heightOffset = isTypstOutput(handlerContext.options.format.pandoc)
? 0.1
: 0.0;
let posSpecifier = "";
if (
isLatexOutput(handlerContext.options.format.pandoc) &&
cell.options?.["fig-pos"] !== false
) {
const v = Array.isArray(cell.options?.["fig-pos"])
? cell.options?.["fig-pos"].join("")
: cell.options?.["fig-pos"];
posSpecifier = ` fig-pos='${v || "H"}'`;
}
const idSpecifier = (cell.options?.label && includeCaption)
? ` #${cell.options?.label}`
: "";
const widthSpecifier = width
? `width="${Math.round(width * 100) / 100}in"`
: "";
const heightSpecifier = height
? ` height="${(Math.round(height * 100) / 100) + heightOffset}in"`
: "";
const captionSpecifier = includeCaption
? (cell.options?.["fig-cap"] || "")
: "";
return `\n{${widthSpecifier}${heightSpecifier}${posSpecifier}${figEnvSpecifier}${idSpecifier}}\n`;
};
const fixupRevealAlignment = (svg: Element) => {
if (isRevealjsOutput(handlerContext.options.context.format.pandoc)) {
const align = (options?.[kFigAlign] as string) ?? "center";
fixupAlignment(svg, align);
}
};
if (
isJavascriptCompatible(handlerContext.options.format) &&
!isIpynbOutput(handlerContext.options.format.pandoc)
) {
const responsive = options?.[kFigResponsive] ??
handlerContext.options.context.format.metadata
?.[kFigResponsive];
svg = (await parseHtml(svg)).querySelector("svg")!.outerHTML;
if (
responsive && options[kFigWidth] === undefined &&
options[kFigHeight] === undefined
) {
svg = await makeResponsive(svg, fixupRevealAlignment);
} else {
svg = await setSvgSize(svg, options, fixupRevealAlignment);
}
svg = mappedConcat(["```{=html}\n", svg, "\n```\n"]);
return this.build(handlerContext, cell, svg, options);
} else {
const {
filenames: [sourceName],
} = await handlerContext.createPngsFromHtml({
prefix: "dot-figure-",
selector: "svg",
count: 1,
deviceScaleFactor: Number(options.deviceScaleFactor) || 4,
html: `<!DOCTYPE html><html><body>${svg}</body></html>`,
});
const {
widthInInches,
heightInInches,
} = await resolveSize(svg, options);
const isIpynbSourceOutput =
isIpynbOutput(handlerContext.options.context.format.pandoc) &&
handlerContext.options.context.format
.render[kIpynbProduceSourceNotebook];
if (isIpynbSourceOutput) {
const figLink = makeFigLink(
sourceName,
widthInInches,
heightInInches,
true,
);
return mappedConcat(["\n:::{.cell .markdown}", figLink, ":::\n"]);
} else {
return this.build(
handlerContext,
cell,
mappedConcat([
makeFigLink(
sourceName,
widthInInches,
heightInInches,
),
]),
options,
);
}
}
},
};
install(dotHandler);