UNPKG

19.8 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 scheduled
17 */
18 RunningStatus[RunningStatus["Scheduled"] = 0] = "Scheduled";
19 /**
20 * Cell is running
21 */
22 RunningStatus[RunningStatus["Running"] = 1] = "Running";
23})(RunningStatus || (RunningStatus = {}));
24/**
25 * Table of content model for Notebook files.
26 */
27export class NotebookToCModel extends TableOfContentsModel {
28 /**
29 * Constructor
30 *
31 * @param widget The widget to search in
32 * @param parser Markdown parser
33 * @param sanitizer Sanitizer
34 * @param configuration Default model configuration
35 */
36 constructor(widget, parser, sanitizer, configuration) {
37 super(widget, configuration);
38 this.parser = parser;
39 this.sanitizer = sanitizer;
40 /**
41 * Mapping between configuration options and notebook metadata.
42 *
43 * If it starts with `!`, the boolean value of the configuration option is
44 * opposite to the one stored in metadata.
45 * If it contains `/`, the metadata data is nested.
46 */
47 this.configMetadataMap = {
48 numberHeaders: ['toc-autonumbering', 'toc/number_sections'],
49 numberingH1: ['!toc/skip_h1_title'],
50 baseNumbering: ['toc/base_numbering']
51 };
52 this._runningCells = new Array();
53 this._cellToHeadingIndex = new WeakMap();
54 void widget.context.ready.then(() => {
55 // Load configuration from metadata
56 this.setConfiguration({});
57 });
58 this.widget.context.model.metadataChanged.connect(this.onMetadataChanged, this);
59 this.widget.content.activeCellChanged.connect(this.onActiveCellChanged, this);
60 NotebookActions.executionScheduled.connect(this.onExecutionScheduled, this);
61 NotebookActions.executed.connect(this.onExecuted, this);
62 this.headingsChanged.connect(this.onHeadingsChanged, this);
63 }
64 /**
65 * Type of document supported by the model.
66 *
67 * #### Notes
68 * A `data-document-type` attribute with this value will be set
69 * on the tree view `.jp-TableOfContents-content[data-document-type="..."]`
70 */
71 get documentType() {
72 return 'notebook';
73 }
74 /**
75 * Whether the model gets updated even if the table of contents panel
76 * is hidden or not.
77 */
78 get isAlwaysActive() {
79 return true;
80 }
81 /**
82 * List of configuration options supported by the model.
83 */
84 get supportedOptions() {
85 return [
86 'baseNumbering',
87 'maximalDepth',
88 'numberingH1',
89 'numberHeaders',
90 'includeOutput',
91 'syncCollapseState'
92 ];
93 }
94 /**
95 * Get the headings of a given cell.
96 *
97 * @param cell Cell
98 * @returns The associated headings
99 */
100 getCellHeadings(cell) {
101 const headings = new Array();
102 let headingIndex = this._cellToHeadingIndex.get(cell);
103 if (headingIndex !== undefined) {
104 const candidate = this.headings[headingIndex];
105 headings.push(candidate);
106 while (this.headings[headingIndex - 1] &&
107 this.headings[headingIndex - 1].cellRef === candidate.cellRef) {
108 headingIndex--;
109 headings.unshift(this.headings[headingIndex]);
110 }
111 }
112 return headings;
113 }
114 /**
115 * Dispose the object
116 */
117 dispose() {
118 var _a, _b, _c;
119 if (this.isDisposed) {
120 return;
121 }
122 this.headingsChanged.disconnect(this.onHeadingsChanged, this);
123 (_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);
124 (_c = this.widget.content) === null || _c === void 0 ? void 0 : _c.activeCellChanged.disconnect(this.onActiveCellChanged, this);
125 NotebookActions.executionScheduled.disconnect(this.onExecutionScheduled, this);
126 NotebookActions.executed.disconnect(this.onExecuted, this);
127 this._runningCells.length = 0;
128 super.dispose();
129 }
130 /**
131 * Model configuration setter.
132 *
133 * @param c New configuration
134 */
135 setConfiguration(c) {
136 // Ensure configuration update
137 const metadataConfig = this.loadConfigurationFromMetadata();
138 super.setConfiguration({ ...this.configuration, ...metadataConfig, ...c });
139 }
140 /**
141 * Callback on heading collapse.
142 *
143 * @param options.heading The heading to change state (all headings if not provided)
144 * @param options.collapsed The new collapsed status (toggle existing status if not provided)
145 */
146 toggleCollapse(options) {
147 super.toggleCollapse(options);
148 this.updateRunningStatus(this.headings);
149 }
150 /**
151 * Produce the headings for a document.
152 *
153 * @returns The list of new headings or `null` if nothing needs to be updated.
154 */
155 getHeadings() {
156 const cells = this.widget.content.widgets;
157 const headings = [];
158 const documentLevels = new Array();
159 // Generate headings by iterating through all notebook cells...
160 for (let i = 0; i < cells.length; i++) {
161 const cell = cells[i];
162 const model = cell.model;
163 switch (model.type) {
164 case 'code': {
165 // Collapsing cells is incompatible with output headings
166 if (!this.configuration.syncCollapseState &&
167 this.configuration.includeOutput) {
168 headings.push(...TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map(heading => {
169 return {
170 ...heading,
171 cellRef: cell,
172 collapsed: false,
173 isRunning: RunningStatus.Idle
174 };
175 }));
176 }
177 break;
178 }
179 case 'markdown': {
180 const cellHeadings = TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map((heading, index) => {
181 return {
182 ...heading,
183 cellRef: cell,
184 collapsed: false,
185 isRunning: RunningStatus.Idle
186 };
187 });
188 // If there are multiple headings, only collapse the highest heading (i.e. minimal level)
189 // consistent with the cell.headingInfo
190 if (this.configuration.syncCollapseState &&
191 cell.headingCollapsed) {
192 const minLevel = Math.min(...cellHeadings.map(h => h.level));
193 const minHeading = cellHeadings.find(h => h.level === minLevel);
194 minHeading.collapsed = cell.headingCollapsed;
195 }
196 headings.push(...cellHeadings);
197 break;
198 }
199 }
200 if (headings.length > 0) {
201 this._cellToHeadingIndex.set(cell, headings.length - 1);
202 }
203 }
204 this.updateRunningStatus(headings);
205 return Promise.resolve(headings);
206 }
207 /**
208 * Read table of content configuration from notebook metadata.
209 *
210 * @returns ToC configuration from metadata
211 */
212 loadConfigurationFromMetadata() {
213 const nbModel = this.widget.content.model;
214 const newConfig = {};
215 if (nbModel) {
216 for (const option in this.configMetadataMap) {
217 const keys = this.configMetadataMap[option];
218 for (const k of keys) {
219 let key = k;
220 const negate = key[0] === '!';
221 if (negate) {
222 key = key.slice(1);
223 }
224 const keyPath = key.split('/');
225 let value = nbModel.getMetadata(keyPath[0]);
226 for (let p = 1; p < keyPath.length; p++) {
227 value = (value !== null && value !== void 0 ? value : {})[keyPath[p]];
228 }
229 if (value !== undefined) {
230 if (typeof value === 'boolean' && negate) {
231 value = !value;
232 }
233 newConfig[option] = value;
234 }
235 }
236 }
237 }
238 return newConfig;
239 }
240 onActiveCellChanged(notebook, cell) {
241 // Highlight the first title as active (if multiple titles are in the same cell)
242 const activeHeading = this.getCellHeadings(cell)[0];
243 this.setActiveHeading(activeHeading !== null && activeHeading !== void 0 ? activeHeading : null, false);
244 }
245 onHeadingsChanged() {
246 if (this.widget.content.activeCell) {
247 this.onActiveCellChanged(this.widget.content, this.widget.content.activeCell);
248 }
249 }
250 onExecuted(_, args) {
251 this._runningCells.forEach((cell, index) => {
252 if (cell === args.cell) {
253 this._runningCells.splice(index, 1);
254 const headingIndex = this._cellToHeadingIndex.get(cell);
255 if (headingIndex !== undefined) {
256 const heading = this.headings[headingIndex];
257 heading.isRunning = RunningStatus.Idle;
258 }
259 }
260 });
261 this.updateRunningStatus(this.headings);
262 this.stateChanged.emit();
263 }
264 onExecutionScheduled(_, args) {
265 if (!this._runningCells.includes(args.cell)) {
266 this._runningCells.push(args.cell);
267 }
268 this.updateRunningStatus(this.headings);
269 this.stateChanged.emit();
270 }
271 onMetadataChanged() {
272 this.setConfiguration({});
273 }
274 updateRunningStatus(headings) {
275 // Update isRunning
276 this._runningCells.forEach((cell, index) => {
277 const headingIndex = this._cellToHeadingIndex.get(cell);
278 if (headingIndex !== undefined) {
279 const heading = this.headings[headingIndex];
280 heading.isRunning = Math.max(index > 0 ? RunningStatus.Scheduled : RunningStatus.Running, heading.isRunning);
281 }
282 });
283 let globalIndex = 0;
284 while (globalIndex < headings.length) {
285 const heading = headings[globalIndex];
286 globalIndex++;
287 if (heading.collapsed) {
288 const maxIsRunning = Math.max(heading.isRunning, getMaxIsRunning(headings, heading.level));
289 heading.dataset = {
290 ...heading.dataset,
291 'data-running': maxIsRunning.toString()
292 };
293 }
294 else {
295 heading.dataset = {
296 ...heading.dataset,
297 'data-running': heading.isRunning.toString()
298 };
299 }
300 }
301 function getMaxIsRunning(headings, collapsedLevel) {
302 let maxIsRunning = RunningStatus.Idle;
303 while (globalIndex < headings.length) {
304 const heading = headings[globalIndex];
305 heading.dataset = {
306 ...heading.dataset,
307 'data-running': heading.isRunning.toString()
308 };
309 if (heading.level > collapsedLevel) {
310 globalIndex++;
311 maxIsRunning = Math.max(heading.isRunning, maxIsRunning);
312 if (heading.collapsed) {
313 maxIsRunning = Math.max(maxIsRunning, getMaxIsRunning(headings, heading.level));
314 heading.dataset = {
315 ...heading.dataset,
316 'data-running': maxIsRunning.toString()
317 };
318 }
319 }
320 else {
321 break;
322 }
323 }
324 return maxIsRunning;
325 }
326 }
327}
328/**
329 * Table of content model factory for Notebook files.
330 */
331export class NotebookToCFactory extends TableOfContentsFactory {
332 /**
333 * Constructor
334 *
335 * @param tracker Widget tracker
336 * @param parser Markdown parser
337 * @param sanitizer Sanitizer
338 */
339 constructor(tracker, parser, sanitizer) {
340 super(tracker);
341 this.parser = parser;
342 this.sanitizer = sanitizer;
343 }
344 /**
345 * Create a new table of contents model for the widget
346 *
347 * @param widget - widget
348 * @param configuration - Table of contents configuration
349 * @returns The table of contents model
350 */
351 _createNew(widget, configuration) {
352 const model = new NotebookToCModel(widget, this.parser, this.sanitizer, configuration);
353 // Connect model signals to notebook panel
354 let headingToElement = new WeakMap();
355 const onActiveHeadingChanged = (model, heading) => {
356 if (heading) {
357 const onCellInViewport = (cell) => {
358 if (!cell.inViewport) {
359 // Bail early
360 return;
361 }
362 const el = headingToElement.get(heading);
363 if (el) {
364 const widgetBox = widget.content.node.getBoundingClientRect();
365 const elementBox = el.getBoundingClientRect();
366 if (elementBox.top > widgetBox.bottom ||
367 elementBox.bottom < widgetBox.top) {
368 el.scrollIntoView({ block: 'center' });
369 }
370 }
371 };
372 const cell = heading.cellRef;
373 const cells = widget.content.widgets;
374 const idx = cells.indexOf(cell);
375 widget.content.activeCellIndex = idx;
376 if (cell.inViewport) {
377 onCellInViewport(cell);
378 }
379 else {
380 widget.content
381 .scrollToItem(idx)
382 .then(() => {
383 onCellInViewport(cell);
384 })
385 .catch(reason => {
386 console.error('Fail to scroll to cell to display the required heading.');
387 });
388 }
389 }
390 };
391 const findHeadingElement = (cell) => {
392 model.getCellHeadings(cell).forEach(async (heading) => {
393 var _a, _b;
394 const elementId = await getIdForHeading(heading, this.parser);
395 const selector = elementId
396 ? `h${heading.level}[id="${elementId}"]`
397 : `h${heading.level}`;
398 if (heading.outputIndex !== undefined) {
399 // Code cell
400 headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.outputArea.widgets[heading.outputIndex].node, selector, (_a = heading.prefix) !== null && _a !== void 0 ? _a : ''));
401 }
402 else {
403 headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.node, selector, (_b = heading.prefix) !== null && _b !== void 0 ? _b : ''));
404 }
405 });
406 };
407 const onHeadingsChanged = (model) => {
408 if (!this.parser) {
409 return;
410 }
411 // Clear all numbering items
412 TableOfContentsUtils.clearNumbering(widget.content.node);
413 // Create a new mapping
414 headingToElement = new WeakMap();
415 widget.content.widgets.forEach(cell => {
416 findHeadingElement(cell);
417 });
418 };
419 const onHeadingCollapsed = (_, heading) => {
420 var _a, _b, _c, _d;
421 if (model.configuration.syncCollapseState) {
422 if (heading !== null) {
423 const cell = heading.cellRef;
424 if (cell.headingCollapsed !== ((_a = heading.collapsed) !== null && _a !== void 0 ? _a : false)) {
425 cell.headingCollapsed = (_b = heading.collapsed) !== null && _b !== void 0 ? _b : false;
426 }
427 }
428 else {
429 const collapseState = (_d = (_c = model.headings[0]) === null || _c === void 0 ? void 0 : _c.collapsed) !== null && _d !== void 0 ? _d : false;
430 widget.content.widgets.forEach(cell => {
431 if (cell instanceof MarkdownCell) {
432 if (cell.headingInfo.level >= 0) {
433 cell.headingCollapsed = collapseState;
434 }
435 }
436 });
437 }
438 }
439 };
440 const onCellCollapsed = (_, cell) => {
441 if (model.configuration.syncCollapseState) {
442 const h = model.getCellHeadings(cell)[0];
443 if (h) {
444 model.toggleCollapse({
445 heading: h,
446 collapsed: cell.headingCollapsed
447 });
448 }
449 }
450 };
451 const onCellInViewportChanged = (_, cell) => {
452 if (cell.inViewport) {
453 findHeadingElement(cell);
454 }
455 else {
456 // Needed to remove prefix in cell outputs
457 TableOfContentsUtils.clearNumbering(cell.node);
458 }
459 };
460 void widget.context.ready.then(() => {
461 onHeadingsChanged(model);
462 model.activeHeadingChanged.connect(onActiveHeadingChanged);
463 model.headingsChanged.connect(onHeadingsChanged);
464 model.collapseChanged.connect(onHeadingCollapsed);
465 widget.content.cellCollapsed.connect(onCellCollapsed);
466 widget.content.cellInViewportChanged.connect(onCellInViewportChanged);
467 widget.disposed.connect(() => {
468 model.activeHeadingChanged.disconnect(onActiveHeadingChanged);
469 model.headingsChanged.disconnect(onHeadingsChanged);
470 model.collapseChanged.disconnect(onHeadingCollapsed);
471 widget.content.cellCollapsed.disconnect(onCellCollapsed);
472 widget.content.cellInViewportChanged.disconnect(onCellInViewportChanged);
473 });
474 });
475 return model;
476 }
477}
478/**
479 * Get the element id for an heading
480 * @param heading Heading
481 * @param parser The markdownparser
482 * @returns The element id
483 */
484export async function getIdForHeading(heading, parser) {
485 let elementId = null;
486 if (heading.type === Cell.HeadingType.Markdown) {
487 elementId = await TableOfContentsUtils.Markdown.getHeadingId(parser,
488 // Type from TableOfContentsUtils.Markdown.IMarkdownHeading
489 heading.raw, heading.level);
490 }
491 else if (heading.type === Cell.HeadingType.HTML) {
492 // Type from TableOfContentsUtils.IHTMLHeading
493 elementId = heading.id;
494 }
495 return elementId;
496}
497//# sourceMappingURL=toc.js.map
\No newline at end of file