Skip to content

Commit 353ee0f

Browse files
committed
Support ids and indexes, ordered
This is broken though, since it will cause dependency collision
1 parent 45009b3 commit 353ee0f

File tree

1 file changed

+173
-62
lines changed

1 file changed

+173
-62
lines changed

src/core/handlers/include-notebook.ts

Lines changed: 173 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -38,103 +38,112 @@ import {
3838

3939
import { dirname, extname } from "path/mod.ts";
4040

41+
const kLabel = "label";
42+
4143
export 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-
83102
export 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

Comments
 (0)