1 |
|
2 |
|
3 |
|
4 | import { Cell, CodeCell, ICellModel, MarkdownCell } from '@jupyterlab/cells';
|
5 | import { IMarkdownParser, IRenderMime } from '@jupyterlab/rendermime';
|
6 | import {
|
7 | TableOfContents,
|
8 | TableOfContentsFactory,
|
9 | TableOfContentsModel,
|
10 | TableOfContentsUtils
|
11 | } from '@jupyterlab/toc';
|
12 | import { KernelError, NotebookActions } from './actions';
|
13 | import { NotebookPanel } from './panel';
|
14 | import { INotebookTracker } from './tokens';
|
15 | import { Notebook } from './widget';
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | export enum RunningStatus {
|
21 | |
22 |
|
23 |
|
24 | Idle = -1,
|
25 | |
26 |
|
27 |
|
28 | Error = -0.5,
|
29 | |
30 |
|
31 |
|
32 | Scheduled = 0,
|
33 | |
34 |
|
35 |
|
36 | Running = 1
|
37 | }
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | export interface INotebookHeading extends TableOfContents.IHeading {
|
43 | |
44 |
|
45 |
|
46 | cellRef: Cell;
|
47 |
|
48 | |
49 |
|
50 |
|
51 | isRunning: RunningStatus;
|
52 |
|
53 | |
54 |
|
55 |
|
56 | outputIndex?: number;
|
57 |
|
58 | |
59 |
|
60 |
|
61 | type: Cell.HeadingType;
|
62 | }
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | export class NotebookToCModel extends TableOfContentsModel<
|
68 | INotebookHeading,
|
69 | NotebookPanel
|
70 | > {
|
71 | |
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | constructor(
|
80 | widget: NotebookPanel,
|
81 | protected parser: IMarkdownParser | null,
|
82 | protected sanitizer: IRenderMime.ISanitizer,
|
83 | configuration?: TableOfContents.IConfig
|
84 | ) {
|
85 | super(widget, configuration);
|
86 | this._runningCells = new Array<Cell>();
|
87 | this._errorCells = new Array<Cell>();
|
88 | this._cellToHeadingIndex = new WeakMap<Cell, number>();
|
89 |
|
90 | void widget.context.ready.then(() => {
|
91 |
|
92 | this.setConfiguration({});
|
93 | });
|
94 |
|
95 | this.widget.context.model.metadataChanged.connect(
|
96 | this.onMetadataChanged,
|
97 | this
|
98 | );
|
99 | this.widget.content.activeCellChanged.connect(
|
100 | this.onActiveCellChanged,
|
101 | this
|
102 | );
|
103 | NotebookActions.executionScheduled.connect(this.onExecutionScheduled, this);
|
104 | NotebookActions.executed.connect(this.onExecuted, this);
|
105 | NotebookActions.outputCleared.connect(this.onOutputCleared, this);
|
106 | this.headingsChanged.connect(this.onHeadingsChanged, this);
|
107 | }
|
108 |
|
109 | |
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | get documentType(): string {
|
117 | return 'notebook';
|
118 | }
|
119 |
|
120 | |
121 |
|
122 |
|
123 |
|
124 | protected get isAlwaysActive(): boolean {
|
125 | return true;
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 | get supportedOptions(): (keyof TableOfContents.IConfig)[] {
|
132 | return [
|
133 | 'baseNumbering',
|
134 | 'maximalDepth',
|
135 | 'numberingH1',
|
136 | 'numberHeaders',
|
137 | 'includeOutput',
|
138 | 'syncCollapseState'
|
139 | ];
|
140 | }
|
141 |
|
142 | |
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 | getCellHeadings(cell: Cell): INotebookHeading[] {
|
149 | const headings = new Array<INotebookHeading>();
|
150 | let headingIndex = this._cellToHeadingIndex.get(cell);
|
151 |
|
152 | if (headingIndex !== undefined) {
|
153 | const candidate = this.headings[headingIndex];
|
154 | headings.push(candidate);
|
155 | while (
|
156 | this.headings[headingIndex - 1] &&
|
157 | this.headings[headingIndex - 1].cellRef === candidate.cellRef
|
158 | ) {
|
159 | headingIndex--;
|
160 | headings.unshift(this.headings[headingIndex]);
|
161 | }
|
162 | }
|
163 |
|
164 | return headings;
|
165 | }
|
166 |
|
167 | |
168 |
|
169 |
|
170 | dispose(): void {
|
171 | if (this.isDisposed) {
|
172 | return;
|
173 | }
|
174 |
|
175 | this.headingsChanged.disconnect(this.onHeadingsChanged, this);
|
176 | this.widget.context?.model?.metadataChanged.disconnect(
|
177 | this.onMetadataChanged,
|
178 | this
|
179 | );
|
180 | this.widget.content?.activeCellChanged.disconnect(
|
181 | this.onActiveCellChanged,
|
182 | this
|
183 | );
|
184 | NotebookActions.executionScheduled.disconnect(
|
185 | this.onExecutionScheduled,
|
186 | this
|
187 | );
|
188 | NotebookActions.executed.disconnect(this.onExecuted, this);
|
189 | NotebookActions.outputCleared.disconnect(this.onOutputCleared, this);
|
190 |
|
191 | this._runningCells.length = 0;
|
192 | this._errorCells.length = 0;
|
193 |
|
194 | super.dispose();
|
195 | }
|
196 |
|
197 | |
198 |
|
199 |
|
200 |
|
201 |
|
202 | setConfiguration(c: Partial<TableOfContents.IConfig>): void {
|
203 |
|
204 | const metadataConfig = this.loadConfigurationFromMetadata();
|
205 | super.setConfiguration({ ...this.configuration, ...metadataConfig, ...c });
|
206 | }
|
207 |
|
208 | |
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 | toggleCollapse(options: {
|
215 | heading?: INotebookHeading;
|
216 | collapsed?: boolean;
|
217 | }): void {
|
218 | super.toggleCollapse(options);
|
219 | this.updateRunningStatus(this.headings);
|
220 | }
|
221 |
|
222 | |
223 |
|
224 |
|
225 |
|
226 |
|
227 | protected getHeadings(): Promise<INotebookHeading[] | null> {
|
228 | const cells = this.widget.content.widgets;
|
229 | const headings: INotebookHeading[] = [];
|
230 | const documentLevels = new Array<number>();
|
231 |
|
232 |
|
233 | for (let i = 0; i < cells.length; i++) {
|
234 | const cell: Cell = cells[i];
|
235 | const model = cell.model;
|
236 |
|
237 | switch (model.type) {
|
238 | case 'code': {
|
239 |
|
240 | if (
|
241 | !this.configuration.syncCollapseState &&
|
242 | this.configuration.includeOutput
|
243 | ) {
|
244 | headings.push(
|
245 | ...TableOfContentsUtils.filterHeadings(
|
246 | cell.headings,
|
247 | this.configuration,
|
248 | documentLevels
|
249 | ).map(heading => {
|
250 | return {
|
251 | ...heading,
|
252 | cellRef: cell,
|
253 | collapsed: false,
|
254 | isRunning: RunningStatus.Idle
|
255 | };
|
256 | })
|
257 | );
|
258 | }
|
259 |
|
260 | break;
|
261 | }
|
262 | case 'markdown': {
|
263 | const cellHeadings = TableOfContentsUtils.filterHeadings(
|
264 | cell.headings,
|
265 | this.configuration,
|
266 | documentLevels
|
267 | ).map((heading, index) => {
|
268 | return {
|
269 | ...heading,
|
270 | cellRef: cell,
|
271 | collapsed: false,
|
272 | isRunning: RunningStatus.Idle
|
273 | };
|
274 | });
|
275 |
|
276 |
|
277 | if (
|
278 | this.configuration.syncCollapseState &&
|
279 | (cell as MarkdownCell).headingCollapsed
|
280 | ) {
|
281 | const minLevel = Math.min(...cellHeadings.map(h => h.level));
|
282 | const minHeading = cellHeadings.find(h => h.level === minLevel);
|
283 | minHeading!.collapsed = (cell as MarkdownCell).headingCollapsed;
|
284 | }
|
285 | headings.push(...cellHeadings);
|
286 | break;
|
287 | }
|
288 | }
|
289 |
|
290 | if (headings.length > 0) {
|
291 | this._cellToHeadingIndex.set(cell, headings.length - 1);
|
292 | }
|
293 | }
|
294 | this.updateRunningStatus(headings);
|
295 | return Promise.resolve(headings);
|
296 | }
|
297 |
|
298 | |
299 |
|
300 |
|
301 |
|
302 |
|
303 | protected loadConfigurationFromMetadata(): Partial<TableOfContents.IConfig> {
|
304 | const nbModel = this.widget.content.model;
|
305 | const newConfig: Partial<TableOfContents.IConfig> = {};
|
306 |
|
307 | if (nbModel) {
|
308 | for (const option in this.configMetadataMap) {
|
309 | const keys = this.configMetadataMap[option];
|
310 | for (const k of keys) {
|
311 | let key = k;
|
312 | const negate = key[0] === '!';
|
313 | if (negate) {
|
314 | key = key.slice(1);
|
315 | }
|
316 |
|
317 | const keyPath = key.split('/');
|
318 | let value = nbModel.getMetadata(keyPath[0]);
|
319 | for (let p = 1; p < keyPath.length; p++) {
|
320 | value = (value ?? {})[keyPath[p]];
|
321 | }
|
322 |
|
323 | if (value !== undefined) {
|
324 | if (typeof value === 'boolean' && negate) {
|
325 | value = !value;
|
326 | }
|
327 | newConfig[option] = value;
|
328 | }
|
329 | }
|
330 | }
|
331 | }
|
332 | return newConfig;
|
333 | }
|
334 |
|
335 | protected onActiveCellChanged(
|
336 | notebook: Notebook,
|
337 | cell: Cell<ICellModel>
|
338 | ): void {
|
339 |
|
340 | const activeHeading = this.getCellHeadings(cell)[0];
|
341 | this.setActiveHeading(activeHeading ?? null, false);
|
342 | }
|
343 |
|
344 | protected onHeadingsChanged(): void {
|
345 | if (this.widget.content.activeCell) {
|
346 | this.onActiveCellChanged(
|
347 | this.widget.content,
|
348 | this.widget.content.activeCell
|
349 | );
|
350 | }
|
351 | }
|
352 |
|
353 | protected onExecuted(
|
354 | _: unknown,
|
355 | args: {
|
356 | notebook: Notebook;
|
357 | cell: Cell;
|
358 | success: boolean;
|
359 | error: KernelError | null;
|
360 | }
|
361 | ): void {
|
362 | this._runningCells.forEach((cell, index) => {
|
363 | if (cell === args.cell) {
|
364 | this._runningCells.splice(index, 1);
|
365 |
|
366 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
367 | if (headingIndex !== undefined) {
|
368 | const heading = this.headings[headingIndex];
|
369 |
|
370 |
|
371 | if (args.success || args.error?.errorName === undefined) {
|
372 | heading.isRunning = RunningStatus.Idle;
|
373 | return;
|
374 | }
|
375 | heading.isRunning = RunningStatus.Error;
|
376 | if (!this._errorCells.includes(cell)) {
|
377 | this._errorCells.push(cell);
|
378 | }
|
379 | }
|
380 | }
|
381 | });
|
382 |
|
383 | this.updateRunningStatus(this.headings);
|
384 | this.stateChanged.emit();
|
385 | }
|
386 |
|
387 | protected onExecutionScheduled(
|
388 | _: unknown,
|
389 | args: { notebook: Notebook; cell: Cell }
|
390 | ): void {
|
391 | if (!this._runningCells.includes(args.cell)) {
|
392 | this._runningCells.push(args.cell);
|
393 | }
|
394 | this._errorCells.forEach((cell, index) => {
|
395 | if (cell === args.cell) {
|
396 | this._errorCells.splice(index, 1);
|
397 | }
|
398 | });
|
399 |
|
400 | this.updateRunningStatus(this.headings);
|
401 | this.stateChanged.emit();
|
402 | }
|
403 |
|
404 | protected onOutputCleared(
|
405 | _: unknown,
|
406 | args: { notebook: Notebook; cell: Cell }
|
407 | ): void {
|
408 | this._errorCells.forEach((cell, index) => {
|
409 | if (cell === args.cell) {
|
410 | this._errorCells.splice(index, 1);
|
411 |
|
412 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
413 | if (headingIndex !== undefined) {
|
414 | const heading = this.headings[headingIndex];
|
415 | heading.isRunning = RunningStatus.Idle;
|
416 | }
|
417 | }
|
418 | });
|
419 | this.updateRunningStatus(this.headings);
|
420 | this.stateChanged.emit();
|
421 | }
|
422 |
|
423 | protected onMetadataChanged(): void {
|
424 | this.setConfiguration({});
|
425 | }
|
426 |
|
427 | protected updateRunningStatus(headings: INotebookHeading[]): void {
|
428 |
|
429 | this._runningCells.forEach((cell, index) => {
|
430 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
431 | if (headingIndex !== undefined) {
|
432 | const heading = this.headings[headingIndex];
|
433 |
|
434 |
|
435 | if (heading.isRunning !== RunningStatus.Running) {
|
436 | heading.isRunning =
|
437 | index > 0 ? RunningStatus.Scheduled : RunningStatus.Running;
|
438 | }
|
439 | }
|
440 | });
|
441 |
|
442 | this._errorCells.forEach((cell, index) => {
|
443 | const headingIndex = this._cellToHeadingIndex.get(cell);
|
444 | if (headingIndex !== undefined) {
|
445 | const heading = this.headings[headingIndex];
|
446 |
|
447 |
|
448 | if (heading.isRunning === RunningStatus.Idle) {
|
449 | heading.isRunning = RunningStatus.Error;
|
450 | }
|
451 | }
|
452 | });
|
453 |
|
454 | let globalIndex = 0;
|
455 | while (globalIndex < headings.length) {
|
456 | const heading = headings[globalIndex];
|
457 | globalIndex++;
|
458 | if (heading.collapsed) {
|
459 | const maxIsRunning = Math.max(
|
460 | heading.isRunning,
|
461 | getMaxIsRunning(headings, heading.level)
|
462 | );
|
463 | heading.dataset = {
|
464 | ...heading.dataset,
|
465 | 'data-running': maxIsRunning.toString()
|
466 | };
|
467 | } else {
|
468 | heading.dataset = {
|
469 | ...heading.dataset,
|
470 | 'data-running': heading.isRunning.toString()
|
471 | };
|
472 | }
|
473 | }
|
474 |
|
475 | function getMaxIsRunning(
|
476 | headings: INotebookHeading[],
|
477 | collapsedLevel: number
|
478 | ): RunningStatus {
|
479 | let maxIsRunning = RunningStatus.Idle;
|
480 |
|
481 | while (globalIndex < headings.length) {
|
482 | const heading = headings[globalIndex];
|
483 | heading.dataset = {
|
484 | ...heading.dataset,
|
485 | 'data-running': heading.isRunning.toString()
|
486 | };
|
487 |
|
488 | if (heading.level > collapsedLevel) {
|
489 | globalIndex++;
|
490 | maxIsRunning = Math.max(heading.isRunning, maxIsRunning);
|
491 | if (heading.collapsed) {
|
492 | maxIsRunning = Math.max(
|
493 | maxIsRunning,
|
494 | getMaxIsRunning(headings, heading.level)
|
495 | );
|
496 | heading.dataset = {
|
497 | ...heading.dataset,
|
498 | 'data-running': maxIsRunning.toString()
|
499 | };
|
500 | }
|
501 | } else {
|
502 | break;
|
503 | }
|
504 | }
|
505 |
|
506 | return maxIsRunning;
|
507 | }
|
508 | }
|
509 |
|
510 | |
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
517 | protected configMetadataMap: {
|
518 | [k: keyof TableOfContents.IConfig]: string[];
|
519 | } = {
|
520 | numberHeaders: ['toc-autonumbering', 'toc/number_sections'],
|
521 | numberingH1: ['!toc/skip_h1_title'],
|
522 | baseNumbering: ['toc/base_numbering']
|
523 | };
|
524 |
|
525 | private _runningCells: Cell[];
|
526 | private _errorCells: Cell[];
|
527 | private _cellToHeadingIndex: WeakMap<Cell, number>;
|
528 | }
|
529 |
|
530 |
|
531 |
|
532 |
|
533 | export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
|
534 | |
535 |
|
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 | constructor(
|
542 | tracker: INotebookTracker,
|
543 | protected parser: IMarkdownParser | null,
|
544 | protected sanitizer: IRenderMime.ISanitizer
|
545 | ) {
|
546 | super(tracker);
|
547 | }
|
548 |
|
549 | |
550 |
|
551 |
|
552 |
|
553 | get scrollToTop(): boolean {
|
554 | return this._scrollToTop;
|
555 | }
|
556 | set scrollToTop(v: boolean) {
|
557 | this._scrollToTop = v;
|
558 | }
|
559 |
|
560 | |
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 |
|
567 | protected _createNew(
|
568 | widget: NotebookPanel,
|
569 | configuration?: TableOfContents.IConfig
|
570 | ): TableOfContentsModel<TableOfContents.IHeading, NotebookPanel> {
|
571 | const model = new NotebookToCModel(
|
572 | widget,
|
573 | this.parser,
|
574 | this.sanitizer,
|
575 | configuration
|
576 | );
|
577 |
|
578 |
|
579 |
|
580 | let headingToElement = new WeakMap<INotebookHeading, Element | null>();
|
581 |
|
582 | const onActiveHeadingChanged = (
|
583 | model: NotebookToCModel,
|
584 | heading: INotebookHeading | null
|
585 | ) => {
|
586 | if (heading) {
|
587 | const onCellInViewport = async (cell: Cell): Promise<void> => {
|
588 | if (!cell.inViewport) {
|
589 |
|
590 | return;
|
591 | }
|
592 |
|
593 | const el = headingToElement.get(heading);
|
594 |
|
595 | if (el) {
|
596 | if (this.scrollToTop) {
|
597 | el.scrollIntoView({ block: 'start' });
|
598 | } else {
|
599 | const widgetBox = widget.content.node.getBoundingClientRect();
|
600 | const elementBox = el.getBoundingClientRect();
|
601 |
|
602 | if (
|
603 | elementBox.top > widgetBox.bottom ||
|
604 | elementBox.bottom < widgetBox.top
|
605 | ) {
|
606 | el.scrollIntoView({ block: 'center' });
|
607 | }
|
608 | }
|
609 | } else {
|
610 | console.debug('scrolling to heading: using fallback strategy');
|
611 | await widget.content.scrollToItem(
|
612 | widget.content.activeCellIndex,
|
613 | this.scrollToTop ? 'start' : undefined,
|
614 | 0
|
615 | );
|
616 | }
|
617 | };
|
618 |
|
619 | const cell = heading.cellRef;
|
620 | const cells = widget.content.widgets;
|
621 | const idx = cells.indexOf(cell);
|
622 |
|
623 |
|
624 | if (cell.model.type == 'markdown' && widget.content.mode != 'command') {
|
625 | widget.content.mode = 'command';
|
626 | }
|
627 |
|
628 | widget.content.activeCellIndex = idx;
|
629 |
|
630 | if (cell.inViewport) {
|
631 | onCellInViewport(cell).catch(reason => {
|
632 | console.error(
|
633 | `Fail to scroll to cell to display the required heading (${reason}).`
|
634 | );
|
635 | });
|
636 | } else {
|
637 | widget.content
|
638 | .scrollToItem(idx, this.scrollToTop ? 'start' : undefined)
|
639 | .then(() => {
|
640 | return onCellInViewport(cell);
|
641 | })
|
642 | .catch(reason => {
|
643 | console.error(
|
644 | `Fail to scroll to cell to display the required heading (${reason}).`
|
645 | );
|
646 | });
|
647 | }
|
648 | }
|
649 | };
|
650 |
|
651 | const findHeadingElement = (cell: Cell): void => {
|
652 | model.getCellHeadings(cell).forEach(async heading => {
|
653 | const elementId = await getIdForHeading(
|
654 | heading,
|
655 | this.parser!,
|
656 | this.sanitizer
|
657 | );
|
658 |
|
659 | const selector = elementId
|
660 | ? `h${heading.level}[id="${CSS.escape(elementId)}"]`
|
661 | : `h${heading.level}`;
|
662 |
|
663 | if (heading.outputIndex !== undefined) {
|
664 |
|
665 | headingToElement.set(
|
666 | heading,
|
667 | TableOfContentsUtils.addPrefix(
|
668 | (heading.cellRef as CodeCell).outputArea.widgets[
|
669 | heading.outputIndex
|
670 | ].node,
|
671 | selector,
|
672 | heading.prefix ?? ''
|
673 | )
|
674 | );
|
675 | } else {
|
676 | headingToElement.set(
|
677 | heading,
|
678 | TableOfContentsUtils.addPrefix(
|
679 | heading.cellRef.node,
|
680 | selector,
|
681 | heading.prefix ?? ''
|
682 | )
|
683 | );
|
684 | }
|
685 | });
|
686 | };
|
687 |
|
688 | const onHeadingsChanged = (model: NotebookToCModel) => {
|
689 | if (!this.parser) {
|
690 | return;
|
691 | }
|
692 |
|
693 | TableOfContentsUtils.clearNumbering(widget.content.node);
|
694 |
|
695 |
|
696 | headingToElement = new WeakMap<INotebookHeading, Element | null>();
|
697 |
|
698 | widget.content.widgets.forEach(cell => {
|
699 | findHeadingElement(cell);
|
700 | });
|
701 | };
|
702 |
|
703 | const onHeadingCollapsed = (
|
704 | _: NotebookToCModel,
|
705 | heading: INotebookHeading | null
|
706 | ) => {
|
707 | if (model.configuration.syncCollapseState) {
|
708 | if (heading !== null) {
|
709 | const cell = heading.cellRef as MarkdownCell;
|
710 | if (cell.headingCollapsed !== (heading.collapsed ?? false)) {
|
711 | cell.headingCollapsed = heading.collapsed ?? false;
|
712 | }
|
713 | } else {
|
714 | const collapseState = model.headings[0]?.collapsed ?? false;
|
715 | widget.content.widgets.forEach(cell => {
|
716 | if (cell instanceof MarkdownCell) {
|
717 | if (cell.headingInfo.level >= 0) {
|
718 | cell.headingCollapsed = collapseState;
|
719 | }
|
720 | }
|
721 | });
|
722 | }
|
723 | }
|
724 | };
|
725 | const onCellCollapsed = (_: unknown, cell: MarkdownCell) => {
|
726 | if (model.configuration.syncCollapseState) {
|
727 | const h = model.getCellHeadings(cell)[0];
|
728 | if (h) {
|
729 | model.toggleCollapse({
|
730 | heading: h,
|
731 | collapsed: cell.headingCollapsed
|
732 | });
|
733 | }
|
734 | }
|
735 | };
|
736 |
|
737 | const onCellInViewportChanged = (_: unknown, cell: Cell) => {
|
738 | if (cell.inViewport) {
|
739 | findHeadingElement(cell);
|
740 | } else {
|
741 |
|
742 | TableOfContentsUtils.clearNumbering(cell.node);
|
743 | }
|
744 | };
|
745 |
|
746 | void widget.context.ready.then(() => {
|
747 | onHeadingsChanged(model);
|
748 |
|
749 | model.activeHeadingChanged.connect(onActiveHeadingChanged);
|
750 | model.headingsChanged.connect(onHeadingsChanged);
|
751 | model.collapseChanged.connect(onHeadingCollapsed);
|
752 | widget.content.cellCollapsed.connect(onCellCollapsed);
|
753 | widget.content.cellInViewportChanged.connect(onCellInViewportChanged);
|
754 | widget.disposed.connect(() => {
|
755 | model.activeHeadingChanged.disconnect(onActiveHeadingChanged);
|
756 | model.headingsChanged.disconnect(onHeadingsChanged);
|
757 | model.collapseChanged.disconnect(onHeadingCollapsed);
|
758 | widget.content.cellCollapsed.disconnect(onCellCollapsed);
|
759 | widget.content.cellInViewportChanged.disconnect(
|
760 | onCellInViewportChanged
|
761 | );
|
762 | });
|
763 | });
|
764 |
|
765 | return model;
|
766 | }
|
767 |
|
768 | private _scrollToTop: boolean = true;
|
769 | }
|
770 |
|
771 |
|
772 |
|
773 |
|
774 |
|
775 |
|
776 |
|
777 | export async function getIdForHeading(
|
778 | heading: INotebookHeading,
|
779 | parser: IRenderMime.IMarkdownParser,
|
780 | sanitizer: IRenderMime.ISanitizer
|
781 | ) {
|
782 | let elementId: string | null = null;
|
783 | if (heading.type === Cell.HeadingType.Markdown) {
|
784 | elementId = await TableOfContentsUtils.Markdown.getHeadingId(
|
785 | parser,
|
786 |
|
787 | (heading as any).raw,
|
788 | heading.level,
|
789 | sanitizer
|
790 | );
|
791 | } else if (heading.type === Cell.HeadingType.HTML) {
|
792 |
|
793 | elementId = (heading as any).id;
|
794 | }
|
795 | return elementId;
|
796 | }
|