UNPKG

23.5 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Cell, MarkdownCell } from '@jupyterlab/cells';
4import { TableOfContentsFactory, TableOfContentsModel, TableOfContentsUtils } from '@jupyterlab/toc';
5import { NotebookActions } from './actions';
6/**
7 * Cell running status
8 */
9export var RunningStatus;
10(function (RunningStatus) {
11 /**
12 * Cell is idle
13 */
14 RunningStatus[RunningStatus["Idle"] = -1] = "Idle";
15 /**
16 * Cell execution is unsuccessful
17 */
18 RunningStatus[RunningStatus["Error"] = -0.5] = "Error";
19 /**
20 * Cell execution is scheduled
21 */
22 RunningStatus[RunningStatus["Scheduled"] = 0] = "Scheduled";
23 /**
24 * Cell is running
25 */
26 RunningStatus[RunningStatus["Running"] = 1] = "Running";
27})(RunningStatus || (RunningStatus = {}));
28/**
29 * Table of content model for Notebook files.
30 */
31export class NotebookToCModel extends TableOfContentsModel {
32 /**
33 * Constructor
34 *
35 * @param widget The widget to search in
36 * @param parser Markdown parser
37 * @param sanitizer Sanitizer
38 * @param configuration Default model configuration
39 */
40 constructor(widget, parser, sanitizer, configuration) {
41 super(widget, configuration);
42 this.parser = parser;
43 this.sanitizer = sanitizer;
44 /**
45 * Mapping between configuration options and notebook metadata.
46 *
47 * If it starts with `!`, the boolean value of the configuration option is
48 * opposite to the one stored in metadata.
49 * If it contains `/`, the metadata data is nested.
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 // Load configuration from metadata
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 * Type of document supported by the model.
72 *
73 * #### Notes
74 * A `data-document-type` attribute with this value will be set
75 * on the tree view `.jp-TableOfContents-content[data-document-type="..."]`
76 */
77 get documentType() {
78 return 'notebook';
79 }
80 /**
81 * Whether the model gets updated even if the table of contents panel
82 * is hidden or not.
83 */
84 get isAlwaysActive() {
85 return true;
86 }
87 /**
88 * List of configuration options supported by the model.
89 */
90 get supportedOptions() {
91 return [
92 'baseNumbering',
93 'maximalDepth',
94 'numberingH1',
95 'numberHeaders',
96 'includeOutput',
97 'syncCollapseState'
98 ];
99 }
100 /**
101 * Get the headings of a given cell.
102 *
103 * @param cell Cell
104 * @returns The associated headings
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 * Dispose the object
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 * Model configuration setter.
140 *
141 * @param c New configuration
142 */
143 setConfiguration(c) {
144 // Ensure configuration update
145 const metadataConfig = this.loadConfigurationFromMetadata();
146 super.setConfiguration({ ...this.configuration, ...metadataConfig, ...c });
147 }
148 /**
149 * Callback on heading collapse.
150 *
151 * @param options.heading The heading to change state (all headings if not provided)
152 * @param options.collapsed The new collapsed status (toggle existing status if not provided)
153 */
154 toggleCollapse(options) {
155 super.toggleCollapse(options);
156 this.updateRunningStatus(this.headings);
157 }
158 /**
159 * Produce the headings for a document.
160 *
161 * @returns The list of new headings or `null` if nothing needs to be updated.
162 */
163 getHeadings() {
164 const cells = this.widget.content.widgets;
165 const headings = [];
166 const documentLevels = new Array();
167 // Generate headings by iterating through all notebook cells...
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 // Collapsing cells is incompatible with output headings
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 // If there are multiple headings, only collapse the highest heading (i.e. minimal level)
197 // consistent with the cell.headingInfo
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 * Read table of content configuration from notebook metadata.
217 *
218 * @returns ToC configuration from metadata
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 // Highlight the first title as active (if multiple titles are in the same cell)
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 // when the execution is not successful but errorName is undefined,
267 // the execution is interrupted by previous cells
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 // Update isRunning
313 this._runningCells.forEach((cell, index) => {
314 const headingIndex = this._cellToHeadingIndex.get(cell);
315 if (headingIndex !== undefined) {
316 const heading = this.headings[headingIndex];
317 // Running is prioritized over Scheduled, so if a heading is
318 // running don't change status
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 // Running and Scheduled are prioritized over Error, so only if
330 // a heading is idle will it be set to Error
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 * Table of content model factory for Notebook files.
383 */
384export class NotebookToCFactory extends TableOfContentsFactory {
385 /**
386 * Constructor
387 *
388 * @param tracker Widget tracker
389 * @param parser Markdown parser
390 * @param sanitizer Sanitizer
391 */
392 constructor(tracker, parser, sanitizer) {
393 super(tracker);
394 this.parser = parser;
395 this.sanitizer = sanitizer;
396 this._scrollToTop = true;
397 }
398 /**
399 * Whether to scroll the active heading to the top
400 * of the document or not.
401 */
402 get scrollToTop() {
403 return this._scrollToTop;
404 }
405 set scrollToTop(v) {
406 this._scrollToTop = v;
407 }
408 /**
409 * Create a new table of contents model for the widget
410 *
411 * @param widget - widget
412 * @param configuration - Table of contents configuration
413 * @returns The table of contents model
414 */
415 _createNew(widget, configuration) {
416 const model = new NotebookToCModel(widget, this.parser, this.sanitizer, configuration);
417 // Connect model signals to notebook panel
418 let headingToElement = new WeakMap();
419 const onActiveHeadingChanged = (model, heading) => {
420 if (heading) {
421 const onCellInViewport = async (cell) => {
422 if (!cell.inViewport) {
423 // Bail early
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 // Switch to command mode to avoid entering Markdown cell in edit mode
449 // if the document was in edit mode
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 // Code cell
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 // Clear all numbering items
492 TableOfContentsUtils.clearNumbering(widget.content.node);
493 // Create a new mapping
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 // Needed to remove prefix in cell outputs
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 * Get the element id for an heading
560 * @param heading Heading
561 * @param parser The markdownparser
562 * @returns The element id
563 */
564export 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 // Type from TableOfContentsUtils.Markdown.IMarkdownHeading
569 heading.raw, heading.level, sanitizer);
570 }
571 else if (heading.type === Cell.HeadingType.HTML) {
572 // Type from TableOfContentsUtils.IHTMLHeading
573 elementId = heading.id;
574 }
575 return elementId;
576}
577//# sourceMappingURL=toc.js.map
\No newline at end of file