Skip to content

Commit 161c67c

Browse files
committed
Use placeholders
This ensures that we render the document in the context of any output format (allowing the notebook to provide format specific assets and outputs).
1 parent d18b78e commit 161c67c

File tree

4 files changed

+131
-64
lines changed

4 files changed

+131
-64
lines changed

src/command/render/render.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
withTimingAsync,
5151
} from "../../core/timing.ts";
5252
import { filesDirMediabagDir } from "./render-paths.ts";
53+
import { replaceNotebookPlaceholders } from "../../core/handlers/include-notebook.ts";
5354

5455
export async function renderPandoc(
5556
file: ExecutedFile,
@@ -103,9 +104,18 @@ export async function renderPandoc(
103104
const mediabagDir = filesDirMediabagDir(context.target.source);
104105
ensureDirSync(join(dirname(context.target.source), mediabagDir));
105106

107+
// Process any placeholder for notebooks that have been injected
108+
const markdown = await replaceNotebookPlaceholders(
109+
format.pandoc.to || "html",
110+
context.target.source,
111+
context,
112+
context.options.flags || {},
113+
executeResult.markdown,
114+
);
115+
106116
// pandoc options
107117
const pandocOptions: PandocOptions = {
108-
markdown: executeResult.markdown,
118+
markdown,
109119
source: context.target.source,
110120
output: recipe.output,
111121
keepYaml: recipe.keepYaml,

src/core/handlers/embed.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
} from "../lib/mapped-text.ts";
1515

1616
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
17-
import { jupyterAssets } from "../jupyter/jupyter.ts";
1817

19-
import { notebookMarkdown, parseNotebookPath } from "./include-notebook.ts";
18+
import {
19+
notebookMarkdownPlaceholder,
20+
parseNotebookPath,
21+
} from "./include-notebook.ts";
2022

2123
interface EmbedHandler {
2224
name: string;
@@ -32,43 +34,30 @@ interface EmbedHandler {
3234
const kHandlers: EmbedHandler[] = [
3335
{
3436
name: "Jupyter Notebook Embed",
35-
async handle(filename: string, handlerContext: LanguageCellHandlerContext) {
37+
handle(filename: string, handlerContext: LanguageCellHandlerContext) {
3638
const markdownFragments: EitherString[] = [];
3739

3840
// Resolve the filename into a path
3941
const path = handlerContext.resolvePath(filename);
4042

4143
const notebookAddress = parseNotebookPath(path);
4244
if (notebookAddress) {
43-
const assets = jupyterAssets(
44-
handlerContext.options.context.target.source,
45-
handlerContext.options.context.format.pandoc.to,
46-
);
47-
48-
// Render the notebook markdown and inject it
49-
const markdown = await notebookMarkdown(
50-
notebookAddress,
51-
assets,
52-
handlerContext.options.context,
53-
handlerContext.options.flags,
54-
{
55-
echo: false,
56-
warning: false,
57-
asis: true,
58-
},
59-
);
60-
if (markdown) {
61-
markdownFragments.push(markdown);
62-
}
63-
return {
45+
const placeHolder = notebookMarkdownPlaceholder(path, {
46+
echo: false,
47+
warning: false,
48+
asis: true,
49+
});
50+
51+
markdownFragments.push(placeHolder);
52+
return Promise.resolve({
6453
handled: true,
6554
markdownFragments,
66-
};
55+
});
6756
} else {
68-
return {
57+
return Promise.resolve({
6958
handled: false,
7059
markdownFragments: [],
71-
};
60+
});
7261
}
7362
},
7463
},

src/core/handlers/include-notebook.ts

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { resourcePath } from "../resources.ts";
99

1010
import { languages } from "./base.ts";
1111

12-
import { jupyterFromFile, jupyterToMarkdown } from "../jupyter/jupyter.ts";
12+
import {
13+
jupyterAssets,
14+
jupyterFromFile,
15+
jupyterToMarkdown,
16+
} from "../jupyter/jupyter.ts";
1317

1418
import {
1519
kCellLabel,
@@ -108,24 +112,25 @@ async function getCellOutputs(
108112
};
109113
const optionsKey = options
110114
? Object.keys(options).reduce((key, current) => {
111-
return current + boolkey(key, options[key]);
115+
if (options[key] !== undefined) {
116+
return current + boolkey(key, options[key]!);
117+
} else {
118+
return current;
119+
}
112120
}, "")
113121
: "";
114122
const notebookKey = `${nbAddress.path}-${optionsKey}`;
115123

116-
// TODO: Ensure that we're properly dealing with formats for includes
117-
// (e.g. docx / pdf
118-
119124
const lifetime = getNamedLifetime("render-file");
120-
/*
121125
if (lifetime === undefined) {
122126
throw new Error("Internal Error: named lifetime render-file not found");
123-
}*/
124-
const nbCache = lifetime
125-
? lifetime.get("notebook-cache") as unknown as JupyterNotebookOutputCache
126-
: undefined ||
127-
{};
127+
}
128+
129+
const nbCache =
130+
lifetime.get("notebook-cache") as unknown as JupyterNotebookOutputCache ||
131+
{};
128132

133+
// TODO: Cache is always missing because we never attach
129134
if (!nbCache[notebookKey]) {
130135
// Render the notebook and place it in the cache
131136
// Read and filter notebook
@@ -183,20 +188,98 @@ async function getCellOutputs(
183188
);
184189
nbCache[notebookKey] = { outputs: result.cellOutputs };
185190

191+
const cacheWithLifetime = {
192+
...nbCache,
193+
cleanup: () => {
194+
},
195+
};
196+
197+
lifetime.attach(cacheWithLifetime, "notebook-cache");
186198
// TODO: set this back into the lifetime
187199
}
188200
return nbCache[notebookKey].outputs;
189201
}
190202

191-
export interface JupyterMarkdownOptions extends Record<string, boolean> {
192-
echo: boolean;
193-
warning: boolean;
194-
asis: boolean;
203+
export interface JupyterMarkdownOptions
204+
extends Record<string, boolean | undefined> {
205+
echo?: boolean;
206+
warning?: boolean;
207+
asis?: boolean;
195208
}
209+
196210
const kEcho = "echo";
197211
const kWarning = "warning";
198212
const kOutput = "output";
199213

214+
export function notebookMarkdownPlaceholder(
215+
path: string,
216+
options: JupyterMarkdownOptions,
217+
) {
218+
return `<!-- 12A0366C:${path} | ${optionsToPlaceholder(options)} -->`;
219+
}
220+
221+
const kPlaceholderRegex = /<!-- 12A0366C:(.*?) \| (.*?) -->/;
222+
export async function replaceNotebookPlaceholders(
223+
to: string,
224+
input: string,
225+
context: RenderContext,
226+
flags: RenderFlags,
227+
markdown: string,
228+
) {
229+
let match = kPlaceholderRegex.exec(markdown);
230+
while (match) {
231+
const nbPath = match[1];
232+
const optionPlaceholder = match[2];
233+
const nbOptions = optionPlaceholder
234+
? placeholderToOptions(optionPlaceholder)
235+
: {};
236+
const nbAddress = parseNotebookPath(nbPath);
237+
if (nbAddress) {
238+
const assets = jupyterAssets(
239+
input,
240+
to,
241+
);
242+
243+
// Render the notebook markdown and inject it
244+
const nbMarkdown = await notebookMarkdown(
245+
nbAddress,
246+
assets,
247+
context,
248+
flags,
249+
nbOptions,
250+
);
251+
252+
markdown = markdown.replaceAll(match[0], nbMarkdown);
253+
}
254+
255+
match = kPlaceholderRegex.exec(markdown);
256+
}
257+
kPlaceholderRegex.lastIndex = 0;
258+
return markdown;
259+
}
260+
261+
function optionsToPlaceholder(options: JupyterMarkdownOptions) {
262+
return Object.keys(options).map((key) => {
263+
return `${key}:${String(options[key])}`;
264+
}).join(",");
265+
}
266+
267+
function placeholderToOptions(placeholder: string) {
268+
const parts = placeholder.split(",");
269+
const options: JupyterMarkdownOptions = {};
270+
for (const part of parts) {
271+
const kv = part.split(":");
272+
if (kv.length > 1) {
273+
const key = part.split(":")[0];
274+
const value = part.split(":").slice(1).join(":");
275+
options[key] = Boolean(value);
276+
} else {
277+
throw new Error("Unexpected placeholder for notebook option: " + part);
278+
}
279+
}
280+
return options;
281+
}
282+
200283
export async function notebookMarkdown(
201284
nbAddress: NotebookAddress,
202285
assets: JupyterAssets,

src/core/handlers/include.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import {
1818
import { rangedLines } from "../lib/ranged-text.ts";
1919
import { isBlockShortcode } from "../lib/parse-shortcode.ts";
2020
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
21-
import { jupyterAssets } from "../jupyter/jupyter.ts";
2221

23-
import { notebookMarkdown, parseNotebookPath } from "./include-notebook.ts";
22+
import {
23+
notebookMarkdownPlaceholder,
24+
parseNotebookPath,
25+
} from "./include-notebook.ts";
2426

2527
const includeHandler: LanguageHandler = {
2628
...baseHandler,
@@ -48,27 +50,10 @@ const includeHandler: LanguageHandler = {
4850
);
4951
}
5052

51-
// Handle notebooks directly by extracting the items
52-
// from the notebook
5353
const notebookAddress = parseNotebookPath(path);
5454
if (notebookAddress) {
55-
// This is a notebook include, so read the notebook (including only
56-
// the cells that are specified in the include and include them)
57-
const assets = jupyterAssets(
58-
source,
59-
handlerContext.options.context.format.pandoc.to,
60-
);
61-
62-
// Render the notebook markdown and inject it
63-
const markdown = await notebookMarkdown(
64-
notebookAddress,
65-
assets,
66-
handlerContext.options.context,
67-
handlerContext.options.flags,
68-
);
69-
if (markdown) {
70-
textFragments.push(markdown);
71-
}
55+
const placeHolder = notebookMarkdownPlaceholder(path, {});
56+
textFragments.push(placeHolder);
7257
} else {
7358
let includeSrc;
7459
try {

0 commit comments

Comments
 (0)