Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/execute/jupyter/jupyter.ts
6460 views
1
/*
2
* jupyter.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { basename, dirname, join, relative } from "../../deno_ral/path.ts";
8
import { satisfies } from "semver/mod.ts";
9
10
import { existsSync } from "../../deno_ral/fs.ts";
11
12
import { error, info } from "../../deno_ral/log.ts";
13
14
import * as ld from "../../core/lodash.ts";
15
16
import {
17
kBaseFormat,
18
kExecuteDaemon,
19
kExecuteEnabled,
20
kExecuteIpynb,
21
kFigDpi,
22
kFigFormat,
23
kFigPos,
24
kIncludeAfterBody,
25
kIncludeInHeader,
26
kIpynbFilters,
27
kIpynbProduceSourceNotebook,
28
kKeepHidden,
29
kKeepIpynb,
30
kNotebookPreserveCells,
31
kRemoveHidden,
32
} from "../../config/constants.ts";
33
import { Format } from "../../config/types.ts";
34
35
import {
36
executeKernelKeepalive,
37
executeKernelOneshot,
38
JupyterExecuteOptions,
39
} from "./jupyter-kernel.ts";
40
import {
41
JupyterCapabilities,
42
JupyterKernelspec,
43
JupyterNotebook,
44
JupyterWidgetDependencies,
45
} from "../../core/jupyter/types.ts";
46
47
import { RenderOptions, RenderResultFile } from "../../command/render/types.ts";
48
import {
49
DependenciesOptions,
50
EngineProjectContext,
51
ExecuteOptions,
52
ExecuteResult,
53
ExecutionEngineDiscovery,
54
ExecutionEngineInstance,
55
ExecutionTarget,
56
kJupyterEngine,
57
kQmdExtensions,
58
PandocIncludes,
59
PostProcessOptions,
60
RunOptions,
61
} from "../types.ts";
62
63
// Target data interface used by Jupyter
64
interface JupyterTargetData {
65
transient: boolean;
66
kernelspec: JupyterKernelspec;
67
}
68
69
// Import quartoAPI directly since we're in core codebase
70
import type { QuartoAPI } from "../../core/api/index.ts";
71
72
let quarto: QuartoAPI;
73
import { MappedString } from "../../core/mapped-text.ts";
74
import { kJupyterPercentScriptExtensions } from "../../core/jupyter/percent.ts";
75
import type { CheckConfiguration } from "../../command/check/check.ts";
76
77
export const jupyterEngineDiscovery: ExecutionEngineDiscovery = {
78
init: (quartoAPI) => {
79
quarto = quartoAPI;
80
},
81
82
name: kJupyterEngine,
83
defaultExt: ".qmd",
84
defaultYaml: (kernel?: string) => [
85
`jupyter: ${kernel || "python3"}`,
86
],
87
defaultContent: (kernel?: string) => {
88
kernel = kernel || "python3";
89
const lang = kernel.startsWith("python")
90
? "python"
91
: kernel.startsWith("julia")
92
? "julia"
93
: undefined;
94
if (lang) {
95
return [
96
"```{" + lang + "}",
97
"1 + 1",
98
"```",
99
];
100
} else {
101
return [];
102
}
103
},
104
validExtensions: () => [
105
...quarto.jupyter.notebookExtensions,
106
...kJupyterPercentScriptExtensions,
107
...kQmdExtensions,
108
],
109
claimsFile: (file: string, ext: string) => {
110
return quarto.jupyter.notebookExtensions.includes(ext.toLowerCase()) ||
111
quarto.jupyter.isPercentScript(file);
112
},
113
claimsLanguage: (language: string) => {
114
// jupyter has to claim julia so that julia may also claim it without changing the old behavior
115
// of preferring jupyter over julia engine by default
116
return language.toLowerCase() === "julia";
117
},
118
canFreeze: true,
119
generatesFigures: true,
120
ignoreDirs: () => {
121
return ["venv", "env"];
122
},
123
124
checkInstallation: async (conf: CheckConfiguration) => {
125
const kIndent = " ";
126
127
// Helper functions (inline)
128
const checkCompleteMessage = (message: string) => {
129
if (!conf.jsonResult) {
130
quarto.console.completeMessage(message);
131
}
132
};
133
const checkInfoMsg = (message: string) => {
134
if (!conf.jsonResult) {
135
info(message);
136
}
137
};
138
139
// Render check helper (inline)
140
const checkJupyterRender = async () => {
141
const json: Record<string, unknown> = {};
142
if (conf.jsonResult) {
143
(conf.jsonResult.render as Record<string, unknown>).jupyter = json;
144
}
145
146
const result = await quarto.system.checkRender({
147
content: `
148
---
149
title: "Title"
150
---
151
152
## Header
153
154
\`\`\`{python}
155
1 + 1
156
\`\`\`
157
`,
158
language: "python",
159
services: conf.services,
160
});
161
162
if (result.error) {
163
if (!conf.jsonResult) {
164
throw result.error;
165
} else {
166
json["error"] = result.error;
167
}
168
} else {
169
json["ok"] = true;
170
}
171
};
172
173
// Main check logic
174
const kMessage = "Checking Python 3 installation....";
175
const jupyterJson: Record<string, unknown> = {};
176
if (conf.jsonResult) {
177
(conf.jsonResult.tools as Record<string, unknown>).jupyter = jupyterJson;
178
}
179
let caps: JupyterCapabilities | undefined;
180
if (conf.jsonResult) {
181
caps = await quarto.jupyter.capabilities();
182
} else {
183
await quarto.console.withSpinner({
184
message: kMessage,
185
doneMessage: false,
186
}, async () => {
187
caps = await quarto.jupyter.capabilities();
188
});
189
}
190
if (caps) {
191
checkCompleteMessage(kMessage + "OK");
192
if (conf.jsonResult) {
193
jupyterJson["capabilities"] = await quarto.jupyter.capabilitiesJson(
194
caps,
195
);
196
} else {
197
checkInfoMsg(await quarto.jupyter.capabilitiesMessage(caps, kIndent));
198
}
199
checkInfoMsg("");
200
if (caps.jupyter_core) {
201
if (await quarto.jupyter.kernelspecForLanguage("python")) {
202
const kJupyterMessage = "Checking Jupyter engine render....";
203
if (conf.jsonResult) {
204
await checkJupyterRender();
205
} else {
206
await quarto.console.withSpinner({
207
message: kJupyterMessage,
208
doneMessage: kJupyterMessage + "OK\n",
209
}, async () => {
210
await checkJupyterRender();
211
});
212
}
213
} else {
214
jupyterJson["kernels"] = [];
215
checkInfoMsg(
216
kIndent + "NOTE: No Jupyter kernel for Python found",
217
);
218
checkInfoMsg("");
219
}
220
} else {
221
const installMessage = quarto.jupyter.installationMessage(
222
caps,
223
kIndent,
224
);
225
checkInfoMsg(installMessage);
226
checkInfoMsg("");
227
jupyterJson["installed"] = false;
228
jupyterJson["how-to-install"] = installMessage;
229
const envMessage = quarto.jupyter.unactivatedEnvMessage(caps, kIndent);
230
if (envMessage) {
231
checkInfoMsg(envMessage);
232
checkInfoMsg("");
233
jupyterJson["env"] = {
234
"warning": envMessage,
235
};
236
}
237
}
238
} else {
239
checkCompleteMessage(kMessage + "(None)\n");
240
const msg = quarto.jupyter.pythonInstallationMessage(kIndent);
241
jupyterJson["installed"] = false;
242
jupyterJson["how-to-install-python"] = msg;
243
checkInfoMsg(msg);
244
checkInfoMsg("");
245
}
246
},
247
248
// Launch method will return an instance with context
249
launch: (context: EngineProjectContext): ExecutionEngineInstance => {
250
return {
251
name: jupyterEngineDiscovery.name,
252
canFreeze: jupyterEngineDiscovery.canFreeze,
253
254
markdownForFile: (file: string): Promise<MappedString> => {
255
if (quarto.jupyter.isJupyterNotebook(file)) {
256
const nbJSON = Deno.readTextFileSync(file);
257
const nb = quarto.jupyter.fromJSON(nbJSON);
258
return Promise.resolve(
259
quarto.mappedString.fromString(
260
quarto.jupyter.markdownFromNotebookJSON(nb),
261
),
262
);
263
} else if (quarto.jupyter.isPercentScript(file)) {
264
return Promise.resolve(
265
quarto.mappedString.fromString(
266
quarto.jupyter.percentScriptToMarkdown(file),
267
),
268
);
269
} else {
270
return Promise.resolve(quarto.mappedString.fromFile(file));
271
}
272
},
273
274
target: async (
275
file: string,
276
quiet?: boolean,
277
markdown?: MappedString,
278
): Promise<ExecutionTarget | undefined> => {
279
if (!markdown) {
280
markdown = await context.resolveFullMarkdownForFile(undefined, file);
281
}
282
283
// at some point we'll resolve a full notebook/kernelspec
284
let nb: JupyterNotebook | undefined;
285
if (quarto.jupyter.isJupyterNotebook(file)) {
286
const nbJSON = Deno.readTextFileSync(file);
287
const nbRaw = JSON.parse(nbJSON);
288
289
// https://github.com/quarto-dev/quarto-cli/issues/12374
290
// kernelspecs are not guaranteed to have a language field
291
// so we need to check for it and if not present
292
// use the language_info.name field
293
if (
294
nbRaw.metadata.kernelspec &&
295
nbRaw.metadata.kernelspec.language === undefined &&
296
nbRaw.metadata.language_info?.name
297
) {
298
nbRaw.metadata.kernelspec.language =
299
nbRaw.metadata.language_info.name;
300
}
301
nb = nbRaw as JupyterNotebook;
302
}
303
304
// cache check for percent script
305
const isPercentScript = quarto.jupyter.isPercentScript(file);
306
307
// get the metadata
308
const metadata = quarto.markdownRegex.extractYaml(markdown!.value);
309
310
// if this is a text markdown file then create a notebook for use as the execution target
311
if (quarto.path.isQmdFile(file) || isPercentScript) {
312
// write a transient notebook
313
const [fileDir, fileStem] = quarto.path.dirAndStem(file);
314
// See #4802
315
// I don't love using an extension other than .ipynb for this file,
316
// but doing something like .quarto.ipynb would require a lot
317
// of additional changes to our file handling code (without changes,
318
// our output files would be called $FILE.quarto.html, which
319
// is not what we want). So for now, we'll use .quarto_ipynb
320
let counter: number | undefined = undefined;
321
let notebook = join(
322
fileDir,
323
`${fileStem}.quarto_ipynb${counter ? "_" + String(counter) : ""}`,
324
);
325
326
while (existsSync(notebook)) {
327
if (!counter) {
328
counter = 1;
329
} else {
330
++counter;
331
}
332
notebook = join(
333
fileDir,
334
`${fileStem}.quarto_ipynb${counter ? "_" + String(counter) : ""}`,
335
);
336
}
337
const target = {
338
source: file,
339
input: notebook,
340
markdown: markdown!,
341
metadata,
342
data: { transient: true, kernelspec: {} },
343
};
344
nb = await createNotebookforTarget(target);
345
target.data.kernelspec = nb.metadata.kernelspec;
346
return target;
347
} else if (quarto.jupyter.isJupyterNotebook(file)) {
348
return {
349
source: file,
350
input: file,
351
markdown: markdown!,
352
metadata,
353
data: { transient: false, kernelspec: nb?.metadata.kernelspec },
354
};
355
} else {
356
return undefined;
357
}
358
},
359
360
partitionedMarkdown: async (file: string, format?: Format) => {
361
if (quarto.jupyter.isJupyterNotebook(file)) {
362
return quarto.markdownRegex.partition(
363
await quarto.jupyter.markdownFromNotebookFile(file, format),
364
);
365
} else if (quarto.jupyter.isPercentScript(file)) {
366
return quarto.markdownRegex.partition(
367
quarto.jupyter.percentScriptToMarkdown(file),
368
);
369
} else {
370
return quarto.markdownRegex.partition(Deno.readTextFileSync(file));
371
}
372
},
373
374
filterFormat: (
375
source: string,
376
options: RenderOptions,
377
format: Format,
378
) => {
379
// if this is shiny server and the user hasn't set keep-hidden then
380
// set it as well as the attibutes required to remove the hidden blocks
381
if (
382
quarto.format.isServerShinyPython(format, kJupyterEngine) &&
383
format.render[kKeepHidden] !== true
384
) {
385
format = {
386
...format,
387
render: {
388
...format.render,
389
},
390
metadata: {
391
...format.metadata,
392
},
393
};
394
format.render[kKeepHidden] = true;
395
format.metadata[kRemoveHidden] = "all";
396
}
397
398
if (quarto.jupyter.isJupyterNotebook(source)) {
399
// see if we want to override execute enabled
400
let executeEnabled: boolean | null | undefined;
401
402
// we never execute for a dev server reload
403
if (options.devServerReload) {
404
executeEnabled = false;
405
406
// if a specific ipynb execution policy is set then reflect it
407
} else if (typeof (format.execute[kExecuteIpynb]) === "boolean") {
408
executeEnabled = format.execute[kExecuteIpynb];
409
410
// if a specific execution policy is set then reflect it
411
} else if (typeof (format.execute[kExecuteEnabled]) == "boolean") {
412
executeEnabled = format.execute[kExecuteEnabled];
413
414
// otherwise default to NOT executing
415
} else {
416
executeEnabled = false;
417
}
418
419
// return format w/ execution policy
420
if (executeEnabled !== undefined) {
421
return {
422
...format,
423
execute: {
424
...format.execute,
425
[kExecuteEnabled]: executeEnabled,
426
},
427
};
428
// otherwise just return the original format
429
} else {
430
return format;
431
}
432
// not an ipynb
433
} else {
434
return format;
435
}
436
},
437
438
execute: async (options: ExecuteOptions): Promise<ExecuteResult> => {
439
// create the target input if we need to (could have been removed
440
// by the cleanup step of another render in this invocation)
441
if (
442
(quarto.path.isQmdFile(options.target.source) ||
443
quarto.jupyter.isPercentScript(options.target.source)) &&
444
!existsSync(options.target.input)
445
) {
446
await createNotebookforTarget(options.target);
447
}
448
449
// determine the kernel (it's in the custom execute options data)
450
let kernelspec = (options.target.data as JupyterTargetData).kernelspec;
451
452
// determine execution behavior
453
const execute = options.format.execute[kExecuteEnabled] !== false;
454
if (execute) {
455
// if yaml front matter has a different kernel then use it
456
if (quarto.jupyter.isJupyterNotebook(options.target.source)) {
457
kernelspec =
458
await ensureYamlKernelspec(options.target, kernelspec) ||
459
kernelspec;
460
}
461
462
// jupyter back end requires full path to input (to ensure that
463
// keepalive kernels are never re-used across multiple inputs
464
// that happen to share a hash)
465
const execOptions = {
466
...options,
467
target: {
468
...options.target,
469
input: quarto.path.absolute(options.target.input),
470
},
471
};
472
473
// use daemon by default if we are in an interactive session (terminal
474
// or rstudio) and not running in a CI system.
475
let executeDaemon = options.format.execute[kExecuteDaemon];
476
if (executeDaemon === null || executeDaemon === undefined) {
477
if (await disableDaemonForNotebook(options.target)) {
478
executeDaemon = false;
479
} else {
480
executeDaemon = quarto.system.isInteractiveSession() &&
481
!quarto.system.runningInCI();
482
}
483
}
484
const jupyterExecOptions: JupyterExecuteOptions = {
485
kernelspec,
486
python_cmd: await quarto.jupyter.pythonExec(kernelspec),
487
supervisor_pid: options.previewServer ? Deno.pid : undefined,
488
...execOptions,
489
};
490
if (executeDaemon === false || executeDaemon === 0) {
491
await executeKernelOneshot(jupyterExecOptions);
492
} else {
493
await executeKernelKeepalive(jupyterExecOptions);
494
}
495
}
496
497
// convert to markdown and write to target (only run notebook filters
498
// if the source is an ipynb file)
499
const nbContents = await quarto.jupyter.notebookFiltered(
500
options.target.input,
501
quarto.jupyter.isJupyterNotebook(options.target.source)
502
? (options.format.execute[kIpynbFilters] as string[] || [])
503
: [],
504
);
505
506
const nb = quarto.jupyter.fromJSON(nbContents);
507
508
// cells tagged 'shinylive' should be emmited as markdown
509
fixupShinyliveCodeCells(nb);
510
511
const assets = quarto.jupyter.assets(
512
options.target.input,
513
options.format.pandoc.to,
514
);
515
516
// Preserve the cell metadata if users have asked us to, or if this is dashboard
517
// that is coming from a non-qmd source
518
const preserveCellMetadata =
519
options.format.render[kNotebookPreserveCells] === true ||
520
(quarto.format.isHtmlDashboardOutput(
521
options.format.identifier[kBaseFormat],
522
) &&
523
!quarto.path.isQmdFile(options.target.source));
524
525
// NOTE: for perforance reasons the 'nb' is mutated in place
526
// by jupyterToMarkdown (we don't want to make a copy of a
527
// potentially very large notebook) so should not be relied
528
// on subseuqent to this call
529
const result = await quarto.jupyter.toMarkdown(
530
nb,
531
{
532
executeOptions: options,
533
language: nb.metadata.kernelspec.language.toLowerCase(),
534
assets,
535
execute: options.format.execute,
536
keepHidden: options.format.render[kKeepHidden],
537
toHtml: quarto.format.isHtmlCompatible(options.format),
538
toLatex: quarto.format.isLatexOutput(options.format.pandoc),
539
toMarkdown: quarto.format.isMarkdownOutput(options.format),
540
toIpynb: quarto.format.isIpynbOutput(options.format.pandoc),
541
toPresentation: quarto.format.isPresentationOutput(
542
options.format.pandoc,
543
),
544
figFormat: options.format.execute[kFigFormat],
545
figDpi: options.format.execute[kFigDpi],
546
figPos: options.format.render[kFigPos],
547
preserveCellMetadata,
548
preserveCodeCellYaml:
549
options.format.render[kIpynbProduceSourceNotebook] === true,
550
},
551
);
552
553
// return dependencies as either includes or raw dependencies
554
let includes: PandocIncludes | undefined;
555
let engineDependencies: Record<string, Array<unknown>> | undefined;
556
if (options.dependencies) {
557
includes = quarto.jupyter.resultIncludes(
558
options.tempDir,
559
result.dependencies,
560
);
561
} else {
562
const dependencies = quarto.jupyter.resultEngineDependencies(
563
result.dependencies,
564
);
565
if (dependencies) {
566
engineDependencies = {
567
[kJupyterEngine]: dependencies,
568
};
569
}
570
}
571
572
// if it's a transient notebook then remove it
573
// (unless keep-ipynb was specified)
574
cleanupNotebook(options.target, options.format, context);
575
576
// Create markdown from the result
577
const outputs = result.cellOutputs.map((output) => output.markdown);
578
if (result.notebookOutputs) {
579
if (result.notebookOutputs.prefix) {
580
outputs.unshift(result.notebookOutputs.prefix);
581
}
582
if (result.notebookOutputs.suffix) {
583
outputs.push(result.notebookOutputs.suffix);
584
}
585
}
586
const markdown = outputs.join("");
587
588
// return results
589
return {
590
engine: kJupyterEngine,
591
markdown: markdown,
592
supporting: [join(assets.base_dir, assets.supporting_dir)],
593
filters: [],
594
pandoc: result.pandoc,
595
includes,
596
engineDependencies,
597
preserve: result.htmlPreserve,
598
postProcess: result.htmlPreserve &&
599
(Object.keys(result.htmlPreserve).length > 0),
600
};
601
},
602
603
executeTargetSkipped: (target: ExecutionTarget, format: Format) => {
604
cleanupNotebook(target, format, context);
605
},
606
607
dependencies: (options: DependenciesOptions) => {
608
const includes: PandocIncludes = {};
609
if (options.dependencies) {
610
const includeFiles = quarto.jupyter.widgetDependencyIncludes(
611
options.dependencies as JupyterWidgetDependencies[],
612
options.tempDir,
613
);
614
if (includeFiles.inHeader) {
615
includes[kIncludeInHeader] = [includeFiles.inHeader];
616
}
617
if (includeFiles.afterBody) {
618
includes[kIncludeAfterBody] = [includeFiles.afterBody];
619
}
620
}
621
return Promise.resolve({
622
includes,
623
});
624
},
625
626
postprocess: (options: PostProcessOptions) => {
627
quarto.text.postProcessRestorePreservedHtml(options);
628
return Promise.resolve();
629
},
630
631
canKeepSource: (target: ExecutionTarget) => {
632
return !quarto.jupyter.isJupyterNotebook(target.source);
633
},
634
635
intermediateFiles: (input: string) => {
636
const files: string[] = [];
637
const [fileDir, fileStem] = quarto.path.dirAndStem(input);
638
639
if (!quarto.jupyter.isJupyterNotebook(input)) {
640
files.push(join(fileDir, fileStem + ".ipynb"));
641
} else if (
642
[...kQmdExtensions, ...kJupyterPercentScriptExtensions].some(
643
(ext) => {
644
return existsSync(join(fileDir, fileStem + ext));
645
},
646
)
647
) {
648
files.push(input);
649
}
650
return files;
651
},
652
653
run: async (options: RunOptions): Promise<void> => {
654
// semver doesn't support 4th component
655
const asSemVer = (version: string) => {
656
const v = version.split(".");
657
if (v.length > 3) {
658
return `${v[0]}.${v[1]}.${v[2]}`;
659
} else {
660
return version;
661
}
662
};
663
664
// confirm required version of shiny
665
const kShinyVersion = ">=0.6";
666
let shinyError: string | undefined;
667
const caps = await quarto.jupyter.capabilities();
668
if (!caps?.shiny) {
669
shinyError =
670
"The shiny package is required for documents with server: shiny";
671
} else if (
672
!satisfies(asSemVer(caps.shiny), asSemVer(kShinyVersion))
673
) {
674
shinyError =
675
`The shiny package version must be ${kShinyVersion} for documents with server: shiny`;
676
}
677
if (shinyError) {
678
shinyError +=
679
"\n\nInstall the latest version of shiny with pip install --upgrade shiny\n";
680
error(shinyError);
681
throw new Error();
682
}
683
684
const [_dir] = quarto.path.dirAndStem(options.input);
685
const appFile = "app.py";
686
const cmd = [
687
...await quarto.jupyter.pythonExec(),
688
"-m",
689
"shiny",
690
"run",
691
appFile,
692
"--host",
693
options.host!,
694
"--port",
695
String(options.port!),
696
];
697
if (options.reload) {
698
cmd.push("--reload");
699
cmd.push(`--reload-includes=*.py`);
700
}
701
702
// start server
703
const readyPattern =
704
/(http:\/\/(?:localhost|127\.0\.0\.1)\:\d+\/?[^\s]*)/;
705
const server = quarto.system.runExternalPreviewServer({
706
cmd,
707
readyPattern,
708
cwd: dirname(options.input),
709
});
710
await server.start();
711
712
// stop the server onCleanup
713
quarto.system.onCleanup(async () => {
714
await server.stop();
715
});
716
717
// notify when ready
718
if (options.onReady) {
719
options.onReady();
720
}
721
722
// run the server
723
return server.serve();
724
},
725
726
postRender: async (file: RenderResultFile) => {
727
// discover non _files dir resources for server: shiny and amend app.py with them
728
if (quarto.format.isServerShiny(file.format)) {
729
const [dir] = quarto.path.dirAndStem(file.input);
730
const filesDir = join(dir, quarto.path.inputFilesDir(file.input));
731
const extraResources = file.resourceFiles
732
.filter((resource) => !resource.startsWith(filesDir))
733
.map((resource) => relative(dir, resource));
734
const appScriptDir = context ? context.getOutputDirectory() : dir;
735
const appScript = join(appScriptDir, `app.py`);
736
if (existsSync(appScript)) {
737
// compute static assets
738
const staticAssets = [
739
quarto.path.inputFilesDir(file.input),
740
...extraResources,
741
];
742
743
// check for (illegal) parent dir assets
744
const parentDirAssets = staticAssets.filter((asset) =>
745
asset.startsWith("..")
746
);
747
if (parentDirAssets.length > 0) {
748
error(
749
`References to files in parent directories found in document with server: shiny ` +
750
`(${basename(file.input)}): ${
751
JSON.stringify(parentDirAssets)
752
}. All resource files referenced ` +
753
`by Shiny documents must exist in the same directory as the source file.`,
754
);
755
throw new Error();
756
}
757
758
// In the app.py file, replace the placeholder with the list of static assets.
759
let appContents = Deno.readTextFileSync(appScript);
760
appContents = appContents.replace(
761
"##STATIC_ASSETS_PLACEHOLDER##",
762
JSON.stringify(staticAssets),
763
);
764
Deno.writeTextFileSync(appScript, appContents);
765
}
766
}
767
},
768
};
769
},
770
};
771
772
async function ensureYamlKernelspec(
773
target: ExecutionTarget,
774
kernelspec: JupyterKernelspec,
775
) {
776
const markdown = target.markdown.value;
777
const yamlJupyter = quarto.markdownRegex.extractYaml(markdown)?.jupyter;
778
if (yamlJupyter && typeof yamlJupyter !== "boolean") {
779
const [yamlKernelspec, _] = await quarto.jupyter.kernelspecFromMarkdown(
780
markdown,
781
);
782
if (yamlKernelspec.name !== kernelspec?.name) {
783
const nb = quarto.jupyter.fromJSON(Deno.readTextFileSync(target.source));
784
nb.metadata.kernelspec = yamlKernelspec;
785
Deno.writeTextFileSync(target.source, JSON.stringify(nb, null, 2));
786
return yamlKernelspec;
787
}
788
}
789
}
790
791
function fixupShinyliveCodeCells(nb: JupyterNotebook) {
792
if (nb.metadata.kernelspec.language === "python") {
793
nb.cells.forEach((cell) => {
794
if (
795
cell.cell_type === "code" && cell.metadata.tags?.includes("shinylive")
796
) {
797
cell.cell_type = "markdown";
798
cell.metadata = {};
799
cell.source = [
800
"```{shinylive-python}\n",
801
...cell.source,
802
"\n```",
803
];
804
delete cell.execution_count;
805
delete cell.outputs;
806
}
807
});
808
}
809
}
810
811
async function createNotebookforTarget(
812
target: ExecutionTarget,
813
project?: EngineProjectContext,
814
) {
815
const nb = await quarto.jupyter.quartoMdToJupyter(
816
target.markdown.value,
817
true,
818
project,
819
);
820
Deno.writeTextFileSync(target.input, JSON.stringify(nb, null, 2));
821
return nb;
822
}
823
824
// mitigate conflict between pexpect and our daamonization, see
825
// https://github.com/quarto-dev/quarto-cli/discussions/728
826
async function disableDaemonForNotebook(target: ExecutionTarget) {
827
const kShellMagics = [
828
"cd",
829
"cat",
830
"cp",
831
"env",
832
"ls",
833
"man",
834
"mkdir",
835
"more",
836
"mv",
837
"pwd",
838
"rm",
839
"rmdir",
840
];
841
const nb = await quarto.markdownRegex.breakQuartoMd(target.markdown);
842
for (const cell of nb.cells) {
843
if (ld.isObject(cell.cell_type)) {
844
const language = (cell.cell_type as { language: string }).language;
845
if (language === "python") {
846
if (cell.source.value.startsWith("!")) {
847
return true;
848
}
849
return (kShellMagics.some((cmd) =>
850
cell.source.value.includes("%" + cmd + " ") ||
851
cell.source.value.includes("!" + cmd + " ") ||
852
cell.source.value.startsWith(cmd + " ")
853
));
854
}
855
}
856
}
857
858
return false;
859
}
860
861
function cleanupNotebook(
862
target: ExecutionTarget,
863
format: Format,
864
project: EngineProjectContext,
865
) {
866
// Make notebook non-transient when keep-ipynb is set
867
const data = target.data as JupyterTargetData;
868
const cached = project.fileInformationCache.get(target.source);
869
if (cached && data.transient && format.execute[kKeepIpynb]) {
870
if (cached.target && cached.target.data) {
871
(cached.target.data as JupyterTargetData).transient = false;
872
}
873
}
874
}
875
876
interface JupyterTargetData {
877
transient: boolean;
878
kernelspec: JupyterKernelspec;
879
}
880
881