@@ -38,103 +38,112 @@ import {
3838
3939import { dirname , extname } from "path/mod.ts" ;
4040
41+ const kLabel = "label" ;
42+
4143export interface NotebookAddress {
4244 path : string ;
43- cellIds : string [ ] | undefined ;
44- params : Record < string , string > ;
45+ ids ? : string [ ] ;
46+ indexes ?: number [ ] ;
4547}
4648
47- const resolveCellIds = ( hash ?: string ) => {
48- if ( hash && hash . indexOf ( "," ) > 0 ) {
49- return hash . split ( "," ) ;
50- } else {
51- return hash ;
52- }
53- } ;
49+ const kHashRegex = / ( .* ?) # ( .* ) / ;
50+ const kIndexRegex = / ( .* ) \[ ( [ 0 - 9 , - ] * ) \] / ;
5451
55- // tag specified in yaml
56- // label in yaml
5752// notebook.ipynb#cellid1
5853// notebook.ipynb#cellid1
5954// notebook.ipynb#cellid1,cellid2,cellid3
6055// notebook.ipynb[0]
6156// notebook.ipynb[0,1]
6257// notebook.ipynb[0-2]
58+ // notebook.ipynb[2,0-1]
59+ export function parseNotebookPath ( path : string ) : NotebookAddress | undefined {
60+ const isNotebook = ( path : string ) => {
61+ return extname ( path ) === ".ipynb" ;
62+ } ;
6363
64- // If the path is a notebook path, then process it separately.
65- export function parseNotebookPath ( path : string ) {
66- const hasHash = path . indexOf ( "#" ) !== - 1 ;
67- const hash = hasHash ? path . split ( "#" ) [ 1 ] : undefined ;
68- path = path . split ( "#" ) [ 0 ] ;
64+ // This is a hash based path
65+ const hashResult = path . match ( kHashRegex ) ;
66+ if ( hashResult ) {
67+ const path = hashResult [ 1 ] ;
68+ if ( isNotebook ( path ) ) {
69+ return {
70+ path,
71+ ids : resolveCellIds ( hashResult [ 2 ] ) ,
72+ } ;
73+ } else {
74+ return undefined ;
75+ }
76+ }
6977
70- if ( extname ( path ) === ".ipynb" ) {
71- const cellIds = resolveCellIds ( hash ) ;
78+ // This is an index based path
79+ const indexResult = path . match ( kIndexRegex ) ;
80+ if ( indexResult ) {
81+ const path = indexResult [ 1 ] ;
82+ if ( isNotebook ( path ) ) {
83+ return {
84+ path,
85+ indexes : resolveCellRange ( indexResult [ 2 ] ) ,
86+ } ;
87+ } else {
88+ return undefined ;
89+ }
90+ }
91+
92+ // This is the path to a notebook
93+ if ( isNotebook ( path ) ) {
7294 return {
7395 path,
74- cellIds,
75- } as NotebookAddress ;
96+ } ;
7697 } else {
7798 return undefined ;
7899 }
79100}
80101
81- const kLabel = "label" ;
82-
83102export function notebookForAddress (
84- nbInclude : NotebookAddress ,
103+ nbAddress : NotebookAddress ,
85104 filter ?: ( cell : JupyterCell ) => JupyterCell ,
86105) {
87106 try {
88- const nb = jupyterFromFile ( nbInclude . path ) ;
89- const cells : JupyterCell [ ] = [ ] ;
90-
91- // If cellIds are present, filter the notebook to only include
92- // those cells (cellIds can eiher be an explicitly set cellId, a label in the
93- // cell metadata, or a tag on a cell that matches an id)
94- if ( nbInclude . cellIds ) {
95- for ( const cell of nb . cells ) {
96- // cellId can either by a literal cell Id, or a tag with that value
97- const hasId = cell . id ? nbInclude . cellIds . includes ( cell . id ) : false ;
98- if ( hasId ) {
99- // It's an ID
100- cells . push ( cell ) ;
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+ ) ;
101119 } else {
102- // Check for label in options
103- const cellWithOptions = jupyterCellWithOptions (
104- nb . metadata . kernelspec . language . toLowerCase ( ) ,
105- cell ,
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` ,
106130 ) ;
107- const hasLabel = cellWithOptions . options [ kLabel ]
108- ? nbInclude . cellIds . includes ( cellWithOptions . options [ kLabel ] )
109- : false ;
110-
111- if ( hasLabel ) {
112- // It matches a label
113- cells . push ( cell ) ;
114- } else {
115- // Check tags
116- const hasTag = cell . metadata . tags
117- ? cell . metadata . tags . find ( ( tag ) =>
118- nbInclude . cellIds ?. includes ( tag )
119- ) !==
120- undefined
121- : false ;
122- if ( hasTag ) {
123- cells . push ( cell ) ;
124- }
125- }
126131 }
127- }
128- nb . cells = cells ;
132+ return nb . cells [ idx ] ;
133+ } ) ;
129134 }
130135
136+ // If there is a cell filter, apply it
131137 if ( filter ) {
132138 nb . cells = nb . cells . map ( filter ) ;
133139 }
134140
135141 return nb ;
136142 } catch ( ex ) {
137- throw new Error ( `Failed to read included notebook ${ nbInclude . path } ` , ex ) ;
143+ throw new Error (
144+ `Failed to read notebook ${ nbAddress . path } \n${ ex . message || "" } ` ,
145+ ex ,
146+ ) ;
138147 }
139148}
140149
@@ -184,3 +193,105 @@ export async function notebookMarkdown(
184193 return undefined ;
185194 }
186195}
196+
197+ function cellForId ( id : string , nb : JupyterNotebook ) {
198+ for ( const cell of nb . cells ) {
199+ // cellId can either by a literal cell Id, or a tag with that value
200+ const hasId = cell . id ? id === cell . id : false ;
201+ if ( hasId ) {
202+ // It's an ID
203+ return cell ;
204+ } 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 ]
212+ : false ;
213+
214+ if ( hasLabel ) {
215+ // It matches a label
216+ return cell ;
217+ } else {
218+ // Check tags
219+ const hasTag = cell . metadata . tags
220+ ? cell . metadata . tags . find ( ( tag ) => id === tag ) !==
221+ undefined
222+ : false ;
223+ if ( hasTag ) {
224+ return cell ;
225+ }
226+ }
227+ }
228+ }
229+ }
230+
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+
263+ const resolveCellIds = ( hash ?: string ) => {
264+ if ( hash && hash . indexOf ( "," ) > 0 ) {
265+ return hash . split ( "," ) ;
266+ } else if ( hash ) {
267+ return [ hash ] ;
268+ } else {
269+ return undefined ;
270+ }
271+ } ;
272+
273+ const resolveCellRange = ( rangeRaw ?: string ) => {
274+ if ( rangeRaw ) {
275+ const result : number [ ] = [ ] ;
276+
277+ const ranges = rangeRaw . split ( "," ) ;
278+ ranges . forEach ( ( range ) => {
279+ if ( range . indexOf ( "-" ) > - 1 ) {
280+ // This is range
281+ const parts = range . split ( "-" ) ;
282+ const start = parseInt ( parts [ 0 ] ) ;
283+ const end = parseInt ( parts [ 1 ] ) ;
284+ const step = start > end ? - 1 : 1 ;
285+ for ( let i = start ; step > 0 ? i <= end : i >= end ; i = i + step ) {
286+ result . push ( i ) ;
287+ }
288+ } else {
289+ // This is raw value
290+ result . push ( parseInt ( range ) ) ;
291+ }
292+ } ) ;
293+ return result ;
294+ } else {
295+ return undefined ;
296+ }
297+ } ;
0 commit comments