Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/render/notebook/notebook-context.ts
6458 views
1
/*
2
* notebook-context.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { ExecutedFile, RenderServices } from "../../command/render/types.ts";
8
import { InternalError } from "../../core/lib/error.ts";
9
import { kJatsSubarticle } from "../../format/jats/format-jats-types.ts";
10
import { ProjectContext } from "../../project/types.ts";
11
import {
12
kHtmlPreview,
13
kQmdIPynb,
14
kRenderedIPynb,
15
Notebook,
16
NotebookContext,
17
NotebookContributor,
18
NotebookMetadata,
19
NotebookOutput,
20
NotebookRenderResult,
21
RenderType,
22
} from "./notebook-types.ts";
23
24
import { basename, dirname, isAbsolute, join } from "../../deno_ral/path.ts";
25
import { jatsContributor } from "./notebook-contributor-jats.ts";
26
import { htmlNotebookContributor } from "./notebook-contributor-html.ts";
27
import { outputNotebookContributor } from "./notebook-contributor-ipynb.ts";
28
import { Format } from "../../config/types.ts";
29
import { safeExistsSync, safeRemoveIfExists } from "../../core/path.ts";
30
import { qmdNotebookContributor } from "./notebook-contributor-qmd.ts";
31
import { debug } from "../../deno_ral/log.ts";
32
33
const contributors: Record<RenderType, NotebookContributor | undefined> = {
34
[kJatsSubarticle]: jatsContributor,
35
[kHtmlPreview]: htmlNotebookContributor,
36
[kRenderedIPynb]: outputNotebookContributor,
37
[kQmdIPynb]: qmdNotebookContributor,
38
};
39
40
export function notebookContext(): NotebookContext {
41
const notebooks: Record<string, Notebook> = {};
42
const preserveNotebooks: Record<string, RenderType[]> = {};
43
let nbCount = 0;
44
45
const token = () => {
46
return `nb-${++nbCount}`;
47
};
48
49
const emptyNotebook = (nbAbsPath: string): Notebook => {
50
return {
51
source: nbAbsPath,
52
};
53
};
54
55
// Adds a rendering of a notebook to the notebook context
56
const addRendering = (
57
nbAbsPath: string,
58
renderType: RenderType,
59
result: NotebookRenderResult,
60
context: ProjectContext,
61
cached?: boolean,
62
) => {
63
debug(`[NotebookContext]: Add Rendering (${renderType}):${nbAbsPath}`);
64
65
const hrefPath = join(dirname(nbAbsPath), basename(result.file));
66
const absPath = isAbsolute(result.file) ? result.file : hrefPath;
67
const output: NotebookOutput = {
68
path: absPath,
69
hrefPath,
70
supporting: result.supporting || [],
71
resourceFiles: result.resourceFiles,
72
cached,
73
};
74
75
const nb: Notebook = notebooks[nbAbsPath] || emptyNotebook(nbAbsPath);
76
nb[renderType] = output;
77
notebooks[nbAbsPath] = nb;
78
needRewrite = true;
79
80
if (context) {
81
const contrib = contributor(renderType);
82
if (contrib.cache) {
83
contrib.cache(output, context);
84
}
85
}
86
};
87
88
// Removes a rendering of a notebook from the notebook context
89
// which includes cleaning up the files. This should only be
90
// used when the caller knows other callers will not need the
91
// notebook.
92
const removeRendering = (
93
nbAbsPath: string,
94
renderType: RenderType,
95
preserveFiles: string[],
96
) => {
97
debug(`[NotebookContext]: Remove Rendering (${renderType}):${nbAbsPath}`);
98
if (
99
preserveNotebooks[nbAbsPath] &&
100
preserveNotebooks[nbAbsPath].includes(renderType)
101
) {
102
// Someone asked to preserve this, don't clean it up
103
return;
104
}
105
const nb: Notebook = notebooks[nbAbsPath];
106
if (nb) {
107
const rendering = nb[renderType];
108
109
if (rendering) {
110
safeRemoveIfExists(rendering.path);
111
const filteredSupporting = rendering.supporting.filter(
112
(file) => {
113
const absPath = join(dirname(nbAbsPath), file);
114
return !preserveFiles.includes(absPath);
115
},
116
);
117
for (const supporting of filteredSupporting) {
118
safeRemoveIfExists(supporting);
119
}
120
}
121
}
122
};
123
124
// Get a contribute for a render type
125
function contributor(renderType: RenderType) {
126
const contributor = contributors[renderType];
127
if (contributor) {
128
return contributor;
129
} else {
130
throw new InternalError(
131
`Missing contributor ${renderType} when resolving`,
132
);
133
}
134
}
135
136
// Add metadata to a given notebook rendering
137
function addMetadata(
138
nbAbsPath: string,
139
nbMeta: NotebookMetadata,
140
) {
141
debug(`[NotebookContext]: Add Notebook Metadata:${nbAbsPath}`);
142
const nb: Notebook = notebooks[nbAbsPath] || emptyNotebook(nbAbsPath);
143
nb.metadata = nbMeta;
144
notebooks[nbAbsPath] = nb;
145
}
146
147
function reviveOutput(
148
nbAbsPath: string,
149
renderType: RenderType,
150
context: ProjectContext,
151
) {
152
debug(
153
`[NotebookContext]: Attempting to Revive Rendering (${renderType}):${nbAbsPath}`,
154
);
155
const contrib = contributor(renderType);
156
157
if (contrib.cachedPath) {
158
const existingPath = contrib.cachedPath(nbAbsPath, context);
159
if (existingPath) {
160
if (safeExistsSync(existingPath)) {
161
const inputTime = Deno.statSync(nbAbsPath).mtime?.valueOf() || 0;
162
const outputTime = Deno.statSync(existingPath).mtime?.valueOf() || 0;
163
if (inputTime <= outputTime) {
164
debug(
165
`[NotebookContext]: Revived Rendering (${renderType}):${nbAbsPath}`,
166
);
167
addRendering(
168
nbAbsPath,
169
renderType,
170
{
171
file: existingPath,
172
supporting: [],
173
resourceFiles: {
174
globs: [],
175
files: [],
176
},
177
},
178
context,
179
true,
180
);
181
}
182
}
183
}
184
}
185
}
186
187
function preserve(nbAbsPath: string, renderType: RenderType) {
188
debug(`[NotebookContext]: Preserving (${renderType}):${nbAbsPath}`);
189
preserveNotebooks[nbAbsPath] = preserveNotebooks[nbAbsPath] || [];
190
if (!preserveNotebooks[nbAbsPath].includes(renderType)) {
191
preserveNotebooks[nbAbsPath].push(renderType);
192
}
193
}
194
195
let allNotebooksTempFilename: string | undefined;
196
let needRewrite = true;
197
198
return {
199
all: (context: ProjectContext) => {
200
if (!allNotebooksTempFilename) {
201
allNotebooksTempFilename = context.temp.createFile({
202
suffix: ".json",
203
});
204
}
205
if (needRewrite) {
206
debug(
207
`[NotebookContext]: Writing all notebooks to ${allNotebooksTempFilename}`,
208
);
209
const objs = Object.values(notebooks);
210
Deno.writeTextFileSync(
211
allNotebooksTempFilename,
212
JSON.stringify(objs),
213
);
214
needRewrite = false;
215
}
216
return allNotebooksTempFilename;
217
},
218
get: (nbAbsPath: string, context?: ProjectContext) => {
219
debug(`[NotebookContext]: Get Notebook:${nbAbsPath}`);
220
const notebook = notebooks[nbAbsPath];
221
const reviveRenders: RenderType[] = [];
222
if (notebook) {
223
// We already have a notebook, try to complete its renderings
224
// by reviving any outputs that are valid
225
[kJatsSubarticle, kHtmlPreview, kRenderedIPynb].forEach(
226
(renderTypeStr) => {
227
const renderType = renderTypeStr as RenderType;
228
if (!notebook[renderType]) {
229
reviveRenders.push(renderType);
230
}
231
},
232
);
233
} else {
234
reviveRenders.push(kHtmlPreview);
235
reviveRenders.push(kJatsSubarticle);
236
reviveRenders.push(kRenderedIPynb);
237
reviveRenders.push(kQmdIPynb);
238
}
239
240
if (context) {
241
// See if an up to date rendered result exists for each contributor
242
// TODO: consider doing this check only when a render type is requested
243
// or at some other time to reduce the frequency (currently revive is being
244
// attempted anytime a notebook `get` is called)
245
for (const renderType of reviveRenders) {
246
reviveOutput(nbAbsPath, renderType, context);
247
}
248
}
249
return notebooks[nbAbsPath];
250
},
251
addMetadata: (nbAbsPath: string, notebookMetadata: NotebookMetadata) => {
252
addMetadata(nbAbsPath, notebookMetadata);
253
},
254
resolve: (
255
nbAbsPath: string,
256
renderType: RenderType,
257
executedFile: ExecutedFile,
258
notebookMetadata?: NotebookMetadata,
259
) => {
260
debug(
261
`[NotebookContext]: Resolving ExecutedFile (${renderType}):${nbAbsPath}`,
262
);
263
if (notebookMetadata) {
264
addMetadata(nbAbsPath, notebookMetadata);
265
}
266
return contributor(renderType).resolve(
267
nbAbsPath,
268
token(),
269
executedFile,
270
notebookMetadata,
271
);
272
},
273
addRendering,
274
removeRendering,
275
render: async (
276
nbAbsPath: string,
277
format: Format,
278
renderType: RenderType,
279
services: RenderServices,
280
notebookMetadata: NotebookMetadata | undefined,
281
project: ProjectContext,
282
) => {
283
debug(`[NotebookContext]: Rendering (${renderType}):${nbAbsPath}`);
284
285
if (notebookMetadata) {
286
addMetadata(nbAbsPath, notebookMetadata);
287
}
288
289
// If there is a source representation of the qmd file
290
// we should use that, which will prevent rexecution of the
291
// QMD
292
const notebook = notebooks[nbAbsPath];
293
const toRenderPath = notebook
294
? notebook[kQmdIPynb] ? notebook[kQmdIPynb].path : nbAbsPath
295
: nbAbsPath;
296
297
const renderedFile = await contributor(renderType).render(
298
toRenderPath,
299
format,
300
token(),
301
services,
302
notebookMetadata,
303
project,
304
);
305
306
addRendering(nbAbsPath, renderType, renderedFile, project);
307
if (!notebooks[nbAbsPath][renderType]) {
308
throw new InternalError(
309
"We just rendered and contributed a notebook, but it isn't present in the notebook context.",
310
);
311
}
312
return notebooks[nbAbsPath][renderType]!;
313
},
314
preserve,
315
cleanup: () => {
316
debug(`[NotebookContext]: Starting Cleanup`);
317
const hasNotebooks = Object.keys(notebooks).length > 0;
318
if (hasNotebooks) {
319
Object.keys(contributors).forEach((renderTypeStr) => {
320
Object.values(notebooks).forEach((notebook) => {
321
const renderType = renderTypeStr as RenderType;
322
// Check to see if this is preserved, if it is
323
// skip clean up for this notebook and render type
324
if (
325
!preserveNotebooks[notebook.source] ||
326
!preserveNotebooks[notebook.source].includes(renderType)
327
) {
328
const notebookOutput = notebook[renderType];
329
if (notebookOutput && notebookOutput.cached !== true) {
330
debug(
331
`[NotebookContext]: Cleanup (${renderType}):${notebook.source}`,
332
);
333
debug(
334
`[NotebookContext]: Deleting (${notebookOutput.path}`,
335
);
336
safeRemoveIfExists(notebookOutput.path);
337
for (const supporting of notebookOutput.supporting) {
338
safeRemoveIfExists(supporting);
339
}
340
}
341
}
342
});
343
});
344
}
345
},
346
};
347
}
348
349