1 |
|
2 |
|
3 | import { Cell, MarkdownCell } from '@jupyterlab/cells';
|
4 | import { TableOfContentsFactory, TableOfContentsModel, TableOfContentsUtils } from '@jupyterlab/toc';
|
5 | import { NotebookActions } from './actions';
|
6 |
|
7 |
|
8 |
|
9 | export var RunningStatus;
|
10 | (function (RunningStatus) {
|
11 | |
12 |
|
13 |
|
14 | RunningStatus[RunningStatus["Idle"] = -1] = "Idle";
|
15 | |
16 |
|
17 |
|
18 | RunningStatus[RunningStatus["Error"] = -0.5] = "Error";
|
19 | |
20 |
|
21 |
|
22 | RunningStatus[RunningStatus["Scheduled"] = 0] = "Scheduled";
|
23 | |
24 |
|
25 |
|
26 | RunningStatus[RunningStatus["Running"] = 1] = "Running";
|
27 | })(RunningStatus || (RunningStatus = {}));
|
28 |
|
29 |
|
30 |
|
31 | export class NotebookToCModel extends TableOfContentsModel {
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | constructor(widget, parser, sanitizer, configuration) {
|
41 | super(widget, configuration);
|
42 | this.parser = parser;
|
43 | this.sanitizer = sanitizer;
|
44 | |
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 | this.configMetadataMap = {
|
52 | numberHeaders: ['toc-autonumbering', 'toc/number_sections'],
|
53 | numberingH1: ['!toc/skip_h1_title'],
|
54 | baseNumbering: ['toc/base_numbering']
|
55 | };
|
56 | this._runningCells = new Array();
|
57 | this._errorCells = new Array();
|
58 | this._cellToHeadingIndex = new WeakMap();
|
59 | void widget.context.ready.then(() => {
|
60 |
|
61 | this.setConfiguration({});
|
62 | });
|
63 | this.widget.context.model.metadataChanged.connect(this.onMetadataChanged, this);
|
64 | this.widget.content.activeCellChanged.connect(this.onActiveCellChanged, this);
|
65 | NotebookActions.executionScheduled.connect(this.onExecutionScheduled, this);
|
66 | NotebookActions.executed.connect(this.onExecuted, this);
|
67 | NotebookActions.outputCleared.connect(this.onOutputCleared, this);
|
68 | this.headingsChanged.connect(this.onHeadingsChanged, this);
|
69 | }
|
70 | |
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | get documentType() {
|
78 | return 'notebook';
|
79 | }
|
80 | |
81 |
|
82 |
|
83 |
|
84 | get isAlwaysActive() {
|
85 | return true;
|
86 | }
|
87 | |
88 |
|
89 |
|
90 | get supportedOptions() {
|
91 | return [
|
92 | 'baseNumbering',
|
93 | 'maximalDepth',
|
94 | 'numberingH1',
|
95 | 'numberHeaders',
|
96 | 'includeOutput',
|
97 | 'syncCollapseState'
|
98 | ];
|
99 | }
|
100 | |
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | getCellHeadings(cell) {
|
107 | const headings = new Array();
|
108 | let headingIndex = this._cellToHeadingIndex.get(cell);
|
109 | if (headingIndex !== undefined) {
|
110 | const candidate = this.headings[headingIndex];
|
111 | headings.push(candidate);
|
112 | while (this.headings[headingIndex - 1] &&
|
113 | this.headings[headingIndex - 1].cellRef === candidate.cellRef) {
|
114 | headingIndex--;
|
115 | headings.unshift(this.headings[headingIndex]);
|
116 | }
|
117 | }
|
118 | return headings;
|
119 | }
|
120 | |
121 |
|
122 |
|
123 | dispose() {
|
124 | var _a, _b, _c;
|
125 | if (this.isDisposed) {
|
126 | return;
|
127 | }
|
128 | this.headingsChanged.disconnect(this.onHeadingsChanged, this);
|
129 | (_b = (_a = this.widget.context) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.metadataChanged.disconnect(this.onMetadataChanged, this);
|
130 | (_c = this.widget.content) === null || _c === void 0 ? void 0 : _c.activeCellChanged.disconnect(this.onActiveCellChanged, this);
|
131 | NotebookActions.executionScheduled.disconnect(this.onExecutionScheduled, this);
|
132 | NotebookActions.executed.disconnect(this.onExecuted, this);
|
133 | NotebookActions.outputCleared.disconnect(this.onOutputCleared, this);
|
134 | this._runningCells.length = 0;
|
135 | this._errorCells.length = 0;
|
136 | super.dispose();
|
137 | }
|
138 | |
139 |
|
140 |
|
141 |
|
142 |
|
143 | setConfiguration(c) {
|
144 |
|
145 | const metadataConfig = this.loadConfigurationFromMetadata();
|
146 | super.setConfiguration({ ...this.configuration, ...metadataConfig, ...c });
|
147 | }
|
148 | |
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | toggleCollapse(options) {
|
155 | super.toggleCollapse(options);
|
156 | this.updateRunningStatus(this.headings);
|
157 | }
|
158 | |
159 |
|
160 |
|
161 |
|
162 |
|
163 | getHeadings() {
|
164 | const cells = this.widget.content.widgets;
|
165 | const headings = [];
|
166 | const documentLevels = new Array();
|
167 |
|
168 | for (let i = 0; i < cells.length; i++) {
|
169 | const cell = cells[i];
|
170 | const model = cell.model;
|
171 | switch (model.type) {
|
172 | case 'code': {
|
173 |
|
174 | if (!this.configuration.syncCollapseState &&
|
175 | this.configuration.includeOutput) {
|
176 | headings.push(...TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map(heading => {
|
177 | return {
|
178 | ...heading,
|
179 | cellRef: cell,
|
180 | collapsed: false,
|
181 | isRunning: RunningStatus.Idle
|
182 | };
|
183 | }));
|
184 | }
|
185 | break;
|
186 | }
|
187 | case 'markdown': {
|
188 | const cellHeadings = TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map((heading, index) => {
|
189 | return {
|
190 | ...heading,
|
191 | cellRef: cell,
|
192 | collapsed: false,
|
193 | isRunning: RunningStatus.Idle
|
194 | };
|
195 | });
|
196 |
|
197 |
|
198 | if (this.configuration.syncCollapseState &&
|
199 | cell.headingCollapsed) {
|
200 | const minLevel = Math.min(...cellHeadings.map(h => h.level));
|
201 | const minHeading = cellHeadings.find(h => h.level === minLevel);
|
202 | minHeading.collapsed = cell.headingCollapsed;
|
203 | }
|
204 | headings.push(...cellHeadings);
|
205 | break;
|
206 | }
|
207 | }
|
208 | if (headings.length > 0) {
|
209 | this._cellToHeadingIndex.set(cell, headings.length - 1);
|
210 | }
|
211 | }
|
212 | this.updateRunningStatus(headings);
|
213 | return Promise.resolve(headings);
|
214 | }
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 | loadConfigurationFromMetadata() {
|
221 | const nbModel = this.widget.content.model;
|
222 | const newConfig = {};
|
223 | if (nbModel) {
|
224 | for (const option in this.configMetadataMap) {
|
225 | const keys = this.configMetadataMap[option];
|
226 | for (const k of keys) {
|
227 | let key = k;
|
228 | const negate = key[0] === '!';
|
229 | if (negate) {
|
230 | key = key.slice(1);
|
231 | }
|
232 | const keyPath = key.split('/');
|
233 | let value = nbModel.getMetadata(keyPath[0]);
|
234 | for (let p = 1; p < keyPath.length; p++) {
|
235 | value = (value !== null && value !== void 0 ? value : {})[keyPath[p]];
|
236 | }
|
237 | if (value !== undefined) {
|
238 | if (typeof value === 'boolean' && negate) {
|
239 | value = !value;
|
240 | }
|
241 | newConfig[option] = value;
|
242 | }
|
243 | }
|
244 | }
|
245 | }
|
246 | return newConfig;
|
247 | }
|
248 | onActiveCellChanged(notebook, cell) {
|
249 |
|
250 | const activeHeading = this.getCellHeadings(cell)[0];
|
251 | this.setActiveHeading(activeHeading !== null && activeHeading !== void 0 ? activeHeading : null, false);
|
252 | }
|
253 | onHeadingsChanged() {
|
254 | if (this.widget.content.activeCell) {
|
255 | this.onActiveCellChanged(this.widget.content, this.widget.content.activeCell);
|
256 | }
|
257 | }
|
258 | onExecuted(_, args) {
|
259 | this._runningCells.forEach((cell, index) => {
|
260 | var _a;
|
261 | if (cell === args.cell) {
|
262 | this._runningCells.splice(index, 1);
|
263 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
264 | if (headingIndex !== undefined) {
|
265 | const heading = this.headings[headingIndex];
|
266 |
|
267 |
|
268 | if (args.success || ((_a = args.error) === null || _a === void 0 ? void 0 : _a.errorName) === undefined) {
|
269 | heading.isRunning = RunningStatus.Idle;
|
270 | return;
|
271 | }
|
272 | heading.isRunning = RunningStatus.Error;
|
273 | if (!this._errorCells.includes(cell)) {
|
274 | this._errorCells.push(cell);
|
275 | }
|
276 | }
|
277 | }
|
278 | });
|
279 | this.updateRunningStatus(this.headings);
|
280 | this.stateChanged.emit();
|
281 | }
|
282 | onExecutionScheduled(_, args) {
|
283 | if (!this._runningCells.includes(args.cell)) {
|
284 | this._runningCells.push(args.cell);
|
285 | }
|
286 | this._errorCells.forEach((cell, index) => {
|
287 | if (cell === args.cell) {
|
288 | this._errorCells.splice(index, 1);
|
289 | }
|
290 | });
|
291 | this.updateRunningStatus(this.headings);
|
292 | this.stateChanged.emit();
|
293 | }
|
294 | onOutputCleared(_, args) {
|
295 | this._errorCells.forEach((cell, index) => {
|
296 | if (cell === args.cell) {
|
297 | this._errorCells.splice(index, 1);
|
298 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
299 | if (headingIndex !== undefined) {
|
300 | const heading = this.headings[headingIndex];
|
301 | heading.isRunning = RunningStatus.Idle;
|
302 | }
|
303 | }
|
304 | });
|
305 | this.updateRunningStatus(this.headings);
|
306 | this.stateChanged.emit();
|
307 | }
|
308 | onMetadataChanged() {
|
309 | this.setConfiguration({});
|
310 | }
|
311 | updateRunningStatus(headings) {
|
312 |
|
313 | this._runningCells.forEach((cell, index) => {
|
314 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
315 | if (headingIndex !== undefined) {
|
316 | const heading = this.headings[headingIndex];
|
317 |
|
318 |
|
319 | if (heading.isRunning !== RunningStatus.Running) {
|
320 | heading.isRunning =
|
321 | index > 0 ? RunningStatus.Scheduled : RunningStatus.Running;
|
322 | }
|
323 | }
|
324 | });
|
325 | this._errorCells.forEach((cell, index) => {
|
326 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
327 | if (headingIndex !== undefined) {
|
328 | const heading = this.headings[headingIndex];
|
329 |
|
330 |
|
331 | if (heading.isRunning === RunningStatus.Idle) {
|
332 | heading.isRunning = RunningStatus.Error;
|
333 | }
|
334 | }
|
335 | });
|
336 | let globalIndex = 0;
|
337 | while (globalIndex < headings.length) {
|
338 | const heading = headings[globalIndex];
|
339 | globalIndex++;
|
340 | if (heading.collapsed) {
|
341 | const maxIsRunning = Math.max(heading.isRunning, getMaxIsRunning(headings, heading.level));
|
342 | heading.dataset = {
|
343 | ...heading.dataset,
|
344 | 'data-running': maxIsRunning.toString()
|
345 | };
|
346 | }
|
347 | else {
|
348 | heading.dataset = {
|
349 | ...heading.dataset,
|
350 | 'data-running': heading.isRunning.toString()
|
351 | };
|
352 | }
|
353 | }
|
354 | function getMaxIsRunning(headings, collapsedLevel) {
|
355 | let maxIsRunning = RunningStatus.Idle;
|
356 | while (globalIndex < headings.length) {
|
357 | const heading = headings[globalIndex];
|
358 | heading.dataset = {
|
359 | ...heading.dataset,
|
360 | 'data-running': heading.isRunning.toString()
|
361 | };
|
362 | if (heading.level > collapsedLevel) {
|
363 | globalIndex++;
|
364 | maxIsRunning = Math.max(heading.isRunning, maxIsRunning);
|
365 | if (heading.collapsed) {
|
366 | maxIsRunning = Math.max(maxIsRunning, getMaxIsRunning(headings, heading.level));
|
367 | heading.dataset = {
|
368 | ...heading.dataset,
|
369 | 'data-running': maxIsRunning.toString()
|
370 | };
|
371 | }
|
372 | }
|
373 | else {
|
374 | break;
|
375 | }
|
376 | }
|
377 | return maxIsRunning;
|
378 | }
|
379 | }
|
380 | }
|
381 |
|
382 |
|
383 |
|
384 | export class NotebookToCFactory extends TableOfContentsFactory {
|
385 | |
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 | constructor(tracker, parser, sanitizer) {
|
393 | super(tracker);
|
394 | this.parser = parser;
|
395 | this.sanitizer = sanitizer;
|
396 | this._scrollToTop = true;
|
397 | }
|
398 | |
399 |
|
400 |
|
401 |
|
402 | get scrollToTop() {
|
403 | return this._scrollToTop;
|
404 | }
|
405 | set scrollToTop(v) {
|
406 | this._scrollToTop = v;
|
407 | }
|
408 | |
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 | _createNew(widget, configuration) {
|
416 | const model = new NotebookToCModel(widget, this.parser, this.sanitizer, configuration);
|
417 |
|
418 | let headingToElement = new WeakMap();
|
419 | const onActiveHeadingChanged = (model, heading) => {
|
420 | if (heading) {
|
421 | const onCellInViewport = async (cell) => {
|
422 | if (!cell.inViewport) {
|
423 |
|
424 | return;
|
425 | }
|
426 | const el = headingToElement.get(heading);
|
427 | if (el) {
|
428 | if (this.scrollToTop) {
|
429 | el.scrollIntoView({ block: 'start' });
|
430 | }
|
431 | else {
|
432 | const widgetBox = widget.content.node.getBoundingClientRect();
|
433 | const elementBox = el.getBoundingClientRect();
|
434 | if (elementBox.top > widgetBox.bottom ||
|
435 | elementBox.bottom < widgetBox.top) {
|
436 | el.scrollIntoView({ block: 'center' });
|
437 | }
|
438 | }
|
439 | }
|
440 | else {
|
441 | console.debug('scrolling to heading: using fallback strategy');
|
442 | await widget.content.scrollToItem(widget.content.activeCellIndex, this.scrollToTop ? 'start' : undefined, 0);
|
443 | }
|
444 | };
|
445 | const cell = heading.cellRef;
|
446 | const cells = widget.content.widgets;
|
447 | const idx = cells.indexOf(cell);
|
448 |
|
449 |
|
450 | if (cell.model.type == 'markdown' && widget.content.mode != 'command') {
|
451 | widget.content.mode = 'command';
|
452 | }
|
453 | widget.content.activeCellIndex = idx;
|
454 | if (cell.inViewport) {
|
455 | onCellInViewport(cell).catch(reason => {
|
456 | console.error(`Fail to scroll to cell to display the required heading (${reason}).`);
|
457 | });
|
458 | }
|
459 | else {
|
460 | widget.content
|
461 | .scrollToItem(idx, this.scrollToTop ? 'start' : undefined)
|
462 | .then(() => {
|
463 | return onCellInViewport(cell);
|
464 | })
|
465 | .catch(reason => {
|
466 | console.error(`Fail to scroll to cell to display the required heading (${reason}).`);
|
467 | });
|
468 | }
|
469 | }
|
470 | };
|
471 | const findHeadingElement = (cell) => {
|
472 | model.getCellHeadings(cell).forEach(async (heading) => {
|
473 | var _a, _b;
|
474 | const elementId = await getIdForHeading(heading, this.parser, this.sanitizer);
|
475 | const selector = elementId
|
476 | ? `h${heading.level}[id="${CSS.escape(elementId)}"]`
|
477 | : `h${heading.level}`;
|
478 | if (heading.outputIndex !== undefined) {
|
479 |
|
480 | headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.outputArea.widgets[heading.outputIndex].node, selector, (_a = heading.prefix) !== null && _a !== void 0 ? _a : ''));
|
481 | }
|
482 | else {
|
483 | headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.node, selector, (_b = heading.prefix) !== null && _b !== void 0 ? _b : ''));
|
484 | }
|
485 | });
|
486 | };
|
487 | const onHeadingsChanged = (model) => {
|
488 | if (!this.parser) {
|
489 | return;
|
490 | }
|
491 |
|
492 | TableOfContentsUtils.clearNumbering(widget.content.node);
|
493 |
|
494 | headingToElement = new WeakMap();
|
495 | widget.content.widgets.forEach(cell => {
|
496 | findHeadingElement(cell);
|
497 | });
|
498 | };
|
499 | const onHeadingCollapsed = (_, heading) => {
|
500 | var _a, _b, _c, _d;
|
501 | if (model.configuration.syncCollapseState) {
|
502 | if (heading !== null) {
|
503 | const cell = heading.cellRef;
|
504 | if (cell.headingCollapsed !== ((_a = heading.collapsed) !== null && _a !== void 0 ? _a : false)) {
|
505 | cell.headingCollapsed = (_b = heading.collapsed) !== null && _b !== void 0 ? _b : false;
|
506 | }
|
507 | }
|
508 | else {
|
509 | const collapseState = (_d = (_c = model.headings[0]) === null || _c === void 0 ? void 0 : _c.collapsed) !== null && _d !== void 0 ? _d : false;
|
510 | widget.content.widgets.forEach(cell => {
|
511 | if (cell instanceof MarkdownCell) {
|
512 | if (cell.headingInfo.level >= 0) {
|
513 | cell.headingCollapsed = collapseState;
|
514 | }
|
515 | }
|
516 | });
|
517 | }
|
518 | }
|
519 | };
|
520 | const onCellCollapsed = (_, cell) => {
|
521 | if (model.configuration.syncCollapseState) {
|
522 | const h = model.getCellHeadings(cell)[0];
|
523 | if (h) {
|
524 | model.toggleCollapse({
|
525 | heading: h,
|
526 | collapsed: cell.headingCollapsed
|
527 | });
|
528 | }
|
529 | }
|
530 | };
|
531 | const onCellInViewportChanged = (_, cell) => {
|
532 | if (cell.inViewport) {
|
533 | findHeadingElement(cell);
|
534 | }
|
535 | else {
|
536 |
|
537 | TableOfContentsUtils.clearNumbering(cell.node);
|
538 | }
|
539 | };
|
540 | void widget.context.ready.then(() => {
|
541 | onHeadingsChanged(model);
|
542 | model.activeHeadingChanged.connect(onActiveHeadingChanged);
|
543 | model.headingsChanged.connect(onHeadingsChanged);
|
544 | model.collapseChanged.connect(onHeadingCollapsed);
|
545 | widget.content.cellCollapsed.connect(onCellCollapsed);
|
546 | widget.content.cellInViewportChanged.connect(onCellInViewportChanged);
|
547 | widget.disposed.connect(() => {
|
548 | model.activeHeadingChanged.disconnect(onActiveHeadingChanged);
|
549 | model.headingsChanged.disconnect(onHeadingsChanged);
|
550 | model.collapseChanged.disconnect(onHeadingCollapsed);
|
551 | widget.content.cellCollapsed.disconnect(onCellCollapsed);
|
552 | widget.content.cellInViewportChanged.disconnect(onCellInViewportChanged);
|
553 | });
|
554 | });
|
555 | return model;
|
556 | }
|
557 | }
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | export async function getIdForHeading(heading, parser, sanitizer) {
|
565 | let elementId = null;
|
566 | if (heading.type === Cell.HeadingType.Markdown) {
|
567 | elementId = await TableOfContentsUtils.Markdown.getHeadingId(parser,
|
568 |
|
569 | heading.raw, heading.level, sanitizer);
|
570 | }
|
571 | else if (heading.type === Cell.HeadingType.HTML) {
|
572 |
|
573 | elementId = heading.id;
|
574 | }
|
575 | return elementId;
|
576 | }
|
577 |
|
\ | No newline at end of file |