Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/html/format-html-notebook-preview.ts
6450 views
1
/*
2
* format-html-notebook-preview.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { asArray } from "../../core/array.ts";
7
import * as ld from "../../core/lodash.ts";
8
9
import {
10
kDownloadUrl,
11
kNotebookPreviewOptions,
12
} from "../../config/constants.ts";
13
import { Format, NotebookPreviewDescriptor } from "../../config/types.ts";
14
15
import { RenderServices } from "../../command/render/types.ts";
16
17
import {
18
basename,
19
dirname,
20
isAbsolute,
21
join,
22
relative,
23
} from "../../deno_ral/path.ts";
24
import { pathWithForwardSlashes } from "../../core/path.ts";
25
import { ProjectContext } from "../../project/types.ts";
26
import { projectIsBook } from "../../project/project-shared.ts";
27
import {
28
kHtmlPreview,
29
NotebookPreviewOptions,
30
} from "../../render/notebook/notebook-types.ts";
31
import { kRenderedIPynb } from "../../render/notebook/notebook-types.ts";
32
import { InternalError } from "../../core/lib/error.ts";
33
import { logProgress } from "../../core/log.ts";
34
35
export interface NotebookPreview {
36
title: string;
37
href: string;
38
filename?: string;
39
supporting?: string[];
40
resources?: string[];
41
order?: number;
42
}
43
44
export interface NotebookPreviewTask {
45
input: string;
46
nbPath: string;
47
title?: string;
48
nbPreviewFile?: string;
49
order?: number;
50
callback?: (nbPreview: NotebookPreview) => void;
51
}
52
53
export const notebookPreviewer = (
54
nbView: boolean | NotebookPreviewDescriptor | NotebookPreviewDescriptor[],
55
format: Format,
56
services: RenderServices,
57
project: ProjectContext,
58
) => {
59
const isBook = projectIsBook(project);
60
const previewQueue: NotebookPreviewTask[] = [];
61
const outputDir = project?.config?.project["output-dir"];
62
63
const nbDescriptors: Record<string, NotebookPreviewDescriptor> = {};
64
if (nbView) {
65
if (typeof nbView !== "boolean") {
66
asArray(nbView).forEach((view) => {
67
const existingView = nbDescriptors[view.notebook];
68
nbDescriptors[view.notebook] = {
69
...existingView,
70
...view,
71
};
72
});
73
}
74
}
75
const descriptor = (
76
notebook: string,
77
) => {
78
return nbDescriptors[notebook];
79
};
80
81
const enQueuePreview = (
82
input: string,
83
nbAbsPath: string,
84
title?: string,
85
order?: number,
86
callback?: (nbPreview: NotebookPreview) => void,
87
) => {
88
if (
89
!previewQueue.find((work) => {
90
return work.nbPath === nbAbsPath;
91
})
92
) {
93
// Try to provide a title
94
previewQueue.push({
95
input,
96
nbPath: nbAbsPath,
97
title: title,
98
callback,
99
order,
100
});
101
}
102
};
103
104
const renderPreviews = async (output?: string, quiet?: boolean) => {
105
const rendered: Record<string, NotebookPreview> = {};
106
107
const nbOptions = format
108
.metadata[kNotebookPreviewOptions] as NotebookPreviewOptions;
109
110
const notebookPaths = previewQueue.map((work) => (work.nbPath));
111
const uniquePaths = ld.uniq(notebookPaths) as string[];
112
const toRenderPaths = uniquePaths.filter((nbPath) => {
113
return services.notebook.get(nbPath, project) === undefined;
114
});
115
const haveRenderedPaths: string[] = [];
116
if (toRenderPaths.length > 0 && !quiet) {
117
logProgress(
118
`Rendering notebook previews`,
119
);
120
}
121
const total = previewQueue.length;
122
let renderCount = 0;
123
for (let i = 0; i < total; i++) {
124
const work = previewQueue[i];
125
const { nbPath, input, title } = work;
126
if (
127
toRenderPaths.includes(nbPath) && !haveRenderedPaths.includes(nbPath) &&
128
!quiet
129
) {
130
logProgress(
131
`[${++renderCount}/${toRenderPaths.length}] ${basename(nbPath)}`,
132
);
133
haveRenderedPaths.push(nbPath);
134
}
135
136
const nbDir = dirname(nbPath);
137
const filename = basename(nbPath);
138
const inputDir = dirname(input);
139
140
if (nbView !== false) {
141
// Read options for this notebook
142
const descriptor: NotebookPreviewDescriptor | undefined =
143
nbDescriptors[relative(dirname(input), nbPath)];
144
const nbAbsPath = isAbsolute(nbPath) ? nbPath : join(inputDir, nbPath);
145
const nbContext = services.notebook;
146
const notebook = nbContext.get(nbAbsPath, project);
147
148
const resolvedTitle = descriptor?.title || title ||
149
notebook?.metadata?.title || basename(nbAbsPath);
150
151
const notebookIsQmd = !nbAbsPath.endsWith(".ipynb");
152
153
// Ensure this has an rendered ipynb and an html preview
154
if (!notebook || !notebook[kHtmlPreview] || !notebook[kRenderedIPynb]) {
155
// Render an ipynb if needed
156
if (
157
(!notebook || !notebook[kRenderedIPynb]) &&
158
!descriptor?.[kDownloadUrl] &&
159
!isBook &&
160
!notebookIsQmd
161
) {
162
const renderedIpynb = await nbContext.render(
163
nbAbsPath,
164
format,
165
kRenderedIPynb,
166
services,
167
{
168
title: resolvedTitle,
169
filename: basename(nbAbsPath),
170
},
171
project,
172
);
173
if (renderedIpynb && (!project || !outputDir)) {
174
nbContext.preserve(nbAbsPath, kRenderedIPynb);
175
}
176
}
177
178
// Render the HTML preview, if needed
179
if (!notebook || !notebook[kHtmlPreview]) {
180
const backHref = nbOptions && nbOptions.back && output
181
? relative(dirname(nbAbsPath), output)
182
: undefined;
183
184
let downloadHref = basename(nbAbsPath);
185
let downloadFileName = basename(nbAbsPath);
186
// If this is an ipynb and there is a rendered version of it
187
// use that instead.
188
if (
189
notebook && notebook[kRenderedIPynb] &&
190
!notebookIsQmd
191
) {
192
downloadHref = relative(
193
dirname(nbAbsPath),
194
notebook[kRenderedIPynb].hrefPath,
195
);
196
downloadFileName = basename(notebook[kRenderedIPynb].hrefPath);
197
}
198
199
const htmlPreview = await nbContext.render(
200
nbAbsPath,
201
format,
202
kHtmlPreview,
203
services,
204
{
205
title: resolvedTitle,
206
filename: basename(nbAbsPath),
207
backHref,
208
downloadHref,
209
downloadFile: downloadFileName,
210
},
211
project,
212
);
213
if (htmlPreview && (!project || !outputDir)) {
214
nbContext.preserve(nbAbsPath, kHtmlPreview);
215
}
216
}
217
}
218
219
const renderedNotebook = nbContext.get(nbAbsPath, project);
220
if (!renderedNotebook || !renderedNotebook[kHtmlPreview]) {
221
throw new InternalError(
222
"We just ensured that notebooks had rendered previews, but the preview then didn't exist.",
223
);
224
}
225
226
// Forward along resources and supporting files from previews
227
const supporting: string[] = [];
228
const resources: string[] = [];
229
if (renderedNotebook[kRenderedIPynb]) {
230
const renderedIpynb = renderedNotebook[kRenderedIPynb];
231
if (renderedIpynb) {
232
if (project) {
233
supporting.push(
234
relative(project.dir, renderedIpynb.hrefPath),
235
);
236
} else {
237
supporting.push(renderedIpynb.hrefPath);
238
}
239
supporting.push(...renderedIpynb.supporting);
240
resources.push(...renderedIpynb.resourceFiles.files);
241
}
242
}
243
244
if (renderedNotebook[kHtmlPreview]) {
245
const htmlPreview = renderedNotebook[kHtmlPreview];
246
if (htmlPreview) {
247
if (project) {
248
supporting.push(relative(project.dir, htmlPreview.hrefPath));
249
} else {
250
supporting.push(htmlPreview.hrefPath);
251
}
252
supporting.push(...htmlPreview.supporting);
253
resources.push(...htmlPreview.resourceFiles.files);
254
}
255
}
256
257
// Compute the final preview information that will be used
258
// to form links to this notebook
259
const nbPreview = {
260
title: resolvedTitle,
261
href: descriptor?.url ||
262
relative(inputDir, renderedNotebook[kHtmlPreview].hrefPath),
263
supporting,
264
resources,
265
order: work.order,
266
};
267
rendered[work.nbPath] = nbPreview;
268
if (work.callback) {
269
work.callback(nbPreview);
270
}
271
} else {
272
const nbPreview = {
273
href: pathWithForwardSlashes(join(nbDir, filename)),
274
title: title || filename,
275
filename,
276
order: work.order,
277
};
278
rendered[work.nbPath] = nbPreview;
279
if (work.callback) {
280
work.callback(nbPreview);
281
}
282
}
283
}
284
return rendered;
285
};
286
287
return {
288
enQueuePreview,
289
renderPreviews,
290
descriptor,
291
};
292
};
293
294