Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/render/notebook/notebook-contributor-html.ts
6458 views
1
/*
2
* notebook-contributor-html.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { renderFile } from "../../command/render/render-files.ts";
8
import {
9
ExecutedFile,
10
RenderedFile,
11
RenderServices,
12
} from "../../command/render/types.ts";
13
import {
14
kClearCellOptions,
15
kClearHiddenClasses,
16
kDisableArticleLayout,
17
kFormatLinks,
18
kIpynbProduceSourceNotebook,
19
kKeepHidden,
20
kNotebookPreserveCells,
21
kNotebookPreviewBack,
22
kNotebookPreviewDownload,
23
kNotebookPreviewDownloadSrc,
24
kNotebookViewStyle,
25
kOutputFile,
26
kRemoveHidden,
27
kTemplate,
28
kTheme,
29
kTo,
30
kToc,
31
kTocLocation,
32
kUnrollMarkdownCells,
33
} from "../../config/constants.ts";
34
import { InternalError } from "../../core/lib/error.ts";
35
import { ProjectContext } from "../../project/types.ts";
36
import {
37
NotebookContributor,
38
NotebookMetadata,
39
NotebookTemplateMetadata,
40
} from "./notebook-types.ts";
41
42
import { error } from "../../deno_ral/log.ts";
43
import { formatResourcePath } from "../../core/resources.ts";
44
import { kNotebookViewStyleNotebook } from "../../format/html/format-html-constants.ts";
45
import { kAppendixStyle } from "../../format/html/format-html-shared.ts";
46
import { basename, dirname, join, relative } from "../../deno_ral/path.ts";
47
import { Format } from "../../config/types.ts";
48
import { dirAndStem, isQmdFile } from "../../core/path.ts";
49
import { projectOutputDir } from "../../project/project-shared.ts";
50
import { existsSync } from "../../deno_ral/fs.ts";
51
import { safeCloneDeep } from "../../core/safe-clone-deep.ts";
52
53
export const htmlNotebookContributor: NotebookContributor = {
54
resolve: resolveHtmlNotebook,
55
render: renderHtmlNotebook,
56
outputFile,
57
cachedPath,
58
};
59
60
export function outputFile(
61
nbAbsPath: string,
62
): string {
63
const [_dir, stem] = dirAndStem(basename(nbAbsPath));
64
return `${stem}-preview.html`;
65
}
66
67
function cachedPath(nbAbsPath: string, project?: ProjectContext) {
68
if (project) {
69
const nbRelative = relative(project.dir, dirname(nbAbsPath));
70
const nbOutputDir = join(projectOutputDir(project), nbRelative);
71
72
const outFile = outputFile(nbAbsPath);
73
const outPath = join(nbOutputDir, outFile);
74
if (existsSync(outPath)) {
75
return outPath;
76
}
77
}
78
}
79
80
function resolveHtmlNotebook(
81
nbAbsPath: string,
82
_token: string,
83
executedFile: ExecutedFile,
84
notebookMetadata?: NotebookMetadata,
85
) {
86
const resolved = safeCloneDeep(executedFile);
87
88
// Set the output file
89
resolved.recipe.format.pandoc[kOutputFile] = `${outputFile(nbAbsPath)}`;
90
resolved.recipe.output = resolved.recipe.format.pandoc[kOutputFile];
91
92
// Configure echo for this rendering to ensure there is output
93
// that we can manually control
94
resolved.recipe.format.execute.echo = false;
95
resolved.recipe.format.execute.warning = false;
96
resolved.recipe.format.render[kKeepHidden] = true;
97
resolved.recipe.format.metadata[kClearHiddenClasses] = "all";
98
resolved.recipe.format.metadata[kRemoveHidden] = "none";
99
100
// If this recipe is using a a source notebook, clear the cell options
101
// from the output when rendering
102
if (resolved.recipe.format.render[kIpynbProduceSourceNotebook]) {
103
resolved.recipe.format.render[kClearCellOptions] = true;
104
}
105
106
// Use the special `embed/notebook` template for this render
107
const template = formatResourcePath(
108
"html",
109
join("embed", "template.html"),
110
);
111
resolved.recipe.format.pandoc[kTemplate] = template;
112
113
// Metadata used by template when rendering
114
resolved.recipe.format.metadata["nbMeta"] = {
115
...notebookMetadata,
116
downloadLabel: downloadLabel(
117
notebookMetadata?.filename || nbAbsPath,
118
resolved.recipe.format,
119
),
120
backLabel: resolved.recipe.format.language[kNotebookPreviewBack],
121
} as NotebookTemplateMetadata;
122
123
// Configure the notebook style
124
resolved.recipe.format.render[kNotebookViewStyle] =
125
kNotebookViewStyleNotebook;
126
resolved.recipe.format.render[kNotebookPreserveCells] = true;
127
resolved.recipe.format.metadata[kUnrollMarkdownCells] = false;
128
129
// Configure the appearance
130
resolved.recipe.format.pandoc[kToc] = true;
131
resolved.recipe.format.metadata[kTocLocation] = "left";
132
resolved.recipe.format.metadata[kAppendixStyle] = "none";
133
resolved.recipe.format.render[kFormatLinks] = false;
134
135
resolved.recipe.format.metadata[kDisableArticleLayout] = true;
136
137
return resolved;
138
}
139
140
async function renderHtmlNotebook(
141
nbPath: string,
142
format: Format,
143
_subArticleToken: string,
144
services: RenderServices,
145
notebookMetadata: NotebookMetadata | undefined,
146
project: ProjectContext,
147
): Promise<RenderedFile> {
148
// Use the special `embed` template for this render
149
const template = formatResourcePath(
150
"html",
151
join("embed", "template.html"),
152
);
153
154
// Render the notebook and update the path
155
const rendered = await renderFile(
156
{ path: nbPath, formats: ["html"] },
157
{
158
services,
159
flags: {
160
metadata: {
161
[kTo]: "html",
162
[kTheme]: format.metadata[kTheme],
163
[kOutputFile]: `${outputFile(nbPath)}`,
164
[kTemplate]: template,
165
[kNotebookViewStyle]: kNotebookViewStyleNotebook,
166
[kAppendixStyle]: "none",
167
[kNotebookPreserveCells]: true,
168
["nbMeta"]: {
169
...notebookMetadata,
170
downloadLabel: downloadLabel(
171
notebookMetadata?.filename || nbPath,
172
format,
173
),
174
backLabel: format.language[kNotebookPreviewBack],
175
} as NotebookTemplateMetadata,
176
[kToc]: true,
177
[kTocLocation]: "left",
178
[kDisableArticleLayout]: true,
179
},
180
quiet: false,
181
},
182
echo: true,
183
warning: true,
184
quietPandoc: true,
185
},
186
services,
187
project,
188
);
189
190
// An error occurred rendering this subarticle
191
if (rendered.error) {
192
error("Rendering of output notebook produced an unexpected result");
193
throw (rendered.error);
194
}
195
196
// There should be only one file
197
if (rendered.files.length !== 1) {
198
throw new InternalError(
199
`Rendering an output notebook should only result in a single file. This attempt resulted in ${rendered.files.length} file(s).`,
200
);
201
}
202
203
return rendered.files[0];
204
}
205
206
function downloadLabel(file: string, format: Format) {
207
return isQmdFile(file)
208
? format.language[kNotebookPreviewDownloadSrc]
209
: format.language[kNotebookPreviewDownload];
210
}
211
212