@@ -12,6 +12,8 @@ import {
1212 jupyterAssets ,
1313 jupyterFromFile ,
1414 jupyterToMarkdown ,
15+ kQuartoOutputDisplay ,
16+ kQuartoOutputOrder ,
1517} from "../jupyter/jupyter.ts" ;
1618
1719import {
@@ -69,18 +71,13 @@ const kOutput = "output";
6971
7072const kHashRegex = / ( .* ?) # ( .* ) / ;
7173const kIndexRegex = / ( .* ) \[ ( [ 0 - 9 , - ] * ) \] / ;
72- const kPlaceholderRegex = / < ! - - 1 2 A 0 3 6 6 C : ( .* ?) \| ( .* ?) - - > / ;
74+ const kPlaceholderRegex = / < ! - - 1 2 A 0 3 6 6 C : ( .* ?) \| ( .* ?) \| ( . * ? ) - - > / ;
7375
7476const kNotebookCache = "notebook-cache" ;
7577const kRenderFileLifeTime = "render-file" ;
7678
77- // notebook.ipynb#cellid1
78- // notebook.ipynb#cellid1
79- // notebook.ipynb#cellid1,cellid2,cellid3
80- // notebook.ipynb[0]
81- // notebook.ipynb[0,1]
82- // notebook.ipynb[0-2]
83- // notebook.ipynb[2,0-1]
79+ // Parses a notebook address string into a file path with
80+ // an optional list of ids or list of cell indexes.
8481export function parseNotebookAddress (
8582 path : string ,
8683) : JupyterNotebookAddress | undefined {
@@ -109,7 +106,7 @@ export function parseNotebookAddress(
109106 if ( isNotebook ( path ) ) {
110107 return {
111108 path,
112- indexes : resolveCellRange ( indexResult [ 2 ] ) ,
109+ indexes : resolveRange ( indexResult [ 2 ] ) ,
113110 } ;
114111 } else {
115112 return undefined ;
@@ -126,13 +123,22 @@ export function parseNotebookAddress(
126123 }
127124}
128125
126+ // Creates a placeholder that will later be replaced with
127+ // rendered notebook markdown. Note that the placeholder
128+ // must stipulate all the information required to generate the
129+ // markdown (e.g. options, output indexes and so on)
129130export function notebookMarkdownPlaceholder (
130131 path : string ,
131132 options : JupyterMarkdownOptions ,
133+ outputs ?: string ,
132134) {
133- return `<!-- 12A0366C:${ path } | ${ optionsToPlaceholder ( options ) } -->` ;
135+ return `<!-- 12A0366C:${ path } | ${ outputs || "" } | ${
136+ optionsToPlaceholder ( options )
137+ } -->`;
134138}
135139
140+ // Replaces any notebook markdown placeholders with the
141+ // rendered contents.
136142export async function replaceNotebookPlaceholders (
137143 to : string ,
138144 input : string ,
@@ -143,22 +149,29 @@ export async function replaceNotebookPlaceholders(
143149 let match = kPlaceholderRegex . exec ( markdown ) ;
144150 let includes ;
145151 while ( match ) {
146- // Find and parse the placeholders
147- const nbPath = match [ 1 ] ;
148- const optionPlaceholder = match [ 2 ] ;
149- const nbOptions = optionPlaceholder
150- ? placeholderToOptions ( optionPlaceholder )
151- : { } ;
152-
153- // Parse the address
154- const nbAddress = parseNotebookAddress ( nbPath ) ;
152+ // Parse the address and if this is a notebook
153+ // then proceed with the replacement
154+ const nbAddressStr = match [ 1 ] ;
155+ const nbAddress = parseNotebookAddress ( nbAddressStr ) ;
155156 if ( nbAddress ) {
157+ // If a list of outputs are provided, resolve that range
158+ const outputsStr = match [ 2 ] ;
159+ const nbOutputs = outputsStr ? resolveRange ( outputsStr ) : undefined ;
160+
161+ // If cell options are provided, resolve those
162+ const placeholderStr = match [ 3 ] ;
163+ const nbOptions = placeholderStr
164+ ? placeholderToOptions ( placeholderStr )
165+ : { } ;
166+
156167 // Assets
157168 const assets = jupyterAssets (
158169 input ,
159170 to ,
160171 ) ;
161172
173+ // Compute appropriate includes based upon the note
174+ // dependendencies
162175 const notebookIncludes = ( ) => {
163176 const notebook = jupyterFromFile ( resolveNbPath ( input , nbAddress . path ) ) ;
164177 const dependencies = isHtmlOutput ( context . format . pandoc )
@@ -183,12 +196,12 @@ export async function replaceNotebookPlaceholders(
183196 context ,
184197 flags ,
185198 nbOptions ,
199+ nbOutputs ,
186200 ) ;
187201
188202 // Replace the placeholders with the rendered markdown
189203 markdown = markdown . replaceAll ( match [ 0 ] , nbMarkdown ) ;
190204 }
191-
192205 match = kPlaceholderRegex . exec ( markdown ) ;
193206 }
194207 kPlaceholderRegex . lastIndex = 0 ;
@@ -206,12 +219,14 @@ function resolveNbPath(input: string, path: string) {
206219 }
207220}
208221
222+ // Gets the markdown for a specific notebook and set of options
209223async function notebookMarkdown (
210224 nbAddress : JupyterNotebookAddress ,
211225 assets : JupyterAssets ,
212226 context : RenderContext ,
213227 flags : RenderFlags ,
214228 options ?: JupyterMarkdownOptions ,
229+ outputs ?: number [ ] ,
215230) {
216231 // Get the cell outputs for this notebook
217232 const notebookInfo = await getCachedNotebookInfo (
@@ -220,6 +235,7 @@ async function notebookMarkdown(
220235 context ,
221236 flags ,
222237 options ,
238+ outputs ,
223239 ) ;
224240
225241 // Wrap any injected cells with a div that includes a back link to
@@ -271,12 +287,17 @@ async function notebookMarkdown(
271287 }
272288}
273289
290+ // Caches the notebook info for a a particular notebook and
291+ // set of options. Since the markdown is what is cached,
292+ // the cache will include options that control markdown output
293+ // when determining whether it can use cached contents.
274294async function getCachedNotebookInfo (
275295 nbAddress : JupyterNotebookAddress ,
276296 assets : JupyterAssets ,
277297 context : RenderContext ,
278298 flags : RenderFlags ,
279299 options ?: JupyterMarkdownOptions ,
300+ outputs ?: number [ ] ,
280301) {
281302 // We can cache outputs on a per rendered file basis to
282303 // improve performance
@@ -293,7 +314,7 @@ async function getCachedNotebookInfo(
293314 } ;
294315
295316 // Compute a cache key
296- const cacheKey = notebookCacheKey ( nbAddress , options ) ;
317+ const cacheKey = notebookCacheKey ( nbAddress , options , outputs ) ;
297318 if ( ! nbCache . cache [ cacheKey ] ) {
298319 // Render the notebook and place it in the cache
299320 // Read and filter notebook
@@ -313,6 +334,23 @@ async function getCachedNotebookInfo(
313334 if ( options . asis !== undefined ) {
314335 cell . metadata [ kOutput ] = options . asis ? true : false ;
315336 }
337+
338+ // Filter outputs if so desired
339+ if ( outputs && cell . outputs ) {
340+ cell . outputs = cell . outputs . map ( ( output , index ) => {
341+ const oneBasedIdx = index + 1 ;
342+ output . metadata = output . metadata || { } ;
343+ if ( ! outputs . includes ( oneBasedIdx ) ) {
344+ output . metadata [ kQuartoOutputDisplay ] = false ;
345+ } else {
346+ const explicitOrder = outputs . indexOf ( oneBasedIdx ) ;
347+ if ( explicitOrder > - 1 ) {
348+ output . metadata [ kQuartoOutputOrder ] = explicitOrder ;
349+ }
350+ }
351+ return output ;
352+ } ) ;
353+ }
316354 return cell ;
317355 } ) ;
318356 }
@@ -363,6 +401,7 @@ async function getCachedNotebookInfo(
363401 return nbCache . cache [ cacheKey ] ;
364402}
365403
404+ // Tries to find a title within a Notebook
366405function findTitle ( cells : JupyterCellOutput [ ] ) {
367406 for ( const cell of cells ) {
368407 const partitioned = partitionMarkdown ( cell . markdown ) ;
@@ -375,9 +414,13 @@ function findTitle(cells: JupyterCellOutput[]) {
375414 return undefined ;
376415}
377416
417+ // Create a notebook hash key for the cache
418+ // that incorporates options that affect markdown
419+ // output
378420function notebookCacheKey (
379421 nbAddress : JupyterNotebookAddress ,
380422 nbOptions ?: JupyterMarkdownOptions ,
423+ nbOutputs ?: number [ ] ,
381424) {
382425 const optionsKey = nbOptions
383426 ? Object . keys ( nbOptions ) . reduce ( ( key , current ) => {
@@ -388,7 +431,13 @@ function notebookCacheKey(
388431 }
389432 } , "" )
390433 : "" ;
391- return optionsKey ? `${ nbAddress . path } -${ optionsKey } ` : nbAddress . path ;
434+
435+ const coreKey = optionsKey
436+ ? `${ nbAddress . path } -${ optionsKey } `
437+ : nbAddress . path ;
438+
439+ const outputsKey = nbOutputs ? nbOutputs . join ( "," ) : "" ;
440+ return `${ coreKey } :${ outputsKey } ` ;
392441}
393442
394443function optionsToPlaceholder ( options : JupyterMarkdownOptions ) {
@@ -413,6 +462,9 @@ function placeholderToOptions(placeholder: string) {
413462 return options ;
414463}
415464
465+ // Finds a cell matching an id. It will first try an explicit
466+ // matching cell Id, then a cell label (in the code chunk front matter),
467+ // and otherwise look for a cell(s) with that tag
416468function cellForId ( id : string , cells : JupyterCellOutput [ ] ) {
417469 for ( const cell of cells ) {
418470 // cellId can either by a literal cell Id, or a tag with that value
@@ -442,6 +494,10 @@ function cellForId(id: string, cells: JupyterCellOutput[]) {
442494 }
443495}
444496
497+ // Parses a string into one or more cellids.
498+ // Syntax like:
499+ // notebook.ipynb#cellid1
500+ // notebook.ipynb#cellid1,cellid2,cellid3
445501function resolveCellIds ( hash ?: string ) {
446502 if ( hash && hash . indexOf ( "," ) > 0 ) {
447503 return hash . split ( "," ) ;
@@ -452,7 +508,15 @@ function resolveCellIds(hash?: string) {
452508 }
453509}
454510
455- function resolveCellRange ( rangeRaw ?: string ) {
511+ // Parses a string with one more numbers or ranges into
512+ // a list of numbers, in order.
513+ // Syntax like:
514+ // notebook.ipynb[0]
515+ // notebook.ipynb[0,1]
516+ // notebook.ipynb[0-2]
517+ // notebook.ipynb[2,0-1]
518+ // notebook.ipynb[2,6-4]
519+ function resolveRange ( rangeRaw ?: string ) {
456520 if ( rangeRaw ) {
457521 const result : number [ ] = [ ] ;
458522
0 commit comments