Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/preview/preview-text.ts
6433 views
1
/*
2
* preview-text.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
kBaseFormat,
9
kPreviewMode,
10
kPreviewModeRaw,
11
} from "../config/constants.ts";
12
import { isJatsOutput } from "../config/format.ts";
13
import { Format } from "../config/types.ts";
14
import { FileResponse } from "../core/http-types.ts";
15
import { kTextXml } from "../core/mime.ts";
16
import { execProcess } from "../core/process.ts";
17
import {
18
formatResourcePath,
19
pandocBinaryPath,
20
textHighlightThemePath,
21
} from "../core/resources.ts";
22
import { safeRemoveSync } from "../deno_ral/fs.ts";
23
24
import { basename, extname, join } from "../deno_ral/path.ts";
25
26
export const jatsStaticResources = () => {
27
return [
28
{
29
name: "quarto-jats-preview.css",
30
dir: "jats",
31
contentType: "text/css",
32
},
33
{
34
name: "quarto-jats-html.xsl",
35
dir: "jats",
36
contentType: "text/xsl",
37
injectClient: (contents: string, client: string) => {
38
const protectedClient = client.replaceAll(
39
/(<style.*?>)|(<script.*?>)/g,
40
(substring: string) => {
41
return `${substring}\n<![CDATA[`;
42
},
43
).replaceAll(
44
/(<\/style.*?>)|(<\/script.*?>)/g,
45
(substring: string) => {
46
return `]]>\n${substring}`;
47
},
48
).replaceAll("data-micromodal-close", 'data-micromodal-close="true"');
49
50
const bodyContents = contents.replace(
51
"<!-- quarto-after-body -->",
52
protectedClient,
53
);
54
return new TextEncoder().encode(bodyContents);
55
},
56
},
57
];
58
};
59
60
export async function previewTextContent(
61
file: string,
62
inputFile: string,
63
format: Format,
64
req: Request,
65
injectClient: (
66
req: Request,
67
file: Uint8Array,
68
inputFile?: string,
69
contentType?: string,
70
) => FileResponse,
71
) {
72
const rawPreviewMode = format.metadata[kPreviewMode] === kPreviewModeRaw;
73
if (!rawPreviewMode && isJatsOutput(format.pandoc)) {
74
const xml = await jatsPreviewXml(file, req);
75
return {
76
contentType: kTextXml,
77
body: new TextEncoder().encode(xml),
78
};
79
} else if (
80
!rawPreviewMode && format.identifier[kBaseFormat] === "gfm"
81
) {
82
const html = await gfmPreview(file, req);
83
return injectClient(
84
req,
85
new TextEncoder().encode(html),
86
inputFile,
87
);
88
} else {
89
const html = await textPreviewHtml(file, req);
90
const fileContents = new TextEncoder().encode(html);
91
return injectClient(req, fileContents, inputFile);
92
}
93
}
94
95
export async function jatsPreviewXml(file: string, _request: Request) {
96
const fileContents = await Deno.readTextFile(file);
97
98
// Attach the stylesheet
99
let xmlContents = fileContents.replace(
100
/<\?xml version="1.0" encoding="utf-8"\s*\?>/,
101
'<?xml version="1.0" encoding="utf-8" ?>\n<?xml-stylesheet href="quarto-jats-html.xsl" type="text/xsl" ?>',
102
);
103
104
// Strip the DTD to disable the fetching of the DTD and validation (for preview)
105
xmlContents = xmlContents.replace(
106
/<!DOCTYPE((.|\n)*?)>/,
107
"",
108
);
109
return xmlContents;
110
}
111
function darkHighlightStyle(request: Request) {
112
const kQuartoPreviewThemeCategory = "quartoPreviewThemeCategory";
113
const themeCategory = new URL(request.url).searchParams.get(
114
kQuartoPreviewThemeCategory,
115
);
116
return themeCategory && themeCategory !== "light";
117
}
118
119
// run pandoc and its syntax highlighter over the passed file
120
// (use the file's extension as its language)
121
async function textPreviewHtml(file: string, req: Request) {
122
// see if we are in dark mode
123
const darkMode = darkHighlightStyle(req);
124
const backgroundColor = darkMode ? "rgb(30,30,30)" : "#FFFFFF";
125
126
// generate the markdown
127
const frontMatter = ["---"];
128
frontMatter.push(`pagetitle: "Quarto Preview"`);
129
frontMatter.push(`document-css: false`);
130
frontMatter.push("---");
131
132
const styles = [
133
"```{=html}",
134
`<style type="text/css">`,
135
`body { margin: 8px 12px; background-color: ${backgroundColor} }`,
136
`div.sourceCode { background-color: transparent; }`,
137
`</style>`,
138
"```",
139
];
140
141
const lang = (extname(file) || ".default").slice(1).toLowerCase();
142
const kFence = "````````````````";
143
const markdown = frontMatter.join("\n") + "\n\n" +
144
styles.join("\n") + "\n\n" +
145
kFence + lang + "\n" +
146
Deno.readTextFileSync(file) + "\n" +
147
kFence;
148
149
// build the pandoc command (we'll feed it the input on stdin)
150
const cmd = [pandocBinaryPath()];
151
cmd.push("--to", "html");
152
cmd.push(
153
"--highlight-style",
154
textHighlightThemePath("atom-one", darkMode ? "dark" : "light")!,
155
);
156
cmd.push("--standalone");
157
const result = await execProcess({
158
cmd: cmd[0],
159
args: cmd.slice(1),
160
stdout: "piped",
161
}, markdown);
162
if (result.success) {
163
return result.stdout;
164
} else {
165
throw new Error();
166
}
167
}
168
169
async function gfmPreview(file: string, request: Request) {
170
const workingDir = Deno.makeTempDirSync();
171
try {
172
// dark mode?
173
const darkMode = darkHighlightStyle(request);
174
175
// Use a custom template that simplifies things
176
const template = formatResourcePath("gfm", "template.html");
177
178
// Add a filter
179
const filter = formatResourcePath("gfm", "mermaid.lua");
180
181
// Inject Mermaid files
182
const mermaidJs = formatResourcePath(
183
"html",
184
join("mermaid", "mermaid.min.js"),
185
);
186
187
// Files to be included verbatim in head
188
const includeInHeader: string[] = [];
189
190
// Add JS files
191
for (const path of [mermaidJs]) {
192
const js = Deno.readTextFileSync(path);
193
const contents = `<script type="text/javascript">\n${js}\n</script>`;
194
const target = join(workingDir, basename(path));
195
Deno.writeTextFileSync(target, contents);
196
includeInHeader.push(target);
197
}
198
199
// JS init
200
const jsInit = `
201
<script>
202
mermaid.initialize({startOnLoad:true, theme: '${
203
darkMode ? "dark" : "default"
204
}'});
205
</script>`;
206
207
// Inject custom HTML into the header
208
const css = formatResourcePath(
209
"gfm",
210
join(
211
"github-markdown-css",
212
darkMode ? "github-markdown-dark.css" : "github-markdown-light.css",
213
),
214
);
215
const cssTempFile = join(workingDir, "github.css");
216
const cssContents = `<style>\n${
217
Deno.readTextFileSync(css)
218
}\n</style>\n${jsInit}`;
219
Deno.writeTextFileSync(cssTempFile, cssContents);
220
includeInHeader.push(cssTempFile);
221
222
// Inject GFM style code cell theming
223
const highlightPath = textHighlightThemePath(
224
"github",
225
darkMode ? "dark" : "light",
226
);
227
228
const cmd = [pandocBinaryPath()];
229
cmd.push("-f");
230
cmd.push("gfm");
231
cmd.push("-t");
232
cmd.push("html");
233
cmd.push("--template");
234
cmd.push(template);
235
includeInHeader.forEach((include) => {
236
cmd.push("--include-in-header");
237
cmd.push(include);
238
});
239
cmd.push("--lua-filter");
240
cmd.push(filter);
241
if (highlightPath) {
242
cmd.push("--highlight-style");
243
cmd.push(highlightPath);
244
}
245
// Github renders math with MathJax now, so our preview mode does the same
246
cmd.push("--mathjax");
247
const result = await execProcess(
248
{ cmd: cmd[0], args: cmd.slice(1), stdout: "piped", stderr: "piped" },
249
Deno.readTextFileSync(file),
250
);
251
if (result.success) {
252
return result.stdout;
253
} else {
254
throw new Error(
255
`Failed to render citation: error code ${result.code}\n${result.stderr}`,
256
);
257
}
258
} finally {
259
safeRemoveSync(workingDir, { recursive: true });
260
}
261
}
262
263