import {
kBaseFormat,
kPreviewMode,
kPreviewModeRaw,
} from "../config/constants.ts";
import { isJatsOutput } from "../config/format.ts";
import { Format } from "../config/types.ts";
import { FileResponse } from "../core/http-types.ts";
import { kTextXml } from "../core/mime.ts";
import { execProcess } from "../core/process.ts";
import {
formatResourcePath,
pandocBinaryPath,
textHighlightThemePath,
} from "../core/resources.ts";
import { safeRemoveSync } from "../deno_ral/fs.ts";
import { basename, extname, join } from "../deno_ral/path.ts";
export const jatsStaticResources = () => {
return [
{
name: "quarto-jats-preview.css",
dir: "jats",
contentType: "text/css",
},
{
name: "quarto-jats-html.xsl",
dir: "jats",
contentType: "text/xsl",
injectClient: (contents: string, client: string) => {
const protectedClient = client.replaceAll(
/(<style.*?>)|(<script.*?>)/g,
(substring: string) => {
return `${substring}\n<![CDATA[`;
},
).replaceAll(
/(<\/style.*?>)|(<\/script.*?>)/g,
(substring: string) => {
return `]]>\n${substring}`;
},
).replaceAll("data-micromodal-close", 'data-micromodal-close="true"');
const bodyContents = contents.replace(
"<!-- quarto-after-body -->",
protectedClient,
);
return new TextEncoder().encode(bodyContents);
},
},
];
};
export async function previewTextContent(
file: string,
inputFile: string,
format: Format,
req: Request,
injectClient: (
req: Request,
file: Uint8Array,
inputFile?: string,
contentType?: string,
) => FileResponse,
) {
const rawPreviewMode = format.metadata[kPreviewMode] === kPreviewModeRaw;
if (!rawPreviewMode && isJatsOutput(format.pandoc)) {
const xml = await jatsPreviewXml(file, req);
return {
contentType: kTextXml,
body: new TextEncoder().encode(xml),
};
} else if (
!rawPreviewMode && format.identifier[kBaseFormat] === "gfm"
) {
const html = await gfmPreview(file, req);
return injectClient(
req,
new TextEncoder().encode(html),
inputFile,
);
} else {
const html = await textPreviewHtml(file, req);
const fileContents = new TextEncoder().encode(html);
return injectClient(req, fileContents, inputFile);
}
}
export async function jatsPreviewXml(file: string, _request: Request) {
const fileContents = await Deno.readTextFile(file);
let xmlContents = fileContents.replace(
/<\?xml version="1.0" encoding="utf-8"\s*\?>/,
'<?xml version="1.0" encoding="utf-8" ?>\n<?xml-stylesheet href="quarto-jats-html.xsl" type="text/xsl" ?>',
);
xmlContents = xmlContents.replace(
/<!DOCTYPE((.|\n)*?)>/,
"",
);
return xmlContents;
}
function darkHighlightStyle(request: Request) {
const kQuartoPreviewThemeCategory = "quartoPreviewThemeCategory";
const themeCategory = new URL(request.url).searchParams.get(
kQuartoPreviewThemeCategory,
);
return themeCategory && themeCategory !== "light";
}
async function textPreviewHtml(file: string, req: Request) {
const darkMode = darkHighlightStyle(req);
const backgroundColor = darkMode ? "rgb(30,30,30)" : "#FFFFFF";
const frontMatter = ["---"];
frontMatter.push(`pagetitle: "Quarto Preview"`);
frontMatter.push(`document-css: false`);
frontMatter.push("---");
const styles = [
"```{=html}",
`<style type="text/css">`,
`body { margin: 8px 12px; background-color: ${backgroundColor} }`,
`div.sourceCode { background-color: transparent; }`,
`</style>`,
"```",
];
const lang = (extname(file) || ".default").slice(1).toLowerCase();
const kFence = "````````````````";
const markdown = frontMatter.join("\n") + "\n\n" +
styles.join("\n") + "\n\n" +
kFence + lang + "\n" +
Deno.readTextFileSync(file) + "\n" +
kFence;
const cmd = [pandocBinaryPath()];
cmd.push("--to", "html");
cmd.push(
"--highlight-style",
textHighlightThemePath("atom-one", darkMode ? "dark" : "light")!,
);
cmd.push("--standalone");
const result = await execProcess({
cmd: cmd[0],
args: cmd.slice(1),
stdout: "piped",
}, markdown);
if (result.success) {
return result.stdout;
} else {
throw new Error();
}
}
async function gfmPreview(file: string, request: Request) {
const workingDir = Deno.makeTempDirSync();
try {
const darkMode = darkHighlightStyle(request);
const template = formatResourcePath("gfm", "template.html");
const filter = formatResourcePath("gfm", "mermaid.lua");
const mermaidJs = formatResourcePath(
"html",
join("mermaid", "mermaid.min.js"),
);
const includeInHeader: string[] = [];
for (const path of [mermaidJs]) {
const js = Deno.readTextFileSync(path);
const contents = `<script type="text/javascript">\n${js}\n</script>`;
const target = join(workingDir, basename(path));
Deno.writeTextFileSync(target, contents);
includeInHeader.push(target);
}
const jsInit = `
<script>
mermaid.initialize({startOnLoad:true, theme: '${
darkMode ? "dark" : "default"
}'});
</script>`;
const css = formatResourcePath(
"gfm",
join(
"github-markdown-css",
darkMode ? "github-markdown-dark.css" : "github-markdown-light.css",
),
);
const cssTempFile = join(workingDir, "github.css");
const cssContents = `<style>\n${
Deno.readTextFileSync(css)
}\n</style>\n${jsInit}`;
Deno.writeTextFileSync(cssTempFile, cssContents);
includeInHeader.push(cssTempFile);
const highlightPath = textHighlightThemePath(
"github",
darkMode ? "dark" : "light",
);
const cmd = [pandocBinaryPath()];
cmd.push("-f");
cmd.push("gfm");
cmd.push("-t");
cmd.push("html");
cmd.push("--template");
cmd.push(template);
includeInHeader.forEach((include) => {
cmd.push("--include-in-header");
cmd.push(include);
});
cmd.push("--lua-filter");
cmd.push(filter);
if (highlightPath) {
cmd.push("--highlight-style");
cmd.push(highlightPath);
}
cmd.push("--mathjax");
const result = await execProcess(
{ cmd: cmd[0], args: cmd.slice(1), stdout: "piped", stderr: "piped" },
Deno.readTextFileSync(file),
);
if (result.success) {
return result.stdout;
} else {
throw new Error(
`Failed to render citation: error code ${result.code}\n${result.stderr}`,
);
}
} finally {
safeRemoveSync(workingDir, { recursive: true });
}
}