Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@jupyterlab/docmanager": "^0.15.5",
"@jupyterlab/fileeditor": "^0.15.4",
"@jupyterlab/notebook": "^0.15.7",
"@jupyterlab/rendermime": "^0.15.4",
"@phosphor/algorithm": "^1.1.2",
"@phosphor/coreutils": "^1.3.0",
"@phosphor/messaging": "^1.2.2",
Expand Down
84 changes: 20 additions & 64 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import {

import {IDocumentManager} from '@jupyterlab/docmanager';

import {FileEditor, IEditorTracker} from '@jupyterlab/fileeditor';
import {IEditorTracker} from '@jupyterlab/fileeditor';

import {INotebookTracker, NotebookPanel} from '@jupyterlab/notebook';
import {INotebookTracker} from '@jupyterlab/notebook';

import {each} from '@phosphor/algorithm';
import {IRenderMimeRegistry} from '@jupyterlab/rendermime';

import {IHeading, TableOfContents} from './toc';
import {TableOfContents} from './toc';

import {
createLatexGenerator,
createNotebookGenerator,
createMarkdownGenerator,
} from './generators';

import {ITableOfContentsRegistry, TableOfContentsRegistry} from './registry';

Expand All @@ -33,12 +39,11 @@ const extension: JupyterLabPlugin<ITableOfContentsRegistry> = {
IEditorTracker,
ILayoutRestorer,
INotebookTracker,
IRenderMimeRegistry,
],
activate: activateTOC,
};



/**
* Activate the ToC extension.
*/
Expand All @@ -48,9 +53,10 @@ function activateTOC(
editorTracker: IEditorTracker,
restorer: ILayoutRestorer,
notebookTracker: INotebookTracker,
rendermime: IRenderMimeRegistry,
): ITableOfContentsRegistry {
// Create the ToC widget.
const toc = new TableOfContents(docmanager);
const toc = new TableOfContents({ docmanager, rendermime });

// Create the ToC registry.
const registry = new TableOfContentsRegistry();
Expand All @@ -64,67 +70,17 @@ function activateTOC(
restorer.add(toc, 'juputerlab-toc');

// Create a notebook TableOfContentsRegistry.IGenerator
const notebookGenerator: TableOfContentsRegistry.IGenerator<NotebookPanel> = {
tracker: notebookTracker,
generate: panel => {
let headings: IHeading[] = [];
each(panel.notebook.widgets, cell => {
let model = cell.model;
if (model.type !== 'markdown') {
return;
}
const lines = model.value.text
.split('\n')
.filter(line => line[0] === '#');
lines.forEach(line => {
const level = line.search(/[^#]/);
const text = line.slice(level).replace(/\[(.+)\]\(.+\)/g, '$1');
const onClick = () => {
cell.node.scrollIntoView();
};
headings.push({text, level, onClick});
});
});
return headings;
},
};
const notebookGenerator = createNotebookGenerator(notebookTracker);
registry.addGenerator(notebookGenerator);

// Create an markdown editor TableOfContentsRegistry.IGenerator
const markdownGenerator: TableOfContentsRegistry.IGenerator<FileEditor> = {
tracker: editorTracker,
isEnabled: editor => {
let mime = editor.model.mimeType;
return (
mime === 'text/x-ipthongfm' ||
mime === 'text/x-markdown' ||
mime === 'text/x-gfm' ||
mime === 'text/markdown'
);
},
generate: editor => {
let headings: IHeading[] = [];
let model = editor.model;
const lines = model.value.text
.split('\n')
.map((value, idx) => {
return {value, idx};
})
.filter(line => line.value[0] === '#');
lines.forEach(line => {
const level = line.value.search(/[^#]/);
const text = line.value.slice(level).replace(/\[(.+)\]\(.+\)/g, '$1');
const onClick = () => {
editor.editor.setCursorPosition({line: line.idx, column: 0});
};
headings.push({text, level, onClick});
});
return headings;
},
};

registry.addGenerator(notebookGenerator);
const markdownGenerator = createMarkdownGenerator(editorTracker);
registry.addGenerator(markdownGenerator);

// Create a latex editor TableOfContentsRegistry.IGenerator
const latexGenerator = createLatexGenerator(editorTracker);
registry.addGenerator(latexGenerator);

// Change the ToC when the active widget changes.
app.shell.currentChanged.connect(() => {
let widget = app.shell.currentWidget;
Expand Down
180 changes: 180 additions & 0 deletions src/generators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {FileEditor, IEditorTracker} from '@jupyterlab/fileeditor';

import {INotebookTracker, NotebookPanel} from '@jupyterlab/notebook';

import {each} from '@phosphor/algorithm';

import {TableOfContentsRegistry} from './registry';

import {IHeading} from './toc';

/**
* Create a TOC generator for notebooks.
*
* @param tracker: A notebook tracker.
*
* @returns A TOC generator that can parse notebooks.
*/
export function createNotebookGenerator(
tracker: INotebookTracker,
): TableOfContentsRegistry.IGenerator<NotebookPanel> {
return {
tracker,
usesLatex: true,
generate: panel => {
let headings: IHeading[] = [];
each(panel.notebook.widgets, cell => {
let model = cell.model;
// Only parse markdown cells
if (model.type !== 'markdown') {
return;
}

// Get the lines that start with a '#'
const lines = model.value.text
.split('\n')
.filter(line => line[0] === '#');

// Iterate over the lines to get the header level and
// the text for the line.
lines.forEach(line => {
const level = line.search(/[^#]/);
// Take special care to parse markdown links into raw text.
const text = line.slice(level).replace(/\[(.+)\]\(.+\)/g, '$1');
const onClick = () => {
cell.node.scrollIntoView();
};
headings.push({text, level, onClick});
});
});
return headings;
},
};
}

/**
* Create a TOC generator for markdown files.
*
* @param tracker: A file editor tracker.
*
* @returns A TOC generator that can parse markdown files.
*/
export function createMarkdownGenerator(
tracker: IEditorTracker,
): TableOfContentsRegistry.IGenerator<FileEditor> {
return {
tracker,
usesLatex: true,
isEnabled: editor => {
// Only enable this if the editor mimetype matches
// one of a few markdown variants.
let mime = editor.model.mimeType;
return (
mime === 'text/x-ipthongfm' ||
mime === 'text/x-markdown' ||
mime === 'text/x-gfm' ||
mime === 'text/markdown'
);
},
generate: editor => {
let headings: IHeading[] = [];
let model = editor.model;

// Split the text into lines, with the line number for each.
// We will use the line number to scroll the editor upon
// TOC item click.
const lines = model.value.text
.split('\n')
.map((value, idx) => {
return {value, idx};
})
.filter(line => line.value[0] === '#');

// Iterate over the lines to get the header level and
// the text for the line.
lines.forEach(line => {
const level = line.value.search(/[^#]/);
// Take special care to parse markdown links into raw text.
const text = line.value.slice(level).replace(/\[(.+)\]\(.+\)/g, '$1');
const onClick = () => {
editor.editor.setCursorPosition({line: line.idx, column: 0});
};
headings.push({text, level, onClick});
});
return headings;
},
};
}

/**
* Create a TOC generator for LaTeX files.
*
* @param tracker: A file editor tracker.
*
* @returns A TOC generator that can parse LaTeX files.
*/
export function createLatexGenerator(
tracker: IEditorTracker,
): TableOfContentsRegistry.IGenerator<FileEditor> {
return {
tracker,
usesLatex: true,
isEnabled: editor => {
// Only enable this if the editor mimetype matches
// one of a few LaTeX variants.
let mime = editor.model.mimeType;
return mime === 'text/x-latex' || mime === 'text/x-stex';
},
generate: editor => {
let headings: IHeading[] = [];
let model = editor.model;

// Split the text into lines, with the line number for each.
// We will use the line number to scroll the editor upon
// TOC item click.
const lines = model.value.text.split('\n').map((value, idx) => {
return {value, idx};
});

// Iterate over the lines to get the header level and
// the text for the line.
lines.forEach(line => {
const match = line.value.match(
/^\s*\\(section|subsection|subsubsection){(.+)}/,
);
if (match) {
const level = Private.LatexLevels[match[1]];
const text = match[2];
const onClick = () => {
editor.editor.setCursorPosition({line: line.idx, column: 0});
};
headings.push({text, level, onClick});
}
});
return headings;
},
};
}

/**
* A private namespace for miscellaneous things.
*/
namespace Private {
/**
* A mapping from LaTeX section headers to HTML header
* levels. `part` and `chapter` are less common in my experience,
* so assign them to header level 1.
*/
export const LatexLevels: {[label: string]: number} = {
part: 1, // Only available for report and book classes
chapter: 1, // Only available for report and book classes
section: 1,
subsection: 2,
subsubsection: 3,
paragraph: 4,
subparagraph: 5,
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

export * from './toc';
export * from './registry';
export * from './generators';
7 changes: 7 additions & 0 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export namespace TableOfContentsRegistry {
*/
isEnabled ?: (widget: W) => boolean;

/**
* Whether the document uses LaTeX typesetting.
*
* Defaults to `false`.
*/
usesLatex?: boolean;

/**
* A function that takes the widget, and produces
* a list of headings.
Expand Down
29 changes: 26 additions & 3 deletions src/toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {ActivityMonitor, PathExt} from '@jupyterlab/coreutils';

import {IDocumentManager} from '@jupyterlab/docmanager';

import {IRenderMimeRegistry} from '@jupyterlab/rendermime';

import {Message} from '@phosphor/messaging';

import {Widget} from '@phosphor/widgets';
Expand All @@ -26,9 +28,10 @@ export class TableOfContents extends Widget {
/**
* Create a new table of contents.
*/
constructor(docmanager: IDocumentManager) {
constructor(options: TableOfContents.IOptions) {
super();
this._docmanager = docmanager;
this._docmanager = options.docmanager;
this._rendermime = options.rendermime;
}

/**
Expand Down Expand Up @@ -92,7 +95,11 @@ export class TableOfContents extends Widget {
title = PathExt.basename(context.localPath);
}
}
ReactDOM.render(<TOCTree title={title} toc={toc} />, this.node);
ReactDOM.render(<TOCTree title={title} toc={toc} />, this.node, () => {
if (this._current.generator.usesLatex === true) {
this._rendermime.latexTypesetter.typeset(this.node);
}
});
}

/**
Expand All @@ -102,6 +109,7 @@ export class TableOfContents extends Widget {
this.update();
}

private _rendermime: IRenderMimeRegistry;
private _docmanager: IDocumentManager;
private _current: TableOfContents.ICurrentWidget | null;
private _monitor: ActivityMonitor<any, any> | null;
Expand All @@ -111,6 +119,21 @@ export class TableOfContents extends Widget {
* A namespace for TableOfContents statics.
*/
export namespace TableOfContents {
/**
* Options for the constructor.
*/
export interface IOptions {
/**
* The document manager for the application.
*/
docmanager: IDocumentManager;

/**
* The rendermime for the application.
*/
rendermime: IRenderMimeRegistry;
}

/**
* A type representing a tuple of a widget,
* and a generator that knows how to generate
Expand Down