Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/execute/ojs/compile.ts
6458 views
1
/*
2
* compile.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import * as ld from "../../core/lodash.ts";
8
import { dirname, join, relative, resolve } from "../../deno_ral/path.ts";
9
import { warning } from "../../deno_ral/log.ts";
10
11
import { parseModule } from "observablehq/parser";
12
import { escape } from "../../core/lodash.ts";
13
14
import { Format, kDependencies } from "../../config/types.ts";
15
import { MappedExecuteResult, PandocIncludes } from "../../execute/types.ts";
16
import {
17
kCodeSummary,
18
kEmbedResources,
19
kIncludeAfterBody,
20
kIncludeInHeader,
21
kSelfContained,
22
} from "../../config/constants.ts";
23
import { RenderContext } from "../../command/render/types.ts";
24
import { ProjectContext } from "../../project/types.ts";
25
26
import {
27
isJavascriptCompatible,
28
isMarkdownOutput,
29
} from "../../config/format.ts";
30
31
import { resolveDependencies } from "../../command/render/pandoc-dependencies-html.ts";
32
import {
33
extractResourceDescriptionsFromOJSChunk,
34
makeSelfContainedResources,
35
ResourceDescription,
36
uniqueResources,
37
} from "./extract-resources.ts";
38
import { ojsParseError } from "./errors.ts";
39
40
import { ojsSimpleWalker } from "./ojs-tools.ts";
41
42
import {
43
kCellFigCap,
44
kCellFigSubCap,
45
kCellLstCap,
46
kCellLstLabel,
47
kCodeFold,
48
kCodeLineNumbers,
49
kCodeOverflow,
50
kEcho,
51
kError,
52
kEval,
53
kInclude,
54
kLayoutNcol,
55
kLayoutNrow,
56
kOutput,
57
} from "../../config/constants.ts";
58
59
import { asHtmlId } from "../../core/html.ts";
60
import { TempContext } from "../../core/temp.ts";
61
import { quartoConfig } from "../../core/quarto.ts";
62
import { mergeConfigs } from "../../core/config.ts";
63
import { formatResourcePath } from "../../core/resources.ts";
64
import { logError } from "../../core/log.ts";
65
import { breakQuartoMd, QuartoMdCell } from "../../core/lib/break-quarto-md.ts";
66
67
import { MappedString } from "../../core/mapped-text.ts";
68
import { languagesInMarkdown } from "../../core/pandoc/pandoc-partition.ts";
69
70
import {
71
pandocBlock,
72
pandocCode,
73
pandocDiv,
74
pandocRawStr,
75
} from "../../core/pandoc/codegen.ts";
76
77
import {
78
EitherString,
79
join as mappedJoin,
80
mappedTrim,
81
} from "../../core/lib/mapped-text.ts";
82
import { getDivAttributes } from "../../core/handlers/base.ts";
83
import { pathWithForwardSlashes } from "../../core/path.ts";
84
import { executeInlineCodeHandlerMapped } from "../../core/execute-inline.ts";
85
86
import { encodeBase64 } from "encoding/base64";
87
88
export interface OjsCompileOptions {
89
source: string;
90
format: Format;
91
markdown: MappedString;
92
libDir: string;
93
temp: TempContext;
94
project: ProjectContext;
95
ojsBlockLineNumbers: number[];
96
}
97
98
export interface OjsCompileResult {
99
markdown: MappedString;
100
filters?: string[];
101
includes?: PandocIncludes;
102
resourceFiles?: string[];
103
}
104
105
interface SubfigureSpec {
106
caption?: string;
107
}
108
109
const ojsHasOutputs = (parse: any) => {
110
let hasOutputs = false;
111
ojsSimpleWalker(parse, {
112
Cell(node: any) {
113
if (node.id === null) {
114
hasOutputs = true;
115
}
116
},
117
});
118
return hasOutputs;
119
};
120
121
// TODO decide how source code is presented, we've lost this
122
// feature from the ojs-engine move
123
export async function ojsCompile(
124
options: OjsCompileOptions,
125
): Promise<OjsCompileResult> {
126
const { markdown, project, ojsBlockLineNumbers } = options;
127
128
const output = await breakQuartoMd(markdown, true);
129
130
// This is a pretty crude check, but
131
// we can't actually make good use breakQuartoMd's output here
132
// because the {r} and {python} cells have already been
133
// executed by the engine.
134
const hasOjsDefines = markdown.value.indexOf("ojs_define") !== -1;
135
136
// if ojs-engine is explicitly `false` or it's an unsupported format,
137
// do nothing
138
if (
139
!isJavascriptCompatible(options.format) ||
140
options.format.metadata?.["ojs-engine"] === false
141
) {
142
return { markdown: markdown };
143
}
144
145
const languages = languagesInMarkdown(markdown.value);
146
// if ojs-engine is not explicitly `true` and we couldn't detect an ojs cell,
147
// do nothing
148
if (
149
(options.format.metadata?.["ojs-engine"] !== true) &&
150
!languages.has("ojs") &&
151
!hasOjsDefines
152
) {
153
return { markdown: markdown };
154
}
155
156
const projDir = project.isSingleFile ? undefined : project.dir;
157
const selfContained = options.format.pandoc?.[kSelfContained] ??
158
options.format.pandoc?.[kEmbedResources] ?? false;
159
const isHtmlMarkdown = isMarkdownOutput(options.format, [
160
"gfm",
161
"commonmark",
162
]);
163
164
let ojsCellID = 0;
165
let ojsBlockIndex = 0; // this is different from ojsCellID because of inline cells.
166
const userIds: Set<string> = new Set();
167
168
const scriptContents: string[] = [];
169
170
const ojsRuntimeDir = resolve(
171
dirname(options.source),
172
options.libDir + "/ojs",
173
);
174
const docDir = dirname(options.source);
175
const rootDir = projDir ?? "./";
176
const runtimeToDoc = pathWithForwardSlashes(relative(ojsRuntimeDir, docDir));
177
const runtimeToRoot = pathWithForwardSlashes(
178
relative(ojsRuntimeDir, rootDir),
179
);
180
const docToRoot = pathWithForwardSlashes(relative(docDir, rootDir));
181
// the check for file:// protocol has to be done in an inline script because
182
// script links are not loaded in file:// protocol cases
183
scriptContents.push(
184
`if (window.location.protocol === "file:") { alert("The OJS runtime does not work with file:// URLs. Please use a web server to view this document."); }`,
185
);
186
scriptContents.push(`window._ojs.paths.runtimeToDoc = "${runtimeToDoc}";`);
187
scriptContents.push(`window._ojs.paths.runtimeToRoot = "${runtimeToRoot}";`);
188
scriptContents.push(`window._ojs.paths.docToRoot = "${docToRoot}";`);
189
scriptContents.push(
190
`window._ojs.selfContained = ${selfContained};`,
191
);
192
193
interface ModuleCell {
194
methodName: string;
195
cellName?: string;
196
inline?: boolean;
197
source: string; // FIXME we want this to be the serialization output of a MappedString;
198
}
199
const moduleContents: ModuleCell[] = [];
200
201
function interpret(jsSrc: MappedString, inline: boolean, lenient: boolean) {
202
const inlineStr = inline ? "inline-" : "";
203
const methodName = lenient ? "interpretLenient" : "interpret";
204
moduleContents.push({
205
methodName,
206
cellName: `ojs-${inlineStr}cell-${ojsCellID}`,
207
inline,
208
209
// FIXME This here is the big problem now. We'd like to send
210
// jsSrc as is,
211
//
212
// but moduleContents needs to be JSON-serializable in order for
213
// the runtime to interpret it. But that gives problems with
214
// respect to our ability to compute offsets etc.
215
source: jsSrc.value,
216
});
217
}
218
219
const ls: EitherString[] = [];
220
const resourceFiles: string[] = [];
221
const pageResources: ResourceDescription[] = [];
222
const ojsViews = new Set<string>();
223
const ojsIdentifiers = new Set<string>();
224
225
for (const cell of output.cells) {
226
const errorVal =
227
(cell.options?.[kError] ?? options.format.execute?.[kError] ??
228
false) as boolean;
229
const handleOJSCell = async (
230
cell: QuartoMdCell,
231
mdClassList?: string[],
232
) => {
233
const cellSrcStr = cell.source;
234
const bumpOjsCellIdString = () => {
235
ojsCellID += 1;
236
return `ojs-cell-${ojsCellID}`;
237
};
238
const ojsId = bumpOjsCellIdString();
239
const userCellId = () => {
240
const chooseId = (label: string) => {
241
const htmlLabel = asHtmlId(label as string);
242
if (userIds.has(htmlLabel)) {
243
// FIXME explain error better to avoid confusion
244
// that might come up under id canonicalization
245
throw new Error(`FATAL: duplicate label ${htmlLabel}`);
246
} else {
247
userIds.add(htmlLabel);
248
return htmlLabel;
249
}
250
};
251
if (cell.options?.label) {
252
return chooseId(cell.options.label as string);
253
} else if (cell.options?.[kCellLstLabel]) {
254
return chooseId(cell.options[kCellLstLabel] as string);
255
} else if (
256
cell.options?.[kCellFigCap] || cell.options?.[kCellFigSubCap] ||
257
cell.options?.[kCellLstCap]
258
) {
259
return chooseId(`fig-${ojsId}`);
260
} else {
261
return undefined;
262
}
263
};
264
const userId = userCellId();
265
const attrs = [];
266
267
const hasFigureSubCaptions = () => {
268
// FIXME figure out runtime type validation. This should check
269
// if fig-subcap is an array of strings.
270
//
271
// WAITING for YAML schemas + validation
272
return cell.options?.[kCellFigSubCap];
273
};
274
275
interface SourceInfo {
276
start: number;
277
end: number;
278
cellType: string;
279
}
280
interface ParsedCellInfo {
281
info: SourceInfo[];
282
}
283
284
const cellStartingLoc = ojsBlockLineNumbers[ojsBlockIndex++] || 0;
285
if (cellStartingLoc === 0) {
286
warning(
287
"OJS block count mismatch. Line number reporting is likely to be wrong",
288
);
289
}
290
291
// deno-lint-ignore no-explicit-any
292
const handleError = (err: any, cellSrc: MappedString) => {
293
const div = pandocDiv({
294
classes: ["quarto-ojs-syntax-error"],
295
});
296
const msg = String(err).split("\n")[0].trim().replace(
297
/ *\(\d+:\d+\)$/,
298
"",
299
);
300
ojsParseError(err, cellSrc);
301
302
const preDiv = pandocCode({
303
classes: ["numberLines", "java"],
304
attrs: [
305
`startFrom="${cellStartingLoc - 1}"`,
306
`syntax-error-position="${err.pos}"`,
307
`source-offset="${cell.sourceOffset}"`,
308
],
309
});
310
preDiv.push(pandocRawStr(cell.sourceVerbatim.value.trim()));
311
div.push(preDiv);
312
const errMsgDiv = pandocDiv({
313
classes: ["cell-output", "cell-output-error"],
314
});
315
const calloutDiv = pandocDiv({
316
classes: ["callout-important"],
317
});
318
const [_heading, fullMsg] = msg.split(": ");
319
calloutDiv.push(
320
pandocRawStr(
321
`#### OJS Syntax Error (line ${
322
err.loc.line +
323
cellStartingLoc + cell.sourceStartLine -
324
1
325
}, column ${err.loc.column + 1})`,
326
),
327
);
328
calloutDiv.push(pandocRawStr(`${fullMsg}`));
329
errMsgDiv.push(calloutDiv);
330
div.push(errMsgDiv);
331
div.emit(ls);
332
};
333
334
let nCells = 0;
335
const parsedCells: ParsedCellInfo[] = [];
336
let hasOutputs = true;
337
338
try {
339
const parse = parseModule(cellSrcStr.value);
340
hasOutputs = ojsHasOutputs(parse);
341
let info: SourceInfo[] = [];
342
const flushSeqSrc = () => {
343
parsedCells.push({ info });
344
for (let i = 1; i < info.length; ++i) {
345
parsedCells.push({ info: [] });
346
}
347
info = [];
348
};
349
ojsSimpleWalker(parse, {
350
// deno-lint-ignore no-explicit-any
351
Cell(node: any) {
352
if (node.id && node.id.type === "ViewExpression") {
353
ojsViews.add(node.id.id.name);
354
} else if (node.id && node.id.type === "Identifier") {
355
ojsIdentifiers.add(node.id.name);
356
}
357
if (
358
node.id === null &&
359
node.body.type !== "ImportDeclaration"
360
) {
361
info.push({
362
start: node.start,
363
end: node.end,
364
cellType: "expression",
365
});
366
flushSeqSrc();
367
} else {
368
info.push({
369
start: node.start,
370
end: node.end,
371
cellType: "declaration",
372
});
373
}
374
},
375
});
376
nCells = parse.cells.length;
377
if (info.length > 0) {
378
flushSeqSrc();
379
}
380
} catch (e) {
381
if (e instanceof SyntaxError) {
382
handleError(e, cellSrcStr);
383
return;
384
} else {
385
logError(e);
386
throw new Error();
387
}
388
}
389
390
pageResources.push(
391
...(await extractResourceDescriptionsFromOJSChunk(
392
cellSrcStr,
393
dirname(options.source),
394
projDir,
395
)),
396
);
397
398
const hasManyRowsCols = () => {
399
// FIXME figure out runtime type validation. This should check
400
// if ncol and nrow are positive integers
401
//
402
// WAITING for YAML schemas + validation
403
const cols = cell.options?.[kLayoutNcol];
404
const rows = cell.options?.[kLayoutNrow];
405
return (Number(cols) && (Number(cols) > 1)) ||
406
(Number(rows) && (Number(rows) > 1)) ||
407
(nCells > 1);
408
};
409
const nCol = () => {
410
const col = cell.options
411
?.[kLayoutNcol] as (string | number | undefined);
412
if (!col) {
413
return 1;
414
}
415
return Number(col);
416
};
417
const nRow = () => {
418
const row = cell.options
419
?.[kLayoutNrow] as (string | number | undefined);
420
if (!row) {
421
return Math.ceil(nCells / nCol());
422
}
423
return Number(row);
424
};
425
const hasSubFigures = () => {
426
return hasFigureSubCaptions() ||
427
(hasManyRowsCols() && ((nRow() * nCol()) > 1));
428
};
429
const idPlacement = () => {
430
if (
431
hasSubFigures() ||
432
cell.options?.[kCellLstLabel]
433
) {
434
return "outer";
435
} else {
436
return "inner";
437
}
438
};
439
440
let outputVal: any = cell.options?.[kOutput] ??
441
options.format.execute[kOutput];
442
// if (
443
// options.format.identifier["base-format"] == "dashboard" &&
444
// !hasOutputs
445
// ) {
446
// outputVal = false;
447
// }
448
outputVal = outputVal ?? true;
449
if (outputVal === "all" || outputVal === "asis") {
450
attrs.push(`output="${outputVal}"`);
451
}
452
const {
453
classes,
454
attrs: otherAttrs,
455
} = getDivAttributes(cell.options || {}); // TODO this import is weird but eventually OJS will be a handler
456
attrs.push(...otherAttrs);
457
458
const evalVal = cell.options?.[kEval] ?? options.format.execute[kEval] ??
459
true;
460
const echoVal = cell.options?.[kEcho] ?? options.format.execute[kEcho] ??
461
true;
462
463
const ojsCellClasses = ["cell"];
464
if (!outputVal) {
465
ojsCellClasses.push("hidden");
466
}
467
468
const div = pandocDiv({
469
id: idPlacement() === "outer" ? userId : undefined,
470
classes: [
471
...ojsCellClasses,
472
...classes,
473
],
474
attrs,
475
});
476
477
const includeVal = cell.options?.[kInclude] ??
478
options.format.execute[kInclude] ?? true;
479
480
const srcClasses = mdClassList ?? ["js", "cell-code"];
481
const srcAttrs = [];
482
483
// the only effect of echoVal in OJS blocks
484
// is to hide the div. We need source always to pinpoint
485
// errors in source in case of runtime errors.
486
//
487
// FIXME This is
488
// potentially wrong in the presence of !includeVal
489
if (!echoVal) {
490
srcClasses.push("hidden");
491
}
492
493
if (cell.options?.[kCodeOverflow] === "wrap") {
494
srcClasses.push("code-overflow-wrap");
495
} else if (cell.options?.[kCodeOverflow] === "scroll") {
496
srcClasses.push("code-overflow-scroll");
497
}
498
499
// options.format.render?.[kCodeFold] appears to use "none"
500
// for "not set", so we interpret "none" as undefined
501
if (
502
asUndefined(options.format.render?.[kCodeFold], "none") ??
503
cell.options?.[kCodeFold]
504
) {
505
srcAttrs.push(`${kCodeFold}="${cell.options?.[kCodeFold]}"`);
506
}
507
508
if (cell.options?.[kCodeSummary]) {
509
srcAttrs.push(
510
`${kCodeSummary}="${escape(cell.options?.[kCodeSummary])}"`,
511
);
512
}
513
514
if (cell.options?.[kCodeLineNumbers]) {
515
srcAttrs.push(
516
`${kCodeLineNumbers}="${cell.options?.[kCodeLineNumbers]}"`,
517
);
518
}
519
520
const srcConfig = {
521
classes: srcClasses.slice(),
522
attrs: srcAttrs.slice(),
523
};
524
525
// only emit interpret if eval is true
526
if (evalVal) {
527
interpret(cell.source, false, errorVal);
528
}
529
530
// handle output of computation
531
const outputCellClasses = ["cell-output", "cell-output-display"];
532
if (!outputVal || !includeVal) {
533
outputCellClasses.push("hidden");
534
}
535
536
if (echoVal === "fenced") {
537
const ourAttrs = srcConfig.attrs.slice();
538
// we replace js with java so that we "fool" pandoc by having it
539
// not recognize triple backticks
540
const ourClasses = srcConfig.classes.filter((d) => d !== "js");
541
ourClasses.push("java");
542
ourAttrs.push(
543
`startFrom="${cellStartingLoc - 1}"`,
544
`source-offset="${cell.sourceOffset}"`,
545
);
546
const srcDiv = pandocCode({
547
classes: ourClasses,
548
attrs: ourAttrs,
549
});
550
srcDiv.push(pandocRawStr(cell.sourceVerbatim.value.trim()));
551
div.push(srcDiv);
552
}
553
554
// if "echo: fenced", then we've already printed the source
555
// and it's neatly localized: don't repeat it
556
//
557
// in addition, if we're emitting html-friendly markdown
558
// (as opposed to "html", which is what we do most of the time),
559
// then pandoc will clobber our classes and our runtime error
560
// reporting things will break anyway. So just don't emit
561
// the source in that case.
562
const shouldEmitSource = echoVal !== "fenced" &&
563
!(echoVal === false && isHtmlMarkdown);
564
565
const makeSubFigures = (specs: SubfigureSpec[]) => {
566
let subfigIx = 1;
567
const cellInfo = ([] as SourceInfo[]).concat(
568
...(parsedCells.map((n) => n.info)),
569
);
570
for (const spec of specs) {
571
const outputDiv = pandocDiv({
572
classes: outputCellClasses,
573
});
574
const outputInnerDiv = pandocDiv({
575
id: userId && `${userId}-${subfigIx}`,
576
});
577
const innerInfo = parsedCells[subfigIx - 1].info;
578
const ojsDiv = pandocDiv({
579
id: `${ojsId}-${subfigIx}`,
580
attrs: [`nodetype="${cellInfo[subfigIx - 1].cellType}"`],
581
});
582
583
if (
584
shouldEmitSource &&
585
innerInfo.length > 0 && srcConfig !== undefined
586
) {
587
const ourAttrs = srcConfig.attrs.slice();
588
// compute offset from cell start to div start
589
const linesSkipped =
590
cellSrcStr.value.substring(0, innerInfo[0].start).split("\n")
591
.length;
592
593
ourAttrs.push(
594
`startFrom="${
595
cellStartingLoc + cell.sourceStartLine - 1 +
596
linesSkipped
597
}"`,
598
);
599
ourAttrs.push(`source-offset="-${innerInfo[0].start}"`);
600
const srcDiv = pandocCode({
601
attrs: ourAttrs,
602
classes: srcConfig.classes,
603
});
604
srcDiv.push(pandocRawStr(
605
cellSrcStr.value.substring(
606
innerInfo[0].start,
607
innerInfo[innerInfo.length - 1].end,
608
).trim(),
609
));
610
div.push(srcDiv);
611
}
612
subfigIx++;
613
outputDiv.push(outputInnerDiv);
614
outputInnerDiv.push(ojsDiv);
615
if (spec.caption) {
616
// FIXME does this also need figcaption?
617
outputInnerDiv.push(pandocRawStr(spec.caption as string));
618
}
619
div.push(outputDiv);
620
}
621
};
622
623
if (!hasFigureSubCaptions() && hasManyRowsCols()) {
624
const cellCount = Math.max(nRow() * nCol(), nCells, 1);
625
const specs = [];
626
for (let i = 0; i < cellCount; ++i) {
627
specs.push({ caption: "" });
628
}
629
makeSubFigures(specs);
630
if (cell.options?.[kCellFigCap]) {
631
div.push(pandocRawStr(cell.options[kCellFigCap] as string));
632
}
633
} else if (hasFigureSubCaptions()) {
634
let subCap = (cell.options?.[kCellFigSubCap]) as string[] | true;
635
if (subCap === true) {
636
subCap = [""];
637
}
638
if (!Array.isArray(subCap)) {
639
subCap = [subCap];
640
}
641
if (
642
hasManyRowsCols() &&
643
(subCap as string[]).length !==
644
(nRow() * nCol())
645
) {
646
throw new Error(
647
"Cannot have subcaptions and multi-row/col layout with mismatched number of cells",
648
);
649
}
650
const specs = (subCap as string[]).map(
651
(caption) => ({ caption }),
652
);
653
makeSubFigures(specs);
654
if (cell.options?.[kCellFigCap]) {
655
div.push(pandocRawStr(cell.options[kCellFigCap] as string));
656
}
657
} else {
658
// FIXME: this should include better file and LOC information!
659
if (parsedCells.length === 0) {
660
throw new Error(
661
`Fatal: OJS cell starting on line ${cellStartingLoc} is empty. OJS cells require at least one declaration.`,
662
);
663
}
664
const innerInfo = parsedCells[0].info;
665
if (innerInfo.length > 0 && srcConfig !== undefined) {
666
const ourAttrs = srcConfig.attrs.slice();
667
// compute offset from cell start to div start
668
ourAttrs.push(
669
`startFrom="${cellStartingLoc + cell.sourceStartLine - 1}"`,
670
);
671
ourAttrs.push(`source-offset="0"`);
672
if (shouldEmitSource) {
673
const srcDiv = pandocCode({
674
attrs: ourAttrs,
675
classes: srcConfig.classes,
676
});
677
srcDiv.push(
678
pandocRawStr(mappedTrim(cellSrcStr)),
679
);
680
div.push(srcDiv);
681
}
682
}
683
const outputDiv = pandocDiv({
684
id: idPlacement() === "inner" ? userId : undefined,
685
classes: outputCellClasses,
686
});
687
div.push(outputDiv);
688
outputDiv.push(pandocDiv({
689
id: ojsId,
690
attrs: [`nodetype="${innerInfo[0].cellType}"`],
691
}));
692
if (cell.options?.[kCellFigCap]) {
693
outputDiv.push(pandocRawStr(cell.options[kCellFigCap] as string));
694
}
695
}
696
697
div.emit(ls);
698
};
699
700
if (
701
cell.cell_type === "raw"
702
) {
703
// The lua filter is in charge of this, we're a NOP.
704
ls.push(cell.sourceVerbatim);
705
} else if (cell.cell_type === "markdown") {
706
// Convert to native OJS inline expression syntax then delegate to lua filter
707
// TODO: for now we just do this to detect use of `{ojs} x` syntax and then
708
// throw an error indicating its unsupported. This code needs to be updated
709
// to handle mapped strings correctly.
710
const markdown = executeInlineCodeHandlerMapped(
711
"ojs",
712
(exec) => "${" + exec + "}",
713
)(cell.sourceVerbatim);
714
715
ls.push(markdown);
716
} else if (cell.cell_type?.language === "ojs") {
717
await handleOJSCell(cell);
718
} else {
719
ls.push(cell.sourceVerbatim);
720
}
721
}
722
723
if (selfContained) {
724
const selfContainedPageResources = await makeSelfContainedResources(
725
pageResources,
726
docDir,
727
);
728
const resolver = JSON.stringify(
729
Object.fromEntries(Array.from(selfContainedPageResources)),
730
);
731
scriptContents.unshift(
732
`window._ojs.runtime.setLocalResolver(${resolver});`,
733
);
734
} else {
735
for (const resource of uniqueResources(pageResources)) {
736
resourceFiles.push(resource.filename);
737
}
738
}
739
740
// Handle shiny input and output YAML declarations
741
// deno-lint-ignore no-explicit-any
742
const serverMetadata = options.format.metadata?.server as any;
743
const normalizeMetadata = (key: string, def: string[]) => {
744
if (
745
!serverMetadata ||
746
(serverMetadata["type"] !== "shiny") ||
747
!serverMetadata[key]
748
) {
749
return def;
750
}
751
if (typeof serverMetadata[key] === "string") {
752
return [serverMetadata[key]];
753
} else {
754
return serverMetadata[key];
755
}
756
};
757
const shinyInputMetadata = normalizeMetadata("ojs-export", ["viewof"]);
758
const shinyOutputMetadata = normalizeMetadata("ojs-import", []);
759
const shinyInputs = new Set<string>();
760
const shinyInputExcludes = new Set<string>();
761
762
if (serverMetadata?.["ojs-exports"]) {
763
throw new Error(
764
"Document metadata contains server.ojs-exports; did you mean 'ojs-export' instead?",
765
);
766
}
767
if (serverMetadata?.["ojs-imports"]) {
768
throw new Error(
769
"Document metadata contains server.ojs-imports; did you mean 'ojs-import' instead?",
770
);
771
}
772
773
let importAllViews = false;
774
let importEverything = false;
775
776
for (const shinyInput of shinyInputMetadata) {
777
if (shinyInput === "viewof") {
778
importAllViews = true;
779
} else if (shinyInput === "all") {
780
importEverything = true;
781
} else if (shinyInput.startsWith("~")) {
782
shinyInputExcludes.add(shinyInput.slice(1));
783
} else {
784
shinyInputs.add(shinyInput);
785
}
786
}
787
788
const resultSet = new Set<string>();
789
if (importEverything) {
790
for (const el of ojsViews) {
791
resultSet.add(el);
792
}
793
for (const el of ojsIdentifiers) {
794
resultSet.add(el);
795
}
796
}
797
if (importAllViews) {
798
for (const el of ojsViews) {
799
resultSet.add(el);
800
}
801
}
802
for (const el of shinyInputs) {
803
resultSet.add(el);
804
}
805
for (const el of shinyInputExcludes) {
806
resultSet.delete(el);
807
}
808
809
for (const el of resultSet) {
810
moduleContents.push({
811
methodName: "interpretQuiet",
812
source: `shinyInput('${el}')`,
813
});
814
}
815
816
for (const el of shinyOutputMetadata) {
817
moduleContents.push({
818
methodName: "interpretQuiet",
819
source: `${el} = shinyOutput('${el}')`,
820
});
821
}
822
823
// finish script by calling runtime's "done with new source" handler,
824
scriptContents.push("window._ojs.runtime.interpretFromScriptTags();");
825
826
// script to append
827
const afterBody = [
828
`<script type="ojs-module-contents">`,
829
encodeBase64(JSON.stringify({ contents: moduleContents })),
830
`</script>`,
831
`<script type="module">`,
832
...scriptContents,
833
`</script>`,
834
]
835
.join("\n");
836
const includeAfterBodyFile = options.temp.createFile();
837
Deno.writeTextFileSync(includeAfterBodyFile, afterBody);
838
839
const extras = resolveDependencies(
840
{
841
html: {
842
[kDependencies]: [ojsFormatDependency(selfContained)],
843
},
844
},
845
dirname(options.source),
846
options.libDir,
847
options.temp,
848
project,
849
);
850
851
const ojsBundleTempFiles = [];
852
// FIXME is this the correct way to specify a resources path in quarto?
853
if (selfContained) {
854
// we need to inline quarto-ojs-runtime.js rather than link to it in order
855
// for ojs to work in non-webserver contexts. <script type="module" src=...></script> runs into CORS restrictions
856
const ojsBundleFilename = join(
857
quartoConfig.sharePath(),
858
"formats/html/ojs/quarto-ojs-runtime.js",
859
);
860
const ojsBundle = [
861
`<script type="module">`,
862
Deno.readTextFileSync(ojsBundleFilename),
863
`</script>`,
864
];
865
866
const filename = options.temp.createFile();
867
Deno.writeTextFileSync(filename, ojsBundle.join("\n"));
868
ojsBundleTempFiles.push(filename);
869
}
870
871
// copy ojs dependencies and inject references to them into the head
872
const includeInHeader = [
873
...(extras?.[kIncludeInHeader] || []),
874
...ojsBundleTempFiles,
875
];
876
877
return {
878
markdown: mappedJoin(ls, ""),
879
filters: [
880
"ojs",
881
],
882
includes: {
883
[kIncludeInHeader]: includeInHeader,
884
[kIncludeAfterBody]: [includeAfterBodyFile],
885
},
886
resourceFiles,
887
};
888
}
889
890
export async function ojsExecuteResult(
891
context: RenderContext,
892
executeResult: MappedExecuteResult,
893
ojsBlockLineNumbers: number[],
894
) {
895
executeResult = {
896
...executeResult,
897
};
898
899
// evaluate ojs chunks
900
const { markdown, includes, filters, resourceFiles } = await ojsCompile({
901
source: context.target.source,
902
format: context.format,
903
markdown: executeResult.markdown,
904
libDir: context.libDir,
905
project: context.project,
906
temp: context.options.services.temp,
907
ojsBlockLineNumbers,
908
});
909
910
// merge in results
911
executeResult.markdown = markdown;
912
913
if (includes) {
914
executeResult.includes = mergeConfigs(
915
includes,
916
executeResult.includes || {},
917
);
918
}
919
if (filters) {
920
executeResult.filters = (executeResult.filters || []).concat(filters);
921
}
922
923
return {
924
executeResult,
925
resourceFiles: resourceFiles || [],
926
};
927
}
928
929
// deno-lint-ignore no-explicit-any
930
function asUndefined(value: any, test: any) {
931
if (value === test) {
932
return undefined;
933
}
934
return value;
935
}
936
937
function ojsFormatDependency(selfContained: boolean) {
938
const ojsResource = (resource: string) =>
939
formatResourcePath(
940
"html",
941
join("ojs", resource),
942
);
943
const ojsDependency = (
944
resource: string,
945
attribs?: Record<string, string>,
946
) => ({
947
name: resource,
948
path: ojsResource(resource),
949
attribs,
950
});
951
952
// we potentially skip scripts here because we might need to force
953
// them to be inline in case we are running in a file:/// context.
954
const scripts = selfContained ? [] : [
955
ojsDependency("quarto-ojs-runtime.js", { type: "module" }),
956
];
957
return {
958
name: "quarto-ojs",
959
stylesheets: [
960
ojsDependency("quarto-ojs.css"),
961
],
962
scripts,
963
};
964
}
965
966