Skip to content

Commit da4e21c

Browse files
committed
Filter outputs not inputs
1 parent 353ee0f commit da4e21c

File tree

6 files changed

+105
-132
lines changed

6 files changed

+105
-132
lines changed

src/core/handlers/embed.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ import {
1616
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
1717
import { jupyterAssets } from "../jupyter/jupyter.ts";
1818

19-
import {
20-
notebookForAddress,
21-
notebookMarkdown,
22-
parseNotebookPath,
23-
} from "./include-notebook.ts";
19+
import { notebookMarkdown, parseNotebookPath } from "./include-notebook.ts";
2420
import { JupyterCell } from "../jupyter/types.ts";
2521

2622
interface EmbedHandler {
@@ -45,26 +41,23 @@ const kHandlers: EmbedHandler[] = [
4541

4642
const notebookAddress = parseNotebookPath(path);
4743
if (notebookAddress) {
48-
const nb = notebookForAddress(
49-
notebookAddress,
50-
(cell: JupyterCell) => {
51-
cell.metadata["echo"] = false;
52-
cell.metadata["warning"] = false;
53-
cell.metadata["output"] = "asis";
54-
return cell;
55-
},
56-
);
5744
const assets = jupyterAssets(
5845
handlerContext.options.context.target.source,
5946
handlerContext.options.context.format.pandoc.to,
6047
);
6148

6249
// Render the notebook markdown and inject it
6350
const markdown = await notebookMarkdown(
64-
nb,
51+
notebookAddress,
6552
assets,
6653
handlerContext.options.context,
6754
handlerContext.options.flags,
55+
(cell: JupyterCell) => {
56+
cell.metadata["echo"] = false;
57+
cell.metadata["warning"] = false;
58+
cell.metadata["output"] = "asis";
59+
return cell;
60+
},
6861
);
6962
if (markdown) {
7063
markdownFragments.push(markdown);

src/core/handlers/include-notebook.ts

Lines changed: 44 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@ import { resourcePath } from "../resources.ts";
99

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

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

1814
import {
15+
kCellLabel,
1916
kFigDpi,
2017
kFigFormat,
2118
kFigPos,
@@ -33,13 +30,11 @@ import { RenderContext, RenderFlags } from "../../command/render/types.ts";
3330
import {
3431
JupyterAssets,
3532
JupyterCell,
36-
JupyterNotebook,
33+
JupyterCellOutput,
3734
} from "../jupyter/types.ts";
3835

3936
import { dirname, extname } from "path/mod.ts";
4037

41-
const kLabel = "label";
42-
4338
export interface NotebookAddress {
4439
path: string;
4540
ids?: string[];
@@ -99,60 +94,19 @@ export function parseNotebookPath(path: string): NotebookAddress | undefined {
9994
}
10095
}
10196

102-
export function notebookForAddress(
103-
nbAddress: NotebookAddress,
104-
filter?: (cell: JupyterCell) => JupyterCell,
105-
) {
106-
try {
107-
const nb = jupyterFromFile(nbAddress.path);
108-
109-
if (nbAddress.ids) {
110-
// If cellIds are present, filter the notebook to only include
111-
// those cells (cellIds can eiher be an explicitly set cellId, a label in the
112-
// cell metadata, or a tag on a cell that matches an id)
113-
const theCells = nbAddress.ids.map((id) => {
114-
const cell = cellForId(id, nb);
115-
if (cell === undefined) {
116-
throw new Error(
117-
`The cell ${id} does not exist in notebook`,
118-
);
119-
} else {
120-
return cell;
121-
}
122-
});
123-
nb.cells = theCells;
124-
} else if (nbAddress.indexes) {
125-
// Filter and sort based upon cell index
126-
nb.cells = nbAddress.indexes.map((idx) => {
127-
if (idx < 0 || idx >= nb.cells.length) {
128-
throw new Error(
129-
`The cell index ${idx} isn't within the range of cells`,
130-
);
131-
}
132-
return nb.cells[idx];
133-
});
134-
}
135-
136-
// If there is a cell filter, apply it
137-
if (filter) {
138-
nb.cells = nb.cells.map(filter);
139-
}
140-
141-
return nb;
142-
} catch (ex) {
143-
throw new Error(
144-
`Failed to read notebook ${nbAddress.path}\n${ex.message || ""}`,
145-
ex,
146-
);
147-
}
148-
}
149-
15097
export async function notebookMarkdown(
151-
notebook: JupyterNotebook,
98+
nbAddress: NotebookAddress,
15299
assets: JupyterAssets,
153100
context: RenderContext,
154101
flags: RenderFlags,
102+
filter?: (cell: JupyterCell) => JupyterCell,
155103
) {
104+
// Read and filter notebook
105+
const notebook = jupyterFromFile(nbAddress.path);
106+
if (filter) {
107+
notebook.cells = notebook.cells.map(filter);
108+
}
109+
156110
const format = context.format;
157111
const executeOptions = {
158112
target: context.target,
@@ -187,36 +141,56 @@ export async function notebookMarkdown(
187141
figPos: format.render[kFigPos],
188142
},
189143
);
190-
if (result) {
191-
return result.markdown;
144+
145+
if (nbAddress.ids) {
146+
// If cellIds are present, filter the notebook to only include
147+
// those cells (cellIds can eiher be an explicitly set cellId, a label in the
148+
// cell metadata, or a tag on a cell that matches an id)
149+
const theCells = nbAddress.ids.map((id) => {
150+
const cell = cellForId(id, result.cellOutputs);
151+
if (cell === undefined) {
152+
throw new Error(
153+
`The cell ${id} does not exist in notebook`,
154+
);
155+
} else {
156+
return cell;
157+
}
158+
});
159+
return theCells.map((cell) => cell.markdown).join("");
160+
} else if (nbAddress.indexes) {
161+
// Filter and sort based upon cell index
162+
const theCells = nbAddress.indexes.map((idx) => {
163+
if (idx < 0 || idx >= result.cellOutputs.length) {
164+
throw new Error(
165+
`The cell index ${idx} isn't within the range of cells`,
166+
);
167+
}
168+
return result.cellOutputs[idx];
169+
});
170+
return theCells.map((cell) => cell.markdown).join("");
192171
} else {
193-
return undefined;
172+
return result.cellOutputs.map((cell) => cell.markdown).join("");
194173
}
195174
}
196175

197-
function cellForId(id: string, nb: JupyterNotebook) {
198-
for (const cell of nb.cells) {
176+
function cellForId(id: string, cells: JupyterCellOutput[]) {
177+
for (const cell of cells) {
199178
// cellId can either by a literal cell Id, or a tag with that value
200179
const hasId = cell.id ? id === cell.id : false;
201180
if (hasId) {
202181
// It's an ID
203182
return cell;
204183
} else {
205-
// Check for label in options
206-
const cellWithOptions = jupyterCellWithOptions(
207-
nb.metadata.kernelspec.language.toLowerCase(),
208-
cell,
209-
);
210-
const hasLabel = cellWithOptions.options[kLabel]
211-
? id === cellWithOptions.options[kLabel]
184+
const hasLabel = cell.options && cell.options[kCellLabel]
185+
? id === cell.options[kCellLabel]
212186
: false;
213187

214188
if (hasLabel) {
215189
// It matches a label
216190
return cell;
217191
} else {
218192
// Check tags
219-
const hasTag = cell.metadata.tags
193+
const hasTag = cell.metadata && cell.metadata.tags
220194
? cell.metadata.tags.find((tag) => id === tag) !==
221195
undefined
222196
: false;
@@ -228,38 +202,6 @@ function cellForId(id: string, nb: JupyterNotebook) {
228202
}
229203
}
230204

231-
function cellInIdList(ids: string[], cell: JupyterCell, nb: JupyterNotebook) {
232-
// cellId can either by a literal cell Id, or a tag with that value
233-
const hasId = cell.id ? ids.includes(cell.id) : false;
234-
if (hasId) {
235-
// It's an ID
236-
return true;
237-
} else {
238-
// Check for label in options
239-
const cellWithOptions = jupyterCellWithOptions(
240-
nb.metadata.kernelspec.language.toLowerCase(),
241-
cell,
242-
);
243-
const hasLabel = cellWithOptions.options[kLabel]
244-
? ids.includes(cellWithOptions.options[kLabel])
245-
: false;
246-
247-
if (hasLabel) {
248-
// It matches a label
249-
return cell;
250-
} else {
251-
// Check tags
252-
const hasTag = cell.metadata.tags
253-
? cell.metadata.tags.find((tag) => ids.includes(tag)) !==
254-
undefined
255-
: false;
256-
if (hasTag) {
257-
return cell;
258-
}
259-
}
260-
}
261-
}
262-
263205
const resolveCellIds = (hash?: string) => {
264206
if (hash && hash.indexOf(",") > 0) {
265207
return hash.split(",");

src/core/handlers/include.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ import { isBlockShortcode } from "../lib/parse-shortcode.ts";
2020
import { DirectiveCell } from "../lib/break-quarto-md-types.ts";
2121
import { jupyterAssets } from "../jupyter/jupyter.ts";
2222

23-
import {
24-
notebookForAddress,
25-
notebookMarkdown,
26-
parseNotebookPath,
27-
} from "./include-notebook.ts";
23+
import { notebookMarkdown, parseNotebookPath } from "./include-notebook.ts";
2824

2925
const includeHandler: LanguageHandler = {
3026
...baseHandler,
@@ -54,21 +50,18 @@ const includeHandler: LanguageHandler = {
5450

5551
// Handle notebooks directly by extracting the items
5652
// from the notebook
57-
const notebookInclude = parseNotebookPath(path);
58-
if (notebookInclude) {
53+
const notebookAddress = parseNotebookPath(path);
54+
if (notebookAddress) {
5955
// This is a notebook include, so read the notebook (including only
6056
// the cells that are specified in the include and include them)
61-
const nb = notebookForAddress(
62-
notebookInclude,
63-
);
6457
const assets = jupyterAssets(
6558
source,
6659
handlerContext.options.context.format.pandoc.to,
6760
);
6861

6962
// Render the notebook markdown and inject it
7063
const markdown = await notebookMarkdown(
71-
nb,
64+
notebookAddress,
7265
assets,
7366
handlerContext.options.context,
7467
handlerContext.options.flags,

src/core/jupyter/jupyter.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import {
130130
} from "./kernels.ts";
131131
import {
132132
JupyterCell,
133+
JupyterCellOutput,
133134
JupyterCellWithOptions,
134135
JupyterKernelspec,
135136
JupyterNotebook,
@@ -594,6 +595,11 @@ export function jupyterAssets(input: string, to?: string) {
594595
};
595596
}
596597

598+
// Attach fully rendered notebook to render services
599+
// Render notebook only once per document
600+
// Return cells with markdown instead of complete markdown
601+
// filter output markdown cells rather than notebook input
602+
597603
export async function jupyterToMarkdown(
598604
nb: JupyterNotebook,
599605
options: JupyterToMarkdownOptions,
@@ -607,7 +613,7 @@ export async function jupyterToMarkdown(
607613
const htmlPreserve = isHtml ? removeAndPreserveHtml(nb) : undefined;
608614

609615
// generate markdown
610-
const md: string[] = [];
616+
const cellOutputs: JupyterCellOutput[] = [];
611617

612618
// validate unique cell labels as we go
613619
const validateCellLabel = cellLabelValidator();
@@ -616,6 +622,9 @@ export async function jupyterToMarkdown(
616622
let codeCellIndex = 0;
617623

618624
for (let i = 0; i < nb.cells.length; i++) {
625+
// Collection the markdown for this cell
626+
const md: string[] = [];
627+
619628
// convert cell yaml to cell metadata
620629
const cell = jupyterCellWithOptions(
621630
nb.metadata.kernelspec.language.toLowerCase(),
@@ -664,10 +673,19 @@ export async function jupyterToMarkdown(
664673

665674
// newline
666675
md.push("\n");
676+
677+
cellOutputs.push({
678+
id: cell.id,
679+
markdown: md.join(""),
680+
metadata: cell.metadata,
681+
options: cell.options,
682+
});
667683
}
668684

669685
// include jupyter metadata if we are targeting ipynb
686+
let notebookOutputs = undefined;
670687
if (options.toIpynb) {
688+
const md: string[] = [];
671689
md.push("---\n");
672690
const jupyterMetadata = {
673691
jupyter: {
@@ -682,11 +700,15 @@ export async function jupyterToMarkdown(
682700
});
683701
md.push(yamlText);
684702
md.push("---\n");
703+
notebookOutputs = {
704+
suffix: md.join(""),
705+
};
685706
}
686707

687708
// return markdown and any widget requirements
688709
return {
689-
markdown: md.join(""),
710+
cellOutputs,
711+
notebookOutputs,
690712
dependencies,
691713
htmlPreserve,
692714
};

src/core/jupyter/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,22 @@ export interface JupyterToMarkdownOptions {
210210
}
211211

212212
export interface JupyterToMarkdownResult {
213-
markdown: string;
214213
metadata?: Metadata;
215214
pandoc?: FormatPandoc;
216215
dependencies?: JupyterWidgetDependencies;
217216
htmlPreserve?: Record<string, string>;
217+
cellOutputs: JupyterCellOutput[];
218+
notebookOutputs?: {
219+
prefix?: string;
220+
suffix?: string;
221+
};
222+
}
223+
224+
export interface JupyterCellOutput {
225+
id?: string;
226+
options?: JupyterCellOptions;
227+
metadata?: JupyterCellMetadata;
228+
markdown: string;
218229
}
219230

220231
export interface JupyterWidgetsState {

0 commit comments

Comments
 (0)