@jupyterlab/notebook
Version:
JupyterLab - Notebook
595 lines • 24.2 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { Cell, MarkdownCell } from '@jupyterlab/cells';
import { TableOfContentsFactory, TableOfContentsModel, TableOfContentsUtils } from '@jupyterlab/toc';
import { NotebookActions } from './actions';
/**
* Cell running status
*/
export var RunningStatus;
(function (RunningStatus) {
/**
* Cell is idle
*/
RunningStatus[RunningStatus["Idle"] = -1] = "Idle";
/**
* Cell execution is unsuccessful
*/
RunningStatus[RunningStatus["Error"] = -0.5] = "Error";
/**
* Cell execution is scheduled
*/
RunningStatus[RunningStatus["Scheduled"] = 0] = "Scheduled";
/**
* Cell is running
*/
RunningStatus[RunningStatus["Running"] = 1] = "Running";
})(RunningStatus || (RunningStatus = {}));
/**
* Table of content model for Notebook files.
*/
export class NotebookToCModel extends TableOfContentsModel {
/**
* Constructor
*
* @param widget The widget to search in
* @param parser Markdown parser
* @param sanitizer Sanitizer
* @param configuration Default model configuration
*/
constructor(widget, parser, sanitizer, configuration) {
super(widget, configuration);
this.parser = parser;
this.sanitizer = sanitizer;
/**
* Mapping between configuration options and notebook metadata.
*
* If it starts with `!`, the boolean value of the configuration option is
* opposite to the one stored in metadata.
* If it contains `/`, the metadata data is nested.
*/
this.configMetadataMap = {
numberHeaders: ['toc-autonumbering', 'toc/number_sections'],
numberingH1: ['!toc/skip_h1_title'],
baseNumbering: ['toc/base_numbering']
};
this._runningCells = new Array();
this._errorCells = new Array();
this._cellToHeadingIndex = new WeakMap();
void widget.context.ready.then(() => {
// Load configuration from metadata
this.setConfiguration({});
});
this.widget.context.model.metadataChanged.connect(this.onMetadataChanged, this);
this.widget.content.activeCellChanged.connect(this.onActiveCellChanged, this);
NotebookActions.executionScheduled.connect(this.onExecutionScheduled, this);
NotebookActions.executed.connect(this.onExecuted, this);
NotebookActions.outputCleared.connect(this.onOutputCleared, this);
this.headingsChanged.connect(this.onHeadingsChanged, this);
}
/**
* Type of document supported by the model.
*
* #### Notes
* A `data-document-type` attribute with this value will be set
* on the tree view `.jp-TableOfContents-content[data-document-type="..."]`
*/
get documentType() {
return 'notebook';
}
/**
* Whether the model gets updated even if the table of contents panel
* is hidden or not.
*/
get isAlwaysActive() {
return true;
}
/**
* List of configuration options supported by the model.
*/
get supportedOptions() {
return [
'baseNumbering',
'maximalDepth',
'numberingH1',
'numberHeaders',
'includeOutput',
'syncCollapseState'
];
}
/**
* Get the headings of a given cell.
*
* @param cell Cell
* @returns The associated headings
*/
getCellHeadings(cell) {
const headings = new Array();
let headingIndex = this._cellToHeadingIndex.get(cell);
if (headingIndex !== undefined) {
const candidate = this.headings[headingIndex];
headings.push(candidate);
while (this.headings[headingIndex - 1] &&
this.headings[headingIndex - 1].cellRef === candidate.cellRef) {
headingIndex--;
headings.unshift(this.headings[headingIndex]);
}
}
return headings;
}
/**
* Dispose the object
*/
dispose() {
var _a, _b, _c;
if (this.isDisposed) {
return;
}
this.headingsChanged.disconnect(this.onHeadingsChanged, this);
(_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);
(_c = this.widget.content) === null || _c === void 0 ? void 0 : _c.activeCellChanged.disconnect(this.onActiveCellChanged, this);
NotebookActions.executionScheduled.disconnect(this.onExecutionScheduled, this);
NotebookActions.executed.disconnect(this.onExecuted, this);
NotebookActions.outputCleared.disconnect(this.onOutputCleared, this);
this._runningCells.length = 0;
this._errorCells.length = 0;
super.dispose();
}
/**
* Model configuration setter.
*
* @param c New configuration
*/
setConfiguration(c) {
// Ensure configuration update
const metadataConfig = this.loadConfigurationFromMetadata();
super.setConfiguration({ ...this.configuration, ...metadataConfig, ...c });
}
/**
* Callback on heading collapse.
*
* @param options.heading The heading to change state (all headings if not provided)
* @param options.collapsed The new collapsed status (toggle existing status if not provided)
*/
toggleCollapse(options) {
super.toggleCollapse(options);
this.updateRunningStatus(this.headings);
}
/**
* Produce the headings for a document.
*
* @returns The list of new headings or `null` if nothing needs to be updated.
*/
getHeadings() {
const cells = this.widget.content.widgets;
const headings = [];
const documentLevels = new Array();
// Generate headings by iterating through all notebook cells...
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
const model = cell.model;
switch (model.type) {
case 'code': {
// Collapsing cells is incompatible with output headings
if (!this.configuration.syncCollapseState &&
this.configuration.includeOutput) {
headings.push(...TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map(heading => {
return {
...heading,
cellRef: cell,
collapsed: false,
isRunning: RunningStatus.Idle
};
}));
}
break;
}
case 'markdown': {
const cellHeadings = TableOfContentsUtils.filterHeadings(cell.headings, this.configuration, documentLevels).map((heading, index) => {
return {
...heading,
cellRef: cell,
collapsed: false,
isRunning: RunningStatus.Idle
};
});
// If there are multiple headings, only collapse the highest heading (i.e. minimal level)
// consistent with the cell.headingInfo
if (this.configuration.syncCollapseState &&
cell.headingCollapsed) {
const minLevel = Math.min(...cellHeadings.map(h => h.level));
const minHeading = cellHeadings.find(h => h.level === minLevel);
minHeading.collapsed = cell.headingCollapsed;
}
headings.push(...cellHeadings);
break;
}
}
if (headings.length > 0) {
this._cellToHeadingIndex.set(cell, headings.length - 1);
}
else {
// If no headings were found, remove the cell from the map
this._cellToHeadingIndex.delete(cell);
}
}
this.updateRunningStatus(headings);
return Promise.resolve(headings);
}
/**
* Test if two headings are equal or not.
*
* @param heading1 First heading
* @param heading2 Second heading
* @returns Whether the headings are equal.
*/
isHeadingEqual(heading1, heading2) {
return (super.isHeadingEqual(heading1, heading2) &&
heading1.cellRef === heading2.cellRef);
}
/**
* Read table of content configuration from notebook metadata.
*
* @returns ToC configuration from metadata
*/
loadConfigurationFromMetadata() {
const nbModel = this.widget.content.model;
const newConfig = {};
if (nbModel) {
for (const option in this.configMetadataMap) {
const keys = this.configMetadataMap[option];
for (const k of keys) {
let key = k;
const negate = key[0] === '!';
if (negate) {
key = key.slice(1);
}
const keyPath = key.split('/');
let value = nbModel.getMetadata(keyPath[0]);
for (let p = 1; p < keyPath.length; p++) {
value = (value !== null && value !== void 0 ? value : {})[keyPath[p]];
}
if (value !== undefined) {
if (typeof value === 'boolean' && negate) {
value = !value;
}
newConfig[option] = value;
}
}
}
}
return newConfig;
}
onActiveCellChanged(notebook, cell) {
// Highlight the first title as active (if multiple titles are in the same cell)
const activeHeading = this.getCellHeadings(cell)[0];
this.setActiveHeading(activeHeading !== null && activeHeading !== void 0 ? activeHeading : null, false);
}
onHeadingsChanged() {
if (this.widget.content.activeCell) {
this.onActiveCellChanged(this.widget.content, this.widget.content.activeCell);
}
}
onExecuted(_, args) {
this._runningCells.forEach((cell, index) => {
var _a;
if (cell === args.cell) {
this._runningCells.splice(index, 1);
const headingIndex = this._cellToHeadingIndex.get(cell);
if (headingIndex !== undefined) {
const heading = this.headings[headingIndex];
// when the execution is not successful but errorName is undefined,
// the execution is interrupted by previous cells
if (args.success || ((_a = args.error) === null || _a === void 0 ? void 0 : _a.errorName) === undefined) {
heading.isRunning = RunningStatus.Idle;
return;
}
heading.isRunning = RunningStatus.Error;
if (!this._errorCells.includes(cell)) {
this._errorCells.push(cell);
}
}
}
});
this.updateRunningStatus(this.headings);
this.stateChanged.emit();
}
onExecutionScheduled(_, args) {
if (!this._runningCells.includes(args.cell)) {
this._runningCells.push(args.cell);
}
this._errorCells.forEach((cell, index) => {
if (cell === args.cell) {
this._errorCells.splice(index, 1);
}
});
this.updateRunningStatus(this.headings);
this.stateChanged.emit();
}
onOutputCleared(_, args) {
this._errorCells.forEach((cell, index) => {
if (cell === args.cell) {
this._errorCells.splice(index, 1);
const headingIndex = this._cellToHeadingIndex.get(cell);
if (headingIndex !== undefined) {
const heading = this.headings[headingIndex];
heading.isRunning = RunningStatus.Idle;
}
}
});
this.updateRunningStatus(this.headings);
this.stateChanged.emit();
}
onMetadataChanged() {
this.setConfiguration({});
}
updateRunningStatus(headings) {
// Update isRunning
this._runningCells.forEach((cell, index) => {
const headingIndex = this._cellToHeadingIndex.get(cell);
if (headingIndex !== undefined) {
const heading = this.headings[headingIndex];
// Running is prioritized over Scheduled, so if a heading is
// running don't change status
if (heading.isRunning !== RunningStatus.Running) {
heading.isRunning =
index > 0 ? RunningStatus.Scheduled : RunningStatus.Running;
}
}
});
this._errorCells.forEach((cell, index) => {
const headingIndex = this._cellToHeadingIndex.get(cell);
if (headingIndex !== undefined) {
const heading = this.headings[headingIndex];
// Running and Scheduled are prioritized over Error, so only if
// a heading is idle will it be set to Error
if (heading.isRunning === RunningStatus.Idle) {
heading.isRunning = RunningStatus.Error;
}
}
});
let globalIndex = 0;
while (globalIndex < headings.length) {
const heading = headings[globalIndex];
globalIndex++;
if (heading.collapsed) {
const maxIsRunning = Math.max(heading.isRunning, getMaxIsRunning(headings, heading.level));
heading.dataset = {
...heading.dataset,
'data-running': maxIsRunning.toString()
};
}
else {
heading.dataset = {
...heading.dataset,
'data-running': heading.isRunning.toString()
};
}
}
function getMaxIsRunning(headings, collapsedLevel) {
let maxIsRunning = RunningStatus.Idle;
while (globalIndex < headings.length) {
const heading = headings[globalIndex];
heading.dataset = {
...heading.dataset,
'data-running': heading.isRunning.toString()
};
if (heading.level > collapsedLevel) {
globalIndex++;
maxIsRunning = Math.max(heading.isRunning, maxIsRunning);
if (heading.collapsed) {
maxIsRunning = Math.max(maxIsRunning, getMaxIsRunning(headings, heading.level));
heading.dataset = {
...heading.dataset,
'data-running': maxIsRunning.toString()
};
}
}
else {
break;
}
}
return maxIsRunning;
}
}
}
/**
* Table of content model factory for Notebook files.
*/
export class NotebookToCFactory extends TableOfContentsFactory {
/**
* Constructor
*
* @param tracker Widget tracker
* @param parser Markdown parser
* @param sanitizer Sanitizer
*/
constructor(tracker, parser, sanitizer) {
super(tracker);
this.parser = parser;
this.sanitizer = sanitizer;
this._scrollToTop = true;
}
/**
* Whether to scroll the active heading to the top
* of the document or not.
*/
get scrollToTop() {
return this._scrollToTop;
}
set scrollToTop(v) {
this._scrollToTop = v;
}
/**
* Create a new table of contents model for the widget
*
* @param widget - widget
* @param configuration - Table of contents configuration
* @returns The table of contents model
*/
_createNew(widget, configuration) {
const model = new NotebookToCModel(widget, this.parser, this.sanitizer, configuration);
// Connect model signals to notebook panel
let headingToElement = new WeakMap();
const onActiveHeadingChanged = (model, heading) => {
if (heading) {
const onCellInViewport = async (cell) => {
if (!cell.inViewport) {
// Bail early
return;
}
const el = headingToElement.get(heading);
if (el) {
if (this.scrollToTop) {
el.scrollIntoView({ block: 'start' });
}
else {
const widgetBox = widget.content.node.getBoundingClientRect();
const elementBox = el.getBoundingClientRect();
if (elementBox.top > widgetBox.bottom ||
elementBox.bottom < widgetBox.top) {
el.scrollIntoView({ block: 'center' });
}
}
}
else {
console.debug('scrolling to heading: using fallback strategy');
await widget.content.scrollToItem(widget.content.activeCellIndex, this.scrollToTop ? 'start' : undefined, 0);
}
};
const cell = heading.cellRef;
const cells = widget.content.widgets;
const idx = cells.indexOf(cell);
// Switch to command mode to avoid entering Markdown cell in edit mode
// if the document was in edit mode
if (cell.model.type == 'markdown' && widget.content.mode != 'command') {
widget.content.mode = 'command';
}
widget.content.activeCellIndex = idx;
if (cell.inViewport) {
onCellInViewport(cell).catch(reason => {
console.error(`Fail to scroll to cell to display the required heading (${reason}).`);
});
}
else {
widget.content
.scrollToItem(idx, this.scrollToTop ? 'start' : undefined)
.then(() => {
return onCellInViewport(cell);
})
.catch(reason => {
console.error(`Fail to scroll to cell to display the required heading (${reason}).`);
});
}
}
};
const findHeadingElement = (cell) => {
model.getCellHeadings(cell).forEach(async (heading) => {
var _a, _b, _c;
const elementId = await getIdForHeading(heading, this.parser, this.sanitizer);
const attribute = ((_a = this.sanitizer.allowNamedProperties) !== null && _a !== void 0 ? _a : false)
? 'id'
: 'data-jupyter-id';
const selector = elementId
? `h${heading.level}[${attribute}="${CSS.escape(elementId)}"]`
: `h${heading.level}`;
if (heading.outputIndex !== undefined) {
// Code cell
headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.outputArea.widgets[heading.outputIndex].node, selector, (_b = heading.prefix) !== null && _b !== void 0 ? _b : ''));
}
else {
headingToElement.set(heading, TableOfContentsUtils.addPrefix(heading.cellRef.node, selector, (_c = heading.prefix) !== null && _c !== void 0 ? _c : ''));
}
});
};
const onHeadingsChanged = (model) => {
if (!this.parser) {
return;
}
// Clear all numbering items
TableOfContentsUtils.clearNumbering(widget.content.node);
// Create a new mapping
headingToElement = new WeakMap();
widget.content.widgets.forEach(cell => {
findHeadingElement(cell);
});
};
const onHeadingCollapsed = (_, heading) => {
var _a, _b, _c, _d;
if (model.configuration.syncCollapseState) {
if (heading !== null) {
const cell = heading.cellRef;
if (cell.headingCollapsed !== ((_a = heading.collapsed) !== null && _a !== void 0 ? _a : false)) {
cell.headingCollapsed = (_b = heading.collapsed) !== null && _b !== void 0 ? _b : false;
}
}
else {
const collapseState = (_d = (_c = model.headings[0]) === null || _c === void 0 ? void 0 : _c.collapsed) !== null && _d !== void 0 ? _d : false;
widget.content.widgets.forEach(cell => {
if (cell instanceof MarkdownCell) {
if (cell.headingInfo.level >= 0) {
cell.headingCollapsed = collapseState;
}
}
});
}
}
};
const onCellCollapsed = (_, cell) => {
if (model.configuration.syncCollapseState) {
const h = model.getCellHeadings(cell)[0];
if (h) {
model.toggleCollapse({
heading: h,
collapsed: cell.headingCollapsed
});
}
}
};
const onCellInViewportChanged = (_, cell) => {
if (cell.inViewport) {
findHeadingElement(cell);
}
else {
// Needed to remove prefix in cell outputs
TableOfContentsUtils.clearNumbering(cell.node);
}
};
void widget.context.ready.then(() => {
onHeadingsChanged(model);
model.activeHeadingChanged.connect(onActiveHeadingChanged);
model.headingsChanged.connect(onHeadingsChanged);
model.collapseChanged.connect(onHeadingCollapsed);
widget.content.cellCollapsed.connect(onCellCollapsed);
widget.content.cellInViewportChanged.connect(onCellInViewportChanged);
widget.disposed.connect(() => {
model.activeHeadingChanged.disconnect(onActiveHeadingChanged);
model.headingsChanged.disconnect(onHeadingsChanged);
model.collapseChanged.disconnect(onHeadingCollapsed);
widget.content.cellCollapsed.disconnect(onCellCollapsed);
widget.content.cellInViewportChanged.disconnect(onCellInViewportChanged);
});
});
return model;
}
}
/**
* Get the element id for an heading
* @param heading Heading
* @param parser The markdownparser
* @returns The element id
*/
export async function getIdForHeading(heading, parser, sanitizer) {
let elementId = null;
if (heading.type === Cell.HeadingType.Markdown) {
elementId = await TableOfContentsUtils.Markdown.getHeadingId(parser,
// Type from TableOfContentsUtils.Markdown.IMarkdownHeading
heading.raw, heading.level, sanitizer);
}
else if (heading.type === Cell.HeadingType.HTML) {
// Type from TableOfContentsUtils.IHTMLHeading
elementId = heading.id;
}
return elementId;
}
//# sourceMappingURL=toc.js.map