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.ts
6450 views
1
/*
2
* format-html-notebook.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { Document, Element } from "../../core/deno-dom.ts";
7
import * as ld from "../../core/lodash.ts";
8
9
import {
10
kNotebookLinks,
11
kNotebookView,
12
kNotebookViewStyle,
13
kRelatedNotebooksTitle,
14
kSourceNotebookPrefix,
15
} from "../../config/constants.ts";
16
import { Format } from "../../config/types.ts";
17
18
import {
19
HtmlPostProcessResult,
20
RenderServices,
21
} from "../../command/render/types.ts";
22
23
import { basename, dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
24
import { ProjectContext } from "../../project/types.ts";
25
import {
26
NotebookPreview,
27
notebookPreviewer,
28
} from "./format-html-notebook-preview.ts";
29
import { projectIsBook } from "../../project/project-shared.ts";
30
31
const kQuartoNbClass = "quarto-notebook";
32
const kQuartoCellContainerClass = "cell-container";
33
const kQuartoCellDecoratorClass = "cell-decorator";
34
35
// Post processes the notebook and adds 'notebook' style affordances
36
export function notebookViewPostProcessor() {
37
return (doc: Document): Promise<HtmlPostProcessResult> => {
38
doc.body.classList.add(kQuartoNbClass);
39
const cells = doc.querySelectorAll("div.cell");
40
let cellCount = 0;
41
for (const cell of cells) {
42
const cellEl = cell as Element;
43
const count = cellEl.getAttribute("data-execution_count") || ++cellCount;
44
const isMarkdown = cellEl.classList.contains("markdown");
45
const hasCodeFolding = cellEl.querySelector("details.code-fold") !== null;
46
47
if (!isMarkdown) {
48
const containerNode = doc.createElement("div");
49
containerNode.classList.add(kQuartoCellContainerClass);
50
containerNode.classList.add("column-page-left");
51
if (hasCodeFolding) {
52
containerNode.classList.add("code-fold");
53
}
54
55
const decoratorNode = doc.createElement("div");
56
decoratorNode.classList.add(kQuartoCellDecoratorClass);
57
58
const contentsEl = doc.createElement("pre");
59
contentsEl.appendChild(doc.createTextNode(`In [${count}]:`));
60
decoratorNode.appendChild(contentsEl);
61
62
containerNode.appendChild(decoratorNode);
63
64
const prevSibling = cellEl.previousElementSibling;
65
if (
66
prevSibling &&
67
prevSibling.tagName === "DIV" &&
68
prevSibling.classList.contains("cell-code")
69
) {
70
// If the previous sibling is a cell-code, that is the code cell
71
// for this output and we should grab it too
72
cell.parentElement?.insertBefore(containerNode, cell);
73
74
// Grab the previous element too
75
const wrapperDiv = doc.createElement("div");
76
containerNode.appendChild(wrapperDiv);
77
78
// move the cells
79
wrapperDiv.appendChild(prevSibling.cloneNode(true));
80
prevSibling.remove();
81
82
wrapperDiv.appendChild(cell);
83
} else {
84
cell.parentElement?.insertBefore(containerNode, cell);
85
containerNode.appendChild(cell);
86
}
87
}
88
}
89
90
const resources: string[] = [];
91
const supporting: string[] = [];
92
return Promise.resolve({
93
resources,
94
supporting,
95
});
96
};
97
}
98
99
// Processes embeds within an HTML page and emits notebook previews as apprpriate
100
// Perhaps in render services or elsewhere, we can pass a notebook renderer that will
101
// demand render a notebook (or use an already rendered notebook if it was discovered as a part of
102
// a project and rendered to the correct format)
103
//
104
export async function emplaceNotebookPreviews(
105
input: string,
106
doc: Document,
107
format: Format,
108
services: RenderServices,
109
project: ProjectContext,
110
output?: string,
111
quiet?: boolean,
112
) {
113
// The notebook view configuration data
114
const notebookView = format.render[kNotebookView] ?? true;
115
// The view style for the notebook
116
const notebookViewStyle = format.render[kNotebookViewStyle];
117
118
// Embedded notebooks don't currently resolve notebooks
119
if (notebookViewStyle === "notebook") {
120
return { resources: [], supporting: [] };
121
}
122
123
// Books don't currently support notebook previews
124
const isBook = projectIsBook(project);
125
if (notebookView !== false && !isBook) {
126
// Utilities and settings for dealing with notebook links
127
const inline = format.render[kNotebookLinks] === "inline" ||
128
format.render[kNotebookLinks] === true;
129
const global = format.render[kNotebookLinks] === "global" ||
130
format.render[kNotebookLinks] === true;
131
const addInlineLineNotebookLink = inlineLinkGenerator(doc, format);
132
133
// Helper interface for creating notebook previews
134
const previewer = notebookPreviewer(
135
notebookView,
136
format,
137
services,
138
project,
139
);
140
141
// Process the root document itself, looking for
142
// computational cells provided by this document itself and if
143
// needed, synthesizing a notebook for them
144
// (only do this if this is a root document
145
// and input itself is in the list of notebooks)
146
const inputNbName = basename(input);
147
if (previewer.descriptor(inputNbName)) {
148
const computationalNodes = doc.querySelectorAll("div.cell");
149
for (const computationalNode of computationalNodes) {
150
const computeEl = computationalNode as Element;
151
const cellId = computeEl.getAttribute("id");
152
previewer.enQueuePreview(
153
input,
154
input,
155
undefined, // title
156
undefined, // order
157
(nbPreview) => {
158
// If this is a cell _in_ a source notebook, it will not be parented
159
// by an embed cell
160
if (inline) {
161
if (
162
!computeEl.parentElement?.classList.contains(
163
"quarto-embed-nb-cell",
164
)
165
) {
166
addInlineLineNotebookLink(
167
computeEl,
168
nbPreview,
169
cellId,
170
);
171
}
172
}
173
},
174
);
175
}
176
}
177
178
// For any notebooks explicitly provided, ensure they are rendered
179
if (typeof notebookView !== "boolean") {
180
const nbs = Array.isArray(notebookView) ? notebookView : [notebookView];
181
for (const nb of nbs) {
182
// Filter out the root article notebook, since that was resolved
183
// above.
184
if (nb.url === undefined && inputNbName !== nb.notebook) {
185
const nbAbsPath = isAbsolute(nb.notebook)
186
? nb.notebook
187
: join(dirname(input), nb.notebook);
188
previewer.enQueuePreview(input, nbAbsPath, nb.title, nb.order);
189
}
190
}
191
}
192
193
// Process embedded notebook contents,
194
// emitting links to the notebooks inline (where the embedded content is located)
195
const notebookDivNodes = doc.querySelectorAll("[data-notebook]");
196
for (const nbDivNode of notebookDivNodes) {
197
const nbDivEl = nbDivNode as Element;
198
const notebookPath = nbDivEl.getAttribute("data-notebook");
199
nbDivEl.removeAttribute("data-notebook");
200
201
const notebookCellId = nbDivEl.getAttribute("data-notebook-cellId");
202
nbDivEl.removeAttribute("data-notebook-cellId");
203
204
const title = nbDivEl.getAttribute("data-notebook-title");
205
nbDivEl.removeAttribute("data-notebook-title");
206
207
if (notebookPath) {
208
previewer.enQueuePreview(
209
input,
210
nbAbsPath(input, notebookPath),
211
title === null ? undefined : title,
212
undefined, // order
213
(nbPreview) => {
214
// Add a decoration to this div node
215
if (inline) {
216
addInlineLineNotebookLink(nbDivEl, nbPreview, notebookCellId);
217
}
218
},
219
);
220
}
221
}
222
223
// Render the notebook previews
224
const previews = await previewer.renderPreviews(output, quiet);
225
226
// Get the preview notebooks in the correct order
227
const previewNotebooks = Object.values(previews).sort((a, b) => {
228
if (a.order !== undefined && b.order !== undefined) {
229
return a.order - b.order;
230
} else if (a.order !== undefined && b.order === undefined) {
231
return -1;
232
} else if (a.order === undefined && b.order !== undefined) {
233
return 1;
234
} else {
235
return a.title.localeCompare(b.title);
236
}
237
});
238
239
// Emit global links to the notebooks
240
if (global && previewNotebooks.length > 0) {
241
const containerEl = doc.createElement("div");
242
containerEl.classList.add("quarto-alternate-notebooks");
243
244
const heading = doc.createElement("h2");
245
if (format.language[kRelatedNotebooksTitle]) {
246
heading.innerText = format.language[kRelatedNotebooksTitle];
247
}
248
containerEl.appendChild(heading);
249
250
const formatList = doc.createElement("ul");
251
containerEl.appendChild(formatList);
252
ld.uniqBy(
253
previewNotebooks,
254
(nbPath: { href: string; title?: string }) => {
255
return nbPath.href;
256
},
257
).forEach((nbPath: NotebookPreview) => {
258
const li = doc.createElement("li");
259
260
const link = doc.createElement("a");
261
link.setAttribute("href", nbPath.href);
262
if (nbPath.filename) {
263
link.setAttribute("download", nbPath.filename);
264
}
265
266
const icon = doc.createElement("i");
267
icon.classList.add("bi");
268
icon.classList.add(`bi-journal-code`);
269
link.appendChild(icon);
270
link.appendChild(
271
doc.createTextNode(nbPath.title),
272
);
273
274
li.appendChild(link);
275
formatList.appendChild(li);
276
});
277
let dlLinkTarget = doc.querySelector(`nav[role="doc-toc"]`);
278
if (dlLinkTarget === null) {
279
dlLinkTarget = doc.querySelector("#quarto-margin-sidebar");
280
}
281
282
if (dlLinkTarget) {
283
dlLinkTarget.appendChild(containerEl);
284
}
285
}
286
287
const supporting: string[] = [];
288
const resources: string[] = [];
289
for (const notebookPath of Object.keys(previews)) {
290
const nbPath = previews[notebookPath];
291
// If there is a view configured for this, then
292
// include it in the supporting dir
293
if (nbPath.supporting) {
294
supporting.push(...nbPath.supporting);
295
}
296
297
if (nbPath.resources) {
298
resources.push(...nbPath.resources.map((file) => {
299
return project ? relative(project?.dir, file) : file;
300
}));
301
}
302
303
// This is the notebook itself
304
resources.push(notebookPath);
305
}
306
307
return {
308
resources,
309
supporting,
310
};
311
}
312
}
313
314
const nbAbsPath = (input: string, nbPath: string) => {
315
if (isAbsolute(nbPath)) {
316
return nbPath;
317
}
318
319
// Ensure that the input path is absolute
320
const inputAbsPath = isAbsolute(input) ? input : join(Deno.cwd(), input);
321
322
// Ensure that the notebook path is absolute
323
return join(dirname(inputAbsPath), nbPath);
324
};
325
326
const inlineLinkGenerator = (doc: Document, format: Format) => {
327
let count = 1;
328
return (
329
nbDivEl: Element,
330
nbPreview: NotebookPreview,
331
cellId?: string | null,
332
) => {
333
const id = "nblink-" + count++;
334
335
const nbLinkEl = doc.createElement("a");
336
nbLinkEl.classList.add("quarto-notebook-link");
337
nbLinkEl.setAttribute("id", `${id}`);
338
339
if (nbPreview.filename) {
340
nbLinkEl.setAttribute("download", nbPreview.filename);
341
nbLinkEl.setAttribute("href", nbPreview.href);
342
} else {
343
if (cellId) {
344
nbLinkEl.setAttribute(
345
"href",
346
`${nbPreview.href}#${cellId}`,
347
);
348
} else {
349
nbLinkEl.setAttribute("href", `${nbPreview.href}`);
350
}
351
}
352
nbLinkEl.appendChild(
353
doc.createTextNode(
354
`${format.language[kSourceNotebookPrefix]}: ${nbPreview.title}`,
355
),
356
);
357
358
// If there is a figure caption, place the source after that
359
// otherwise just place it at the bottom of the notebook div
360
const nbParentEl = nbDivEl.parentElement;
361
if (nbParentEl?.tagName.toLocaleLowerCase() === "figure") {
362
const figCapEl = nbDivEl.parentElement?.querySelector("figcaption");
363
if (figCapEl) {
364
figCapEl.after(nbLinkEl);
365
} else {
366
nbDivEl.appendChild(nbLinkEl);
367
}
368
} else {
369
nbDivEl.appendChild(nbLinkEl);
370
}
371
};
372
};
373
374