Repository: ian-r-rose/jupyterlab-toc Branch: master Commit: d1f81f37aad2 Files: 29 Total size: 88.9 KB Directory structure: gitextract_6lby2670/ ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── extension.ts │ ├── generators/ │ │ ├── index.ts │ │ ├── latexgenerator.ts │ │ ├── markdowndocgenerator/ │ │ │ ├── index.ts │ │ │ ├── itemrenderer.tsx │ │ │ ├── optionsmanager.ts │ │ │ └── toolbargenerator.tsx │ │ ├── notebookgenerator/ │ │ │ ├── codemirror.tsx │ │ │ ├── heading.ts │ │ │ ├── index.ts │ │ │ ├── itemrenderer.tsx │ │ │ ├── optionsmanager.ts │ │ │ ├── tagstool/ │ │ │ │ ├── index.tsx │ │ │ │ ├── tag.tsx │ │ │ │ └── tagslist.tsx │ │ │ └── toolbargenerator.tsx │ │ └── shared.ts │ ├── index.ts │ ├── registry.ts │ └── toc.tsx ├── style/ │ └── index.css ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.bundle.* lib/ node_modules/ *.egg-info/ .ipynb_checkpoints package-lock.json ================================================ FILE: .prettierignore ================================================ node_modules **/lib ================================================ FILE: .prettierrc ================================================ { "singleQuote": true } ================================================ FILE: LICENSE ================================================ Copyright (c) 2017, Project Jupyter Contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # jupyterlab-toc A Table of Contents extension for JupyterLab. This auto-generates a table of contents in the left area when you have a notebook or markdown document open. The entries are clickable, and scroll the document to the heading in question. Here is an animation showing the extension's use, with a notebook from the [Python Data Science Handbook](https://github.com/jakevdp/PythonDataScienceHandbook): ![Table of Contents](toc.gif 'Table of Contents') ## Prerequisites - JupyterLab 1.0 ## Installation ```bash jupyter labextension install @jupyterlab/toc ``` ## Development For a development install, do the following in the repository directory: ```bash jlpm install jlpm run build jupyter labextension install . ``` You can then run JupyterLab in watch mode to automatically pick up changes to `@jupyterlab/toc`. Open a terminal in the `@jupyterlab/toc` repository directory and enter ```bash jlpm run watch ``` Then launch JupyterLab using ```bash jupyter lab --watch ``` This will automatically recompile `@jupyterlab/toc` upon changes, and JupyterLab will rebuild itself. You should then be able to refresh the page and see your changes. ================================================ FILE: package.json ================================================ { "name": "@jupyterlab/toc", "version": "1.0.0-pre.1", "private": false, "description": "Table of Contents extension for JupyterLab", "keywords": [ "jupyter", "jupyterlab", "jupyterlab-extension" ], "homepage": "https://github.com/jupyterlab/jupyterlab-toc", "bugs": { "url": "https://github.com/jupyterlab/jupyterlab-toc/issues" }, "license": "BSD-3-Clause", "author": "Project Jupyter", "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" ], "main": "lib/index.js", "types": "lib/index.d.ts", "repository": { "type": "git", "url": "https://github.com/jupyterlab/jupyterlab-toc.git" }, "scripts": { "build": "tsc", "clean": "rimraf lib", "precommit": "lint-staged", "prettier": "prettier --write '**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}'", "watch": "tsc -w" }, "dependencies": { "@jupyterlab/application": "^1.0.0-alpha.6", "@jupyterlab/apputils": "^1.0.0-alpha.6", "@jupyterlab/cells": "^1.0.0-alpha.6", "@jupyterlab/coreutils": "3.0.0-alpha.6", "@jupyterlab/docmanager": "^1.0.0-alpha.6", "@jupyterlab/docregistry": "^1.0.0-alpha.6", "@jupyterlab/fileeditor": "^1.0.0-alpha.6", "@jupyterlab/markdownviewer": "^1.0.0-alpha.6", "@jupyterlab/notebook": "^1.0.0-alpha.7", "@jupyterlab/rendermime": "^1.0.0-alpha.6", "@phosphor/algorithm": "^1.1.2", "@phosphor/coreutils": "^1.3.0", "@phosphor/messaging": "^1.2.2", "@phosphor/widgets": "^1.6.0", "react": "~16.4.2", "react-dom": "~16.4.2" }, "devDependencies": { "@types/react": "~16.4.13", "@types/react-dom": "~16.0.5", "husky": "^0.14.3", "lint-staged": "^7.2.0", "prettier": "^1.13.7", "rimraf": "^2.6.1", "tslint": "^5.10.0", "tslint-config-prettier": "^1.13.0", "tslint-plugin-prettier": "^1.3.0", "typescript": "~3.1.1" }, "jupyterlab": { "extension": "lib/extension.js" }, "lint-staged": { "**/*{.ts,.tsx,.css,.json,.md}": [ "prettier --write", "git add" ] }, "resolutions": { "@types/react": "~16.4.13" }, "publishConfig": { "access": "public" } } ================================================ FILE: src/extension.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ILabShell, ILayoutRestorer, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IDocumentManager } from '@jupyterlab/docmanager'; import { IEditorTracker } from '@jupyterlab/fileeditor'; import { IMarkdownViewerTracker } from '@jupyterlab/markdownviewer'; import { INotebookTracker } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { TableOfContents } from './toc'; import { createLatexGenerator, createNotebookGenerator, createMarkdownGenerator, createRenderedMarkdownGenerator } from './generators'; import { ITableOfContentsRegistry, TableOfContentsRegistry } from './registry'; import '../style/index.css'; /** * Initialization data for the jupyterlab-toc extension. */ const extension: JupyterFrontEndPlugin = { id: 'jupyterlab-toc', autoStart: true, provides: ITableOfContentsRegistry, requires: [ IDocumentManager, IEditorTracker, ILabShell, ILayoutRestorer, IMarkdownViewerTracker, INotebookTracker, IRenderMimeRegistry ], activate: activateTOC }; /** * Activate the ToC extension. */ function activateTOC( app: JupyterFrontEnd, docmanager: IDocumentManager, editorTracker: IEditorTracker, labShell: ILabShell, restorer: ILayoutRestorer, markdownViewerTracker: IMarkdownViewerTracker, notebookTracker: INotebookTracker, rendermime: IRenderMimeRegistry ): ITableOfContentsRegistry { // Create the ToC widget. const toc = new TableOfContents({ docmanager, rendermime }); // Create the ToC registry. const registry = new TableOfContentsRegistry(); // Add the ToC to the left area. toc.title.iconClass = 'jp-TableOfContents-icon jp-SideBar-tabIcon'; toc.title.caption = 'Table of Contents'; toc.id = 'table-of-contents'; labShell.add(toc, 'left', { rank: 700 }); // Add the ToC widget to the application restorer. restorer.add(toc, 'juputerlab-toc'); // Create a notebook TableOfContentsRegistry.IGenerator const notebookGenerator = createNotebookGenerator( notebookTracker, rendermime.sanitizer, toc ); registry.addGenerator(notebookGenerator); // Create an markdown editor TableOfContentsRegistry.IGenerator const markdownGenerator = createMarkdownGenerator( editorTracker, toc, rendermime.sanitizer ); registry.addGenerator(markdownGenerator); // Create an rendered markdown editor TableOfContentsRegistry.IGenerator const renderedMarkdownGenerator = createRenderedMarkdownGenerator( markdownViewerTracker, rendermime.sanitizer, toc ); registry.addGenerator(renderedMarkdownGenerator); // Create a latex editor TableOfContentsRegistry.IGenerator const latexGenerator = createLatexGenerator(editorTracker); registry.addGenerator(latexGenerator); // Change the ToC when the active widget changes. labShell.currentChanged.connect(() => { let widget = app.shell.currentWidget; if (!widget) { return; } let generator = registry.findGeneratorForWidget(widget); if (!generator) { // If the previously used widget is still available, stick with it. // Otherwise, set the current TOC widget to null. if (toc.current && toc.current.widget.isDisposed) { toc.current = null; } return; } toc.current = { widget, generator }; }); return registry; } export default extension; ================================================ FILE: src/generators/index.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. export * from './markdowndocgenerator'; export * from './latexgenerator'; export * from './notebookgenerator'; ================================================ FILE: src/generators/latexgenerator.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { IDocumentWidget } from '@jupyterlab/docregistry'; import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; import { TableOfContentsRegistry } from '../registry'; import { IHeading } from '../toc'; /** * 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> { return { tracker, usesLatex: true, isEnabled: editor => { // Only enable this if the editor mimetype matches // one of a few LaTeX variants. let mime = editor.content.model.mimeType; return mime === 'text/x-latex' || mime === 'text/x-stex'; }, generate: editor => { let headings: IHeading[] = []; let model = editor.content.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.content.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 }; } ================================================ FILE: src/generators/markdowndocgenerator/index.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ISanitizer } from '@jupyterlab/apputils'; import { IDocumentWidget } from '@jupyterlab/docregistry'; import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; import { IMarkdownViewerTracker, MarkdownDocument } from '@jupyterlab/markdownviewer'; import { TableOfContentsRegistry } from '../../registry'; import { generateNumbering, sanitizerOptions, isMarkdown, INumberedHeading } from '../shared'; import { MarkdownDocGeneratorOptionsManager } from './optionsmanager'; import { TableOfContents } from '../../toc'; import { markdownDocItemRenderer } from './itemrenderer'; import { markdownDocGeneratorToolbar } from './toolbargenerator'; /** * 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, widget: TableOfContents, sanitizer: ISanitizer ): TableOfContentsRegistry.IGenerator> { // Create a option manager to manage user settings const options = new MarkdownDocGeneratorOptionsManager(widget, { needsNumbering: true, sanitizer }); return { tracker, usesLatex: true, options: options, toolbarGenerator: () => { return markdownDocGeneratorToolbar(options); }, itemRenderer: (item: INumberedHeading) => { return markdownDocItemRenderer(options, item); }, isEnabled: editor => { // Only enable this if the editor mimetype matches // one of a few markdown variants. return isMarkdown(editor.content.model.mimeType); }, generate: editor => { let numberingDict: { [level: number]: number } = {}; let model = editor.content.model; let onClickFactory = (line: number) => { return () => { editor.content.editor.setCursorPosition({ line, column: 0 }); }; }; return Private.getMarkdownDocHeadings( model.value.text, onClickFactory, numberingDict ); } }; } /** * Create a TOC generator for rendered markdown files. * * @param tracker: A file editor tracker. * * @returns A TOC generator that can parse markdown files. */ export function createRenderedMarkdownGenerator( tracker: IMarkdownViewerTracker, sanitizer: ISanitizer, widget: TableOfContents ): TableOfContentsRegistry.IGenerator { const options = new MarkdownDocGeneratorOptionsManager(widget, { needsNumbering: true, sanitizer }); return { tracker, usesLatex: true, options: options, toolbarGenerator: () => { return markdownDocGeneratorToolbar(options); }, itemRenderer: (item: INumberedHeading) => { return markdownDocItemRenderer(options, item); }, generate: widget => { let numberingDict: { [level: number]: number } = {}; return Private.getRenderedHTMLHeadingsForMarkdownDoc( widget.content.node, sanitizer, numberingDict, options.numbering ); } }; } /** * A private namespace for miscellaneous things. */ namespace Private { export function getMarkdownDocHeadings( text: string, onClickFactory: (line: number) => (() => void), numberingDict: { [level: number]: number } ): INumberedHeading[] { // Split the text into lines. const lines = text.split('\n'); let headings: INumberedHeading[] = []; let inCodeBlock = false; // Iterate over the lines to get the header level and // the text for the line. lines.forEach((line, idx) => { // Don't check for markdown headings if we // are in a code block (demarcated by backticks). if (line.indexOf('```') === 0) { inCodeBlock = !inCodeBlock; } if (inCodeBlock) { return; } // Make an onClick handler for this line. const onClick = onClickFactory(idx); // First test for '#'-style headers. let match = line.match(/^([#]{1,6}) (.*)/); if (match) { const level = match[1].length; // Take special care to parse markdown links into raw text. const text = match[2].replace(/\[(.+)\]\(.+\)/g, '$1'); let numbering = generateNumbering(numberingDict, level); headings.push({ text, numbering, level, onClick }); return; } // Next test for '==='-style headers. match = line.match(/^([=]{2,}|[-]{2,})/); if (match && idx > 0) { const level = match[1][0] === '=' ? 1 : 2; const prev = lines[idx - 1]; // If the previous line is already a '#'-style heading, // then this is not a '===' style heading. const prevMatch = prev.match(/^([#]{1,6}) (.*)/); if (prevMatch) { return; } // Take special care to parse markdown links into raw text. const text = prev.replace(/\[(.+)\]\(.+\)/g, '$1'); let numbering = generateNumbering(numberingDict, level); headings.push({ text, numbering, level, onClick }); return; } // Finally test for HTML headers. This will not catch multiline // headers, nor will it catch multiple headers on the same line. // It should do a decent job of catching many, though. match = line.match(/(.*)<\/h\1>/i); if (match) { const level = parseInt(match[1], 10); const text = match[2]; let numbering = generateNumbering(numberingDict, level); headings.push({ text, numbering, level, onClick }); return; } }); return headings; } /** * Given a HTML DOM element, get the markdown headings * in that string. */ export function getRenderedHTMLHeadingsForMarkdownDoc( node: HTMLElement, sanitizer: ISanitizer, numberingDict: { [level: number]: number }, needsNumbering = true ): INumberedHeading[] { let headings: INumberedHeading[] = []; let headingNodes = node.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (let i = 0; i < headingNodes.length; i++) { const heading = headingNodes[i]; const level = parseInt(heading.tagName[1], 10); let text = heading.textContent ? heading.textContent : ''; let shallHide = !needsNumbering; // Show/hide numbering DOM element based on user settings if (heading.getElementsByClassName('numbering-entry').length > 0) { heading.removeChild( heading.getElementsByClassName('numbering-entry')[0] ); } let html = sanitizer.sanitize(heading.innerHTML, sanitizerOptions); html = html.replace('¶', ''); // Remove the anchor symbol. const onClick = () => { heading.scrollIntoView(); }; // Get the numbering string let numbering = generateNumbering(numberingDict, level); // Generate the DOM element for numbering let numDOM = ''; if (!shallHide) { numDOM = '' + numbering + ''; } // Add DOM numbering element to document heading.innerHTML = numDOM + html; text = text.replace('¶', ''); headings.push({ level, text, numbering, html, onClick }); } return headings; } } ================================================ FILE: src/generators/markdowndocgenerator/itemrenderer.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { MarkdownDocGeneratorOptionsManager } from './optionsmanager'; import { INumberedHeading, sanitizerOptions } from '../shared'; import * as React from 'react'; export function markdownDocItemRenderer( options: MarkdownDocGeneratorOptionsManager, item: INumberedHeading ) { let fontSizeClass = 'toc-level-size-default'; // Render numbering if needed let numbering = item.numbering && options.numbering ? item.numbering : ''; fontSizeClass = 'toc-level-size-' + item.level; let jsx; if (item.html) { jsx = ( ); } else { jsx = {numbering + item.text}; } return jsx; } ================================================ FILE: src/generators/markdowndocgenerator/optionsmanager.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ISanitizer } from '@jupyterlab/apputils'; import { TableOfContentsRegistry } from '../../registry'; import { TableOfContents } from '../../toc'; export class MarkdownDocGeneratorOptionsManager extends TableOfContentsRegistry.IGeneratorOptionsManager { constructor( widget: TableOfContents, options: { needsNumbering: boolean; sanitizer: ISanitizer } ) { super(); this._numbering = options.needsNumbering; this._widget = widget; this.sanitizer = options.sanitizer; } readonly sanitizer: ISanitizer; set numbering(value: boolean) { this._numbering = value; this._widget.update(); } get numbering() { return this._numbering; } // initialize options, will NOT change notebook metadata initializeOptions(numbering: boolean) { this._numbering = numbering; this._widget.update(); } private _numbering: boolean; private _widget: TableOfContents; } ================================================ FILE: src/generators/markdowndocgenerator/toolbargenerator.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { MarkdownDocGeneratorOptionsManager } from './optionsmanager'; import * as React from 'react'; interface INotebookGeneratorToolbarProps {} interface INotebookGeneratorToolbarState { numbering: boolean; } export function markdownDocGeneratorToolbar( options: MarkdownDocGeneratorOptionsManager ) { // Render the toolbar return class extends React.Component< INotebookGeneratorToolbarProps, INotebookGeneratorToolbarState > { constructor(props: INotebookGeneratorToolbarProps) { super(props); this.state = { numbering: false }; options.initializeOptions(false); } render() { const toggleAutoNumbering = () => { options.numbering = !options.numbering; this.setState({ numbering: options.numbering }); }; let numberingIcon = this.state.numbering ? (
toggleAutoNumbering()} >
) : (
toggleAutoNumbering()} >
); return (
{numberingIcon}
); } }; } ================================================ FILE: src/generators/notebookgenerator/codemirror.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import * as React from 'react'; import { ISanitizer } from '@jupyterlab/apputils'; import { INotebookHeading } from './heading'; import { sanitizerOptions } from '../shared'; export interface ICodeComponentProps { sanitizer: ISanitizer; heading: INotebookHeading; } export interface ICodeComponentState { heading: INotebookHeading; } export class CodeComponent extends React.Component< ICodeComponentProps, ICodeComponentState > { constructor(props: ICodeComponentProps) { super(props); this.state = { heading: props.heading }; } componentWillReceiveProps(nextProps: ICodeComponentProps) { this.setState({ heading: nextProps.heading }); } render() { // Grab the rendered CodeMirror DOM in the document, show it in TOC. let html = this.state.heading.cellRef!.editor.host.innerHTML; // Sanitize it to be safe. html = this.props.sanitizer.sanitize(html, sanitizerOptions); return (
); } } ================================================ FILE: src/generators/notebookgenerator/heading.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Cell } from '@jupyterlab/cells'; import { INumberedHeading } from '../shared'; /** * A heading for a notebook cell. */ export interface INotebookHeading extends INumberedHeading { type: 'header' | 'markdown' | 'code'; prompt?: string; cellRef: Cell; hasChild?: boolean; } ================================================ FILE: src/generators/notebookgenerator/index.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ISanitizer } from '@jupyterlab/apputils'; import { CodeCell, CodeCellModel, MarkdownCell, Cell } from '@jupyterlab/cells'; import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { notebookItemRenderer } from './itemrenderer'; import { notebookGeneratorToolbar } from './toolbargenerator'; import { TableOfContentsRegistry } from '../../registry'; import { TableOfContents } from '../../toc'; import { NotebookGeneratorOptionsManager } from './optionsmanager'; import { INotebookHeading } from './heading'; import { generateNumbering, isDOM, isMarkdown, sanitizerOptions } from '../shared'; /** * Create a TOC generator for notebooks. * * @param tracker: A notebook tracker. * * @returns A TOC generator that can parse notebooks. */ export function createNotebookGenerator( tracker: INotebookTracker, sanitizer: ISanitizer, widget: TableOfContents ): TableOfContentsRegistry.IGenerator { // Create a option manager to manage user settings const options = new NotebookGeneratorOptionsManager(widget, tracker, { needsNumbering: false, sanitizer: sanitizer }); return { tracker, usesLatex: true, options: options, toolbarGenerator: () => { return notebookGeneratorToolbar(options, tracker); }, itemRenderer: (item: INotebookHeading) => { return notebookItemRenderer(options, item); }, generate: panel => { let headings: INotebookHeading[] = []; let numberingDict: { [level: number]: number } = {}; let collapseLevel = -1; // Keep track of the previous heading, so it can be // marked as having a child if one is discovered let prevHeading: INotebookHeading | null = null; // Iterate through the cells in the notebook, generating their headings for (let i = 0; i < panel.content.widgets.length; i++) { let cell: Cell = panel.content.widgets[i]; let collapsed = cell.model.metadata.get('toc-hr-collapsed') as boolean; collapsed = collapsed !== undefined ? collapsed : false; let model = cell.model; if (model.type === 'code') { // Code is shown by default, overridden by previously saved settings if (!widget || (widget && options.showCode)) { // Generate the heading and add to headings if appropriate let executionCountNumber = (cell as CodeCell).model .executionCount as number | null; let executionCount = executionCountNumber !== null ? '[' + executionCountNumber + ']: ' : '[ ]: '; let text = (model as CodeCellModel).value.text; const onClickFactory = (line: number) => { return () => { panel.content.activeCellIndex = i; cell.node.scrollIntoView(); }; }; let lastLevel = Private.getLastLevel(headings); let renderedHeading = Private.getCodeCells( text, onClickFactory, executionCount, lastLevel, cell ); [headings, prevHeading] = Private.addMDOrCode( headings, renderedHeading, prevHeading, collapseLevel, options.filtered ); } // Iterate over the code cell outputs to check for MD/HTML for (let j = 0; j < (model as CodeCellModel).outputs.length; j++) { const outputModel = (model as CodeCellModel).outputs.get(j); const dataTypes = Object.keys(outputModel.data); const htmlData = dataTypes.filter(t => isMarkdown(t) || isDOM(t)); if (!htmlData.length) { continue; } // If MD/HTML generate the heading and add to headings if applicable const outputWidget = (cell as CodeCell).outputArea.widgets[j]; const onClickFactory = (el: Element) => { return () => { panel.content.activeCellIndex = i; panel.content.mode = 'command'; el.scrollIntoView(); }; }; let lastLevel = Private.getLastLevel(headings); let numbering = options.numbering; let renderedHeading = Private.getRenderedHTMLHeading( outputWidget.node, onClickFactory, sanitizer, numberingDict, lastLevel, numbering, cell ); [headings, prevHeading, collapseLevel] = Private.processMD( renderedHeading, options.showMarkdown, headings, prevHeading, collapseLevel, options.filtered, collapsed ); } } else if (model.type === 'markdown') { let mdCell = cell as MarkdownCell; let renderedHeading: INotebookHeading | undefined = undefined; let lastLevel = Private.getLastLevel(headings); // If the cell is rendered, generate the ToC items from the HTML if (mdCell.rendered && !mdCell.inputHidden) { const onClickFactory = (el: Element) => { return () => { if (!mdCell.rendered) { panel.content.activeCellIndex = i; el.scrollIntoView(); } else { panel.content.mode = 'command'; cell.node.scrollIntoView(); panel.content.activeCellIndex = i; } }; }; renderedHeading = Private.getRenderedHTMLHeading( cell.node, onClickFactory, sanitizer, numberingDict, lastLevel, options.numbering, cell ); // If not rendered, generate ToC items from the text of the cell } else { const onClickFactory = (line: number) => { return () => { panel.content.activeCellIndex = i; cell.node.scrollIntoView(); }; }; renderedHeading = Private.getMarkdownHeading( model!.value.text, onClickFactory, numberingDict, lastLevel, cell ); } // Add to headings if applicable [headings, prevHeading, collapseLevel] = Private.processMD( renderedHeading, options.showMarkdown, headings, prevHeading, collapseLevel, options.filtered, collapsed ); } } return headings; } }; } namespace Private { /** * Determine whether a heading is filtered out by selected tags. */ export function headingIsFilteredOut( heading: INotebookHeading, tags: string[] ) { if (tags.length === 0) { return false; } if (heading && heading.cellRef) { let cellMetadata = heading.cellRef.model.metadata; let cellTagsData = cellMetadata.get('tags') as string[]; if (cellTagsData) { for (let j = 0; j < cellTagsData.length; j++) { let name = cellTagsData[j]; for (let k = 0; k < tags.length; k++) { if (tags[k] === name) { return false; } } } } } return true; } export function getLastLevel(headings: INotebookHeading[]) { if (headings.length > 0) { let location = headings.length - 1; while (location >= 0) { if (headings[location].type === 'header') { return headings[location].level; } location = location - 1; } } return 0; } export function processMD( renderedHeading: INotebookHeading | undefined, showMarkdown: boolean, headings: INotebookHeading[], prevHeading: INotebookHeading | null, collapseLevel: number, filtered: string[], collapsed: boolean ): [INotebookHeading[], INotebookHeading | null, number] { // If the heading is MD and MD is shown, add to headings if ( renderedHeading && renderedHeading.type === 'markdown' && showMarkdown ) { [headings, prevHeading] = Private.addMDOrCode( headings, renderedHeading, prevHeading, collapseLevel, filtered ); // Otherwise, if the heading is a header, add to headings } else if (renderedHeading && renderedHeading.type === 'header') { [headings, prevHeading, collapseLevel] = Private.addHeader( headings, renderedHeading, prevHeading, collapseLevel, filtered, collapsed ); } return [headings, prevHeading, collapseLevel]; } export function addMDOrCode( headings: INotebookHeading[], renderedHeading: INotebookHeading, prevHeading: INotebookHeading | null, collapseLevel: number, filtered: string[] ): [INotebookHeading[], INotebookHeading | null] { if ( !Private.headingIsFilteredOut(renderedHeading, filtered) && renderedHeading && renderedHeading.text ) { // If there is a previous header, find it and mark hasChild true if (prevHeading && prevHeading.type === 'header') { for (let j = headings.length - 1; j >= 0; j--) { if (headings[j] === prevHeading) { headings[j].hasChild = true; } } } if (collapseLevel < 0) { headings.push(renderedHeading); } prevHeading = renderedHeading; } return [headings, prevHeading]; } export function addHeader( headings: INotebookHeading[], renderedHeading: INotebookHeading, prevHeading: INotebookHeading | null, collapseLevel: number, filtered: string[], collapsed: boolean ): [INotebookHeading[], INotebookHeading | null, number] { if (!Private.headingIsFilteredOut(renderedHeading, filtered)) { // if the previous heading is a header of a higher level, // find it and mark it as having a child if ( prevHeading && prevHeading.type === 'header' && prevHeading.level < renderedHeading.level ) { for (let j = headings.length - 1; j >= 0; j--) { if (headings[j] === prevHeading) { headings[j].hasChild = true; } } } // if the collapse level doesn't include the header, or if there is no // collapsing, add to headings and adjust the collapse level appropriately if (collapseLevel >= renderedHeading.level || collapseLevel < 0) { headings.push(renderedHeading); collapseLevel = collapsed ? renderedHeading.level : -1; } prevHeading = renderedHeading; } else if (prevHeading && renderedHeading.level <= prevHeading.level) { // If header is filtered out and has a previous heading of smaller level, go // back through headings to determine if it has a parent let k = headings.length - 1; let parentHeading = false; while (k >= 0 && parentHeading === false) { if (headings[k].level < renderedHeading.level) { prevHeading = headings[k]; parentHeading = true; } k--; } // If there is no parent, set prevHeading to null and reset collapsing if (!parentHeading) { prevHeading = null; collapseLevel = -1; // Otherwise, reset collapsing appropriately } else { let parentState = headings[k + 1].cellRef.model.metadata.get( 'toc-hr-collapsed' ) as boolean; parentState = parentState !== undefined ? parentState : false; collapseLevel = parentState ? headings[k + 1].level : -1; } } return [headings, prevHeading, collapseLevel]; } /** * Given a string of code, get the code entry. */ export function getCodeCells( text: string, onClickFactory: (line: number) => (() => void), executionCount: string, lastLevel: number, cellRef: Cell ): INotebookHeading { let headings: INotebookHeading[] = []; if (text) { const lines = text.split('\n'); let headingText = ''; let numLines = Math.min(lines.length, 3); for (let i = 0; i < numLines - 1; i++) { headingText = headingText + lines[i] + '\n'; } headingText = headingText + lines[numLines - 1]; const onClick = onClickFactory(0); const level = lastLevel + 1; headings.push({ text: headingText, level, onClick, type: 'code', prompt: executionCount, cellRef: cellRef, hasChild: false }); } return headings[0]; } /** * Given a string of markdown, get the markdown headings in that string. */ export function getMarkdownHeading( text: string, onClickFactory: (line: number) => (() => void), numberingDict: any, lastLevel: number, cellRef: Cell ): INotebookHeading { const lines = text.split('\n'); const line = lines[0]; const line2 = lines.length > 1 ? lines[1] : undefined; const onClick = onClickFactory(0); // First test for '#'-style headers. let match = line.match(/^([#]{1,6}) (.*)/); let match2 = line2 && line2.match(/^([=]{2,}|[-]{2,})/); let match3 = line.match(/(.*)<\/h\1>/i); if (match) { const level = match[1].length; // Take special care to parse markdown links into raw text. const text = match[2].replace(/\[(.+)\]\(.+\)/g, '$1'); let numbering = generateNumbering(numberingDict, level); return { text, level, numbering, onClick, type: 'header', cellRef: cellRef, hasChild: false }; } else if (match2) { // Next test for '==='-style headers. const level = match2[1][0] === '=' ? 1 : 2; // Take special care to parse markdown links into raw text. const text = line.replace(/\[(.+)\]\(.+\)/g, '$1'); let numbering = generateNumbering(numberingDict, level); return { text, level, numbering, onClick, type: 'header', cellRef: cellRef, hasChild: false }; } else if (match3) { // Finally test for HTML headers. This will not catch multiline // headers, nor will it catch multiple headers on the same line. // It should do a decent job of catching many, though. const level = parseInt(match3[1], 10); const text = match3[2]; let numbering = generateNumbering(numberingDict, level); return { text, level, numbering, onClick, type: 'header', cellRef: cellRef, hasChild: false }; } else { return { text: line, level: lastLevel + 1, onClick, type: 'markdown', cellRef: cellRef, hasChild: false }; } } /** * Given an HTML element, generate ToC headings * by finding all the headers and making IHeading objects for them. */ export function getRenderedHTMLHeading( node: HTMLElement, onClickFactory: (el: Element) => (() => void), sanitizer: ISanitizer, numberingDict: { [level: number]: number }, lastLevel: number, needsNumbering = false, cellRef: Cell ): INotebookHeading | undefined { let headingNodes = node.querySelectorAll('h1, h2, h3, h4, h5, h6, p'); if (headingNodes.length > 0) { let markdownCell = headingNodes[0]; if (markdownCell.nodeName.toLowerCase() === 'p') { if (markdownCell.innerHTML) { let html = sanitizer.sanitize( markdownCell.innerHTML, sanitizerOptions ); html = html.replace('¶', ''); return { level: lastLevel + 1, html: html, text: markdownCell.textContent ? markdownCell.textContent : '', onClick: onClickFactory(markdownCell), type: 'markdown', cellRef: cellRef, hasChild: false }; } } else { const heading = headingNodes[0]; const level = parseInt(heading.tagName[1], 10); const text = heading.textContent ? heading.textContent : ''; let shallHide = !needsNumbering; if (heading.getElementsByClassName('numbering-entry').length > 0) { heading.removeChild( heading.getElementsByClassName('numbering-entry')[0] ); } let html = sanitizer.sanitize(heading.innerHTML, sanitizerOptions); html = html.replace('¶', ''); const onClick = onClickFactory(heading); let numbering = generateNumbering(numberingDict, level); let numDOM = ''; if (!shallHide) { numDOM = '' + numbering + ''; } heading.innerHTML = numDOM + html; return { level, text, numbering, html, onClick, type: 'header', cellRef: cellRef, hasChild: false }; } } return undefined; } } ================================================ FILE: src/generators/notebookgenerator/itemrenderer.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CodeComponent } from './codemirror'; import { Cell } from '@jupyterlab/cells'; import { NotebookGeneratorOptionsManager } from './optionsmanager'; import { INotebookHeading } from './heading'; import { sanitizerOptions } from '../shared'; import * as React from 'react'; export function notebookItemRenderer( options: NotebookGeneratorOptionsManager, item: INotebookHeading ) { let jsx; if (item.type === 'markdown' || item.type === 'header') { const collapseOnClick = (cellRef?: Cell) => { let collapsed = cellRef!.model.metadata.get( 'toc-hr-collapsed' ) as boolean; collapsed = collapsed != undefined ? collapsed : false; cellRef!.model.metadata.set('toc-hr-collapsed', !collapsed); options.updateWidget(); }; let fontSizeClass = 'toc-level-size-default'; let numbering = item.numbering && options.numbering ? item.numbering : ''; if (item.type === 'header') { fontSizeClass = 'toc-level-size-' + item.level; } if (item.html && (item.type === 'header' || options.showMarkdown)) { jsx = ( ); // Render the headers if (item.type === 'header') { let collapsed = item.cellRef!.model.metadata.get( 'toc-hr-collapsed' ) as boolean; collapsed = collapsed != undefined ? collapsed : false; // Render the twist button let twistButton = (
{ event.stopPropagation(); collapseOnClick(item.cellRef); }} >
placeholder
); if (collapsed) { twistButton = (
{ event.stopPropagation(); collapseOnClick(item.cellRef); }} >
placeholder
); } // Render the header item jsx = (
{item.hasChild && twistButton} {jsx}
); } } else if (item.type === 'header' || options.showMarkdown) { // Render headers/markdown for plain text jsx = ( {numbering + item.text} ); if (item.type === 'header') { let collapsed = item.cellRef!.model.metadata.get( 'toc-hr-collapsed' ) as boolean; collapsed = collapsed != undefined ? collapsed : false; let twistButton = (
{ event.stopPropagation(); collapseOnClick(item.cellRef); }} >
placeholder
); if (collapsed) { twistButton = (
{ event.stopPropagation(); collapseOnClick(item.cellRef); }} >
placeholder
); } jsx = (
{item.hasChild && twistButton} {jsx}
); } } else { jsx = null; } } else if (item.type === 'code' && options.showCode) { // Render code cells jsx = (
{item.prompt}
); } else { jsx = null; } return jsx; } ================================================ FILE: src/generators/notebookgenerator/optionsmanager.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { ISanitizer } from '@jupyterlab/apputils'; import { INotebookTracker } from '@jupyterlab/notebook'; import { TableOfContentsRegistry } from '../../registry'; import { TableOfContents } from '../../toc'; import { TagsToolComponent } from './tagstool'; export class NotebookGeneratorOptionsManager extends TableOfContentsRegistry.IGeneratorOptionsManager { constructor( widget: TableOfContents, notebook: INotebookTracker, options: { needsNumbering: boolean; sanitizer: ISanitizer; tagTool?: TagsToolComponent; } ) { super(); this._numbering = options.needsNumbering; this._widget = widget; this._notebook = notebook; this.sanitizer = options.sanitizer; this.tagTool = null; this.storeTags = []; } readonly sanitizer: ISanitizer; public tagTool?: TagsToolComponent | null; set notebookMetadata(value: [string, any]) { if (this._notebook.currentWidget != null) { this._notebook.currentWidget.model.metadata.set(value[0], value[1]); } } set numbering(value: boolean) { this._numbering = value; this._widget.update(); this.notebookMetadata = ['toc-autonumbering', this._numbering]; } get numbering() { return this._numbering; } set showCode(value: boolean) { this._showCode = value; this.notebookMetadata = ['toc-showcode', this._showCode]; this._widget.update(); } get showCode() { return this._showCode; } set showMarkdown(value: boolean) { this._showMarkdown = value; this.notebookMetadata = ['toc-showmarkdowntxt', this._showMarkdown]; this._widget.update(); } get showMarkdown() { return this._showMarkdown; } set showTags(value: boolean) { this._showTags = value; this.notebookMetadata = ['toc-showtags', this._showTags]; this._widget.update(); } get showTags() { return this._showTags; } get filtered() { if (this.tagTool) { this._filtered = this.tagTool.getFiltered(); } else if (this.storeTags.length > 0) { this._filtered = this.storeTags; } else { this._filtered = []; } return this._filtered; } set preRenderedToolbar(value: any) { this._preRenderedToolbar = value; } get preRenderedToolbar() { return this._preRenderedToolbar; } updateWidget() { this._widget.update(); } setTagTool(tagTool: TagsToolComponent | null) { this.tagTool = tagTool; } // initialize options, will NOT change notebook metadata initializeOptions( numbering: boolean, showCode: boolean, showMarkdown: boolean, showTags: boolean ) { this._numbering = numbering; this._showCode = showCode; this._showMarkdown = showMarkdown; this._showTags = showTags; this._widget.update(); } private _preRenderedToolbar: any = null; private _filtered: string[] = []; private _numbering: boolean; private _showCode = false; private _showMarkdown = false; private _showTags = false; private _notebook: INotebookTracker; private _widget: TableOfContents; public storeTags: string[]; } ================================================ FILE: src/generators/notebookgenerator/tagstool/index.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { INotebookTracker } from '@jupyterlab/notebook'; import { Cell } from '@jupyterlab/cells'; import { TagListComponent } from './tagslist'; import * as React from 'react'; import { NotebookGeneratorOptionsManager } from '../optionsmanager'; export interface ITagsToolComponentProps { allTagsList: string[]; tracker: INotebookTracker; generatorOptionsRef: NotebookGeneratorOptionsManager; inputFilter: string[]; } export interface ITagsToolComponentState { selected: string[]; } /* * Create a React component that handles state for the tag dropdown */ export class TagsToolComponent extends React.Component< ITagsToolComponentProps, ITagsToolComponentState > { constructor(props: ITagsToolComponentProps) { super(props); this.state = { selected: this.props.inputFilter }; } /* * Manage the selection state of the dropdown, taking in the name of a tag and * whether to add or remove it. */ changeSelectionState = (newState: string, add: boolean) => { if (add) { let selectedTags = this.state.selected; selectedTags.push(newState); this.setState({ selected: selectedTags }); this.filterTags(selectedTags); } else { let selectedTags = this.state.selected; let newSelectedTags: string[] = []; for (let i = 0; i < selectedTags.length; i++) { if (selectedTags[i] !== newState) { newSelectedTags.push(selectedTags[i]); } } if (newSelectedTags.length === 0) { newSelectedTags = []; } this.setState({ selected: newSelectedTags }); this.filterTags(newSelectedTags); } }; public getFiltered() { return this.state.selected; } /* * Deselect all tags in the dropdown and clear filters in the TOC. */ deselectAllTags = () => { this.setState({ selected: [] }); this.props.generatorOptionsRef.updateWidget(); }; /** * Check whether a cell is tagged with a certain string */ containsTag(tag: string, cell: Cell) { if (cell === null) { return false; } let tagList = cell.model.metadata.get('tags') as string[]; if (tagList) { for (let i = 0; i < tagList.length; i++) { if (tagList[i] === tag) { return true; } } return false; } } /* * Tells the generator to filter the TOC by the selected tags. */ filterTags = (selected: string[]) => { this.setState({ selected }); this.props.generatorOptionsRef.updateWidget(); }; updateFilters = () => { let temp: string[] = []; let idx = 0; let needsUpdate = false; for (let i = 0; i < this.state.selected.length; i++) { if ( this.props.allTagsList.indexOf(this.state.selected[i] as string) > -1 ) { temp[idx] = this.state.selected[i]; idx++; } else if (this.props.generatorOptionsRef.showTags === true) { needsUpdate = true; } } if (needsUpdate) { this.filterTags(temp); this.setState({ selected: temp }); } }; componentWillUpdate() { this.updateFilters(); } /* * Render the interior of the tag dropdown. */ render() { let renderedJSX =
No Tags Available
; let filterText; if (this.state.selected.length === 0) { filterText = ( Clear Filters ); } else if (this.state.selected.length === 1) { filterText = ( this.deselectAllTags()} > {' '} Clear 1 Filter{' '} ); } else { filterText = ( this.deselectAllTags()} > {' '} Clear {this.state.selected.length} Filters{' '} ); } if (this.props.allTagsList && this.props.allTagsList.length > 0) { renderedJSX = (
{filterText}
); } return renderedJSX; } } ================================================ FILE: src/generators/notebookgenerator/tagstool/tag.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import * as React from 'react'; export interface ITagComponentProps { selectionStateHandler: (newState: string, add: boolean) => void; selectedTags: string[]; tag: string; } /* * Create a React component containing one tag label */ export abstract class TagComponent extends React.Component { constructor(props: ITagComponentProps) { super(props); } render() { const tag = this.props.tag as string; return (
); } } ================================================ FILE: src/generators/notebookgenerator/tagstool/tagslist.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { TagComponent } from './tag'; import * as React from 'react'; /* * The TagList takes a list of selected tags, a handler to change selection state, * and a list of all tags (strings). */ export interface ITagListComponentProps { selectedTags: string[]; selectionStateHandler: (newState: string, add: boolean) => void; allTagsList: string[] | null; } /* * The TagList state contains a list of selected tags */ export interface ITagListComponentState { selected: string[]; } /* * Create a React component that renders all tags in a list. */ export class TagListComponent extends React.Component< ITagListComponentProps, ITagListComponentState > { constructor(props: ITagListComponentProps) { super(props); this.state = { selected: this.props.selectedTags }; } /* * Toggle whether a tag is selected when it is clicked */ selectedTagWithName = (name: string) => { if (this.props.selectedTags.indexOf(name) >= 0) { this.props.selectionStateHandler(name, false); } else { this.props.selectionStateHandler(name, true); } }; /* * Render a tag, putting it in a TagComponent */ renderElementForTags = (tags: string[]) => { const selectedTags = this.props.selectedTags; const _self = this; return tags.map((tag, index) => { const tagClass = selectedTags.indexOf(tag) >= 0 ? 'toc-selected-tag toc-tag' : 'toc-unselected-tag toc-tag'; return (
{ _self.selectedTagWithName(tag); }} tabIndex={-1} >
); }); }; /* * Render the list of tags in the TOC tags dropdown. */ render() { let allTagsList = this.props.allTagsList; let renderedTagsForAllCells = null; if (allTagsList) { renderedTagsForAllCells = this.renderElementForTags(allTagsList); } return
{renderedTagsForAllCells}
; } } ================================================ FILE: src/generators/notebookgenerator/toolbargenerator.tsx ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { INotebookTracker } from '@jupyterlab/notebook'; import { JSONValue } from '@phosphor/coreutils'; import { NotebookGeneratorOptionsManager } from './optionsmanager'; import * as React from 'react'; import { TagsToolComponent } from './tagstool'; interface INotebookGeneratorToolbarProps {} interface INotebookGeneratorToolbarState { showCode: boolean; showMarkdown: boolean; showTags: boolean; numbering: boolean; } export function notebookGeneratorToolbar( options: NotebookGeneratorOptionsManager, tracker: INotebookTracker ) { // Render the toolbar return class extends React.Component< INotebookGeneratorToolbarProps, INotebookGeneratorToolbarState > { constructor(props: INotebookGeneratorToolbarProps) { super(props); this.tagTool = null; this.state = { showCode: true, showMarkdown: false, showTags: false, numbering: false }; if (tracker.currentWidget) { // Read saved user settings in notebook metadata tracker.currentWidget.context.ready.then(() => { if (tracker.currentWidget) { tracker.currentWidget.content.activeCellChanged.connect(() => { options.updateWidget(); }); let _numbering = tracker.currentWidget.model.metadata.get( 'toc-autonumbering' ) as boolean; let numbering = _numbering != undefined ? _numbering : options.numbering; let _showCode = tracker.currentWidget.model.metadata.get( 'toc-showcode' ) as boolean; let showCode = _showCode != undefined ? _showCode : options.showCode; let _showMarkdown = tracker.currentWidget.model.metadata.get( 'toc-showmarkdowntxt' ) as boolean; let showMarkdown = _showMarkdown != undefined ? _showMarkdown : options.showMarkdown; let _showTags = tracker.currentWidget.model.metadata.get( 'toc-showtags' ) as boolean; let showTags = _showTags != undefined ? _showTags : options.showTags; this.allTags = []; options.initializeOptions( numbering, showCode, showMarkdown, showTags ); this.setState({ showCode: options.showCode, showMarkdown: options.showMarkdown, showTags: options.showTags, numbering: options.numbering }); } }); } } toggleCode = (component: React.Component) => { options.showCode = !options.showCode; this.setState({ showCode: options.showCode }); }; toggleMarkdown = (component: React.Component) => { options.showMarkdown = !options.showMarkdown; this.setState({ showMarkdown: options.showMarkdown }); }; toggleAutoNumbering = () => { options.numbering = !options.numbering; this.setState({ numbering: options.numbering }); }; toggleTagDropdown = () => { if (options.showTags && this.tagTool) { options.storeTags = this.tagTool.state.selected; } options.showTags = !options.showTags; this.setState({ showTags: options.showTags }); }; // Load all tags in the document getTags = () => { let notebook = tracker.currentWidget; if (notebook) { const cells = notebook.model.cells; const tagSet = new Set(); this.allTags = []; for (let i = 0; i < cells.length; i++) { const cell = cells.get(i)!; const tagData = cell.metadata.get('tags') as JSONValue; if (Array.isArray(tagData)) { tagData.forEach((tag: string) => tag && tagSet.add(tag)); } } this.allTags = Array.from(tagSet); } }; render() { let codeIcon = this.state.showCode ? (
this.toggleCode.bind(this)()} >
) : (
this.toggleCode.bind(this)()} >
); let markdownIcon = this.state.showMarkdown ? (
this.toggleMarkdown.bind(this)()} >
) : (
this.toggleMarkdown.bind(this)()} >
); let numberingIcon = this.state.numbering ? (
this.toggleAutoNumbering()} >
) : (
this.toggleAutoNumbering()} >
); let tagDropdown =
; let tagIcon = (
); if (this.state.showTags) { this.getTags(); let tagTool = ( (this.tagTool = tagTool)} /> ); options.setTagTool(this.tagTool); tagDropdown =
{tagTool}
; tagIcon = (
); } return (
{codeIcon} {markdownIcon} {numberingIcon}
this.toggleTagDropdown()} > {tagIcon}
{tagDropdown}
); } allTags: string[]; tagTool: TagsToolComponent | null; }; } ================================================ FILE: src/generators/shared.ts ================================================ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { IHeading } from '../toc'; const VDOM_MIME_TYPE = 'application/vdom.v1+json'; const HTML_MIME_TYPE = 'text/html'; export interface INumberedHeading extends IHeading { numbering?: string | null; } /** * Given a dictionary that keep tracks of the numbering and the level, * update the dictionary. */ function incrementNumberingDict(dict: any, level: number) { let x = level + 1; while (x <= 6) { if (dict[x] != undefined) { dict[x] = undefined; } x++; } if (dict[level] === undefined) { dict[level] = 1; } else { dict[level]++; } } /** * Given a dictionary that keep tracks of the numbering and the current level, * generate the current numbering based on the dictionary and current level. */ export function generateNumbering( numberingDict: { [level: number]: number }, level: number ) { let numbering = undefined; if (numberingDict != null) { incrementNumberingDict(numberingDict, level); numbering = ''; for (let j = 1; j <= level; j++) { numbering += (numberingDict[j] == undefined ? '0' : numberingDict[j]) + '.'; if (j === level) { numbering += ' '; } } } return numbering; } /** * Return whether the mime type is some flavor of markdown. */ export function isMarkdown(mime: string): boolean { return ( mime === 'text/x-ipythongfm' || mime === 'text/x-markdown' || mime === 'text/x-gfm' || mime === 'text/markdown' ); } /** * Return whether the mime type is DOM-ish (html or vdom). */ export function isDOM(mime: string): boolean { return mime === VDOM_MIME_TYPE || mime === HTML_MIME_TYPE; } /** * Allowed HTML tags for the ToC entries. We use this to * sanitize HTML headings, if they are given. We specifically * disallow anchor tags, since we are adding our own. */ export const sanitizerOptions = { allowedTags: [ 'p', 'blockquote', 'b', 'i', 'strong', 'em', 'strike', 'code', 'br', 'div', 'span', 'pre', 'del' ], allowedAttributes: { // Allow "class" attribute for tags. code: ['class'], // Allow "class" attribute for tags. span: ['class'], // Allow "class" attribute for
tags. div: ['class'], // Allow "class" attribute for

tags. p: ['class'], // Allow "class" attribute for

 tags.
    pre: ['class']
  }
};


================================================
FILE: src/index.ts
================================================
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

export * from './toc';
export * from './registry';
export * from './generators';


================================================
FILE: src/registry.ts
================================================
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { IInstanceTracker } from '@jupyterlab/apputils';

import { Token } from '@phosphor/coreutils';

import { Widget } from '@phosphor/widgets';

import { IHeading } from './toc';

/**
 * An interface for a TableOfContentsRegistry.
 */
export interface ITableOfContentsRegistry extends TableOfContentsRegistry {}

/* tslint:disable */
/**
 * The TableOfContentsRegistry token.
 */
export const ITableOfContentsRegistry = new Token(
  'jupyterlab-toc:ITableOfContentsRegistry'
);
/* tslint:enable */

/**
 * A class that keeps track of the different kinds
 * of widgets for which there can be tables-of-contents.
 */
export class TableOfContentsRegistry {
  /**
   * Given a widget, find an IGenerator for it,
   * or undefined if none can be found.
   */
  findGeneratorForWidget(
    widget: Widget
  ): TableOfContentsRegistry.IGenerator | undefined {
    let generator: TableOfContentsRegistry.IGenerator | undefined;
    this._generators.forEach(gen => {
      if (gen.tracker.has(widget)) {
        // If isEnabled is present, check for it.
        if (gen.isEnabled && !gen.isEnabled(widget)) {
          return;
        }
        generator = gen;
      }
    });
    return generator;
  }

  /**
   * Add a new IGenerator to the registry.
   */
  addGenerator(generator: TableOfContentsRegistry.IGenerator): void {
    this._generators.push(generator);
  }

  private _generators: TableOfContentsRegistry.IGenerator[] = [];
}

/**
 * A namespace for TableOfContentsRegistry statics.
 */
export namespace TableOfContentsRegistry {
  /**
   * An interface for an object that knows how to generate a table-of-contents
   * for a type of widget.
   */

  export abstract class IGeneratorOptionsManager {}

  export interface IGenerator {
    /**
     * An instance tracker for the widget.
     */
    tracker: IInstanceTracker;

    /**
     * A function to test whether to generate a ToC for a widget.
     *
     * #### Notes
     * By default is assumed to be enabled if the widget
     * is hosted in `tracker`. However, the user may want to add
     * additional checks. For instance, this can be used to generate
     * a ToC for text files only if they have a given mimeType.
     */
    isEnabled?: (widget: W) => boolean;

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

    /**
     * An object that manage user settings for the generator.
     *
     * Defaults to `undefined`.
     */
    options?: IGeneratorOptionsManager;

    /**
     * A function that generates JSX element for each heading
     *
     * If not given, the default renderer will be used, which renders the text
     */
    itemRenderer?: (item: IHeading) => JSX.Element | null;

    /**
     * A function that generates a toolbar for the generator
     *
     * If not given, no toolbar will show up
     */
    toolbarGenerator?: () => any;

    /**
     * A function that takes the widget, and produces
     * a list of headings.
     */
    generate(widget: W): IHeading[];
  }
}


================================================
FILE: src/toc.tsx
================================================
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

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';

import { TableOfContentsRegistry } from './registry';

import * as React from 'react';
import * as ReactDOM from 'react-dom';

/**
 * Timeout for throttling TOC rendering.
 */
const RENDER_TIMEOUT = 1000;

/**
 * A widget for hosting a notebook table-of-contents.
 */
export class TableOfContents extends Widget {
  /**
   * Create a new table of contents.
   */
  constructor(options: TableOfContents.IOptions) {
    super();
    this._docmanager = options.docmanager;
    this._rendermime = options.rendermime;
  }

  /**
   * The current widget-generator tuple for the ToC.
   */
  get current(): TableOfContents.ICurrentWidget | null {
    return this._current;
  }
  set current(value: TableOfContents.ICurrentWidget | null) {
    // If they are the same as previously, do nothing.
    if (
      value &&
      this._current &&
      this._current.widget === value.widget &&
      this._current.generator === value.generator
    ) {
      return;
    }
    this._current = value;

    if (this.generator && this.generator.toolbarGenerator) {
      this._toolbar = this.generator.toolbarGenerator();
    }

    // Dispose an old activity monitor if it existsd
    if (this._monitor) {
      this._monitor.dispose();
      this._monitor = null;
    }
    // If we are wiping the ToC, update and return.
    if (!this._current) {
      this.updateTOC();
      return;
    }

    // Find the document model associated with the widget.
    const context = this._docmanager.contextForWidget(this._current.widget);
    if (!context || !context.model) {
      throw Error('Could not find a context for the Table of Contents');
    }

    // Throttle the rendering rate of the table of contents.
    this._monitor = new ActivityMonitor({
      signal: context.model.contentChanged,
      timeout: RENDER_TIMEOUT
    });
    this._monitor.activityStopped.connect(this.update, this);
    this.updateTOC();
  }

  /**
   * Handle an update request.
   */
  protected onUpdateRequest(msg: Message): void {
    // Don't bother if the TOC is not visible
    /* if (!this.isVisible) {
      return;
    } */
    this.updateTOC();
  }

  updateTOC() {
    let toc: IHeading[] = [];
    let title = 'Table of Contents';
    if (this._current) {
      toc = this._current.generator.generate(this._current.widget);
      const context = this._docmanager.contextForWidget(this._current.widget);
      if (context) {
        title = PathExt.basename(context.localPath);
      }
    }
    let itemRenderer: (item: IHeading) => JSX.Element | null = (
      item: IHeading
    ) => {
      return {item.text};
    };
    if (this._current && this._current.generator.itemRenderer) {
      itemRenderer = this._current.generator.itemRenderer!;
    }
    let renderedJSX = (
      
{title}
); if (this._current && this._current.generator) { renderedJSX = ( ); } ReactDOM.render(renderedJSX, this.node, () => { if ( this._current && this._current.generator.usesLatex === true && this._rendermime.latexTypesetter ) { this._rendermime.latexTypesetter.typeset(this.node); } }); } get generator() { if (this._current) { return this._current.generator; } return null; } /** * Rerender after showing. */ protected onAfterShow(msg: Message): void { this.update(); } private _toolbar: any; private _rendermime: IRenderMimeRegistry; private _docmanager: IDocumentManager; private _current: TableOfContents.ICurrentWidget | null; private _monitor: ActivityMonitor | null; } /** * 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 * heading information from that widget. */ export interface ICurrentWidget { widget: W; generator: TableOfContentsRegistry.IGenerator; } } /** * An object that represents a heading. */ export interface IHeading { /** * The text of the heading. */ text: string; /** * The HTML header level for the heading. */ level: number; /** * A function to execute when clicking the ToC * item. Typically this will be used to scroll * the parent widget to this item. */ onClick: () => void; /** * If there is special markup, we can instead * render the heading using a raw HTML string. This * HTML *should be properly sanitized!* * * For instance, this can be used to render * already-renderd-to-html markdown headings. */ html?: string; } /** * Props for the TOCItem component. */ export interface ITOCItemProps extends React.Props { /** * An IHeading to render. */ heading: IHeading; itemRenderer: (item: IHeading) => JSX.Element | null; } export interface ITOCItemStates {} /** * A React component for a table of contents entry. */ export class TOCItem extends React.Component { /** * Render the item. */ render() { const { heading } = this.props; // Create an onClick handler for the TOC item // that scrolls the anchor into view. const handleClick = (event: React.SyntheticEvent) => { event.preventDefault(); event.stopPropagation(); heading.onClick(); }; let content = this.props.itemRenderer(heading); return content &&
  • {content}
  • ; } } export interface ITOCTreeStates {} /** * Props for the TOCTree component. */ export interface ITOCTreeProps extends React.Props { /** * A title to display. */ title: string; /** * A list of IHeadings to render. */ toc: IHeading[]; toolbar: any; generator: TableOfContentsRegistry.IGenerator | null; itemRenderer: (item: IHeading) => JSX.Element | null; } /** * A React component for a table of contents. */ export class TOCTree extends React.Component { /** * Render the TOCTree. */ render() { // Map the heading objects onto a list of JSX elements. let i = 0; const Toolbar = this.props.toolbar; let listing: JSX.Element[] = this.props.toc.map(el => { return ( ); }); return (
    {this.props.title}
    {Toolbar && }
      {listing}
    ); } } ================================================ FILE: style/index.css ================================================ /*----------------------------------------------------------------------------- | Copyright (c) Jupyter Development Team. | Distributed under the terms of the Modified BSD License. |----------------------------------------------------------------------------*/ /*----------------------------------------------------------------------------- | Table of Contents |----------------------------------------------------------------------------*/ .jp-TableOfContents-content { flex: 1 1 auto; margin: 0; padding: 0; list-style-type: none; overflow: auto; background-color: var(--jp-layout-color1); } .jp-TableOfContents-content li { display: flex; flex-direction: row; padding: 4px 12px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; padding-top: 8px; padding-bottom: 8px; } .jp-TableOfContents-content li:hover { background: var(--jp-layout-color2); } .jp-TableOfContents { display: flex; flex-direction: column; background: var(--jp-layout-color1); color: var(--jp-ui-font-color1); font-size: var(--jp-ui-font-size0); height: 100%; } .jp-TableOfContents header { border-bottom: var(--jp-border-width) solid var(--jp-border-color2); flex: 0 0 auto; font-size: var(--jp-ui-font-size0); font-weight: 600; letter-spacing: 1px; margin: 0px; padding: 12px 0 4px 12px; text-transform: uppercase; } [data-theme-light='true'] .jp-TableOfContents-icon { background-image: url(list-light.svg); } [data-theme-light='false'] .jp-TableOfContents-icon { background-image: url(list-dark.svg); } .jp-TableOfContents-codeContainer { overflow: hidden; } .jp-TableOfContents-code { font-size: 9px; max-height: 70px; } .cm-toc .CodeMirror { font-size: 9px; z-index: 0; border: var(--jp-border-width) solid var(--jp-cell-editor-border-color); border-radius: 0px; background: var(--jp-cell-editor-background); max-width: 100%; max-height: 36px; } .toc-code-span { width: 100%; max-width: 100%; overflow: hidden; } .cm-toc .CodeMirror-scroll { overflow: hidden !important; } .CodeMirror-scroll::-webkit-scrollbar-track { background-color: transparent; } .toc-toolbar-icon, .toc-toolbar-icon-selected { float: left; padding: 0px; margin: 4px; background-repeat: no-repeat; background-color: none; background-size: 100%; background-position: center; height: 24px; width: 24px; margin: 4px; border-radius: 2px; } [data-theme-light='true'] .toc-toolbar-code-icon { background-image: url('img/code.svg'); } [data-theme-light='false'] .toc-toolbar-code-icon { background-image: url('img/code_darktheme.svg'); } [data-theme-light='true'] .toc-toolbar-markdown-icon { background-image: url('img/markdown.svg'); } [data-theme-light='false'] .toc-toolbar-markdown-icon { background-image: url('img/markdown_darktheme.svg'); } [data-theme-light='true'] .toc-toolbar-auto-numbering-icon { background-image: url('img/autonumbering.svg'); } [data-theme-light='false'] .toc-toolbar-auto-numbering-icon { background-image: url('img/autonumbering_darktheme.svg'); } .toc-toolbar-tag-icon { width: 30px; height: 22px; margin: 4px 9px 4px 4px; } [data-theme-light='true'] .toc-toolbar-tag-icon { background-image: url('img/tag.svg'); } [data-theme-light='false'] .toc-toolbar-tag-icon { background-image: url('img/tag_darktheme.svg'); } [data-theme-light='true'] .toc-toolbar-icon:hover { background-color: var(--jp-input-background); } [data-theme-light='false'] .toc-toolbar-icon:hover { background-color: #3a3a3a; } [data-theme-light='true'] .toc-toolbar-icon-selected { background-color: var(--jp-layout-color2); } [data-theme-light='false'] .toc-toolbar-icon-selected { background-color: #565656; } .toc-code-cell-prompt { flex: 0 0 27px; color: var(--jp-cell-prompt-not-active-font-color); opacity: var(--jp-cell-prompt-not-active-opacity); font-family: var(--jp-cell-prompt-font-family); padding: var(--jp-code-padding); padding-right: 0px; padding-left: 0px; letter-spacing: var(--jp-cell-prompt-letter-spacing); line-height: var(--jp-code-line-height); font-size: 8px; border: var(--jp-border-width) solid transparent; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .toc-toolbar { position: relative; width: 100%; margin: 0px; user-select: none; border-bottom: var(--jp-border-width) solid var(--jp-border-color2); height: 36px; display: flex; align-items: center; } .toc-code-cell-div { display: inline-flex; width: 100%; } .toc-entry-holder { display: inline-flex; position: relative; align-items: center; width: 100%; } .toc-collapse-button { padding-left: 3px; cursor: default; min-width: 11px; position: absolute; } .toc-arrow-img { top: 0; bottom: 0; margin: auto; position: absolute; background-repeat: no-repeat; } .toc-downarrow-img { width: 12px; height: 6px; } [data-theme-light='true'] .toc-downarrow-img { background-image: url('img/toggle_down.svg'); } [data-theme-light='false'] .toc-downarrow-img { background-image: url('img/toggle_down_darktheme.svg'); } .toc-rightarrow-img { width: 7px; height: 12px; } [data-theme-light='true'] .toc-rightarrow-img { background-image: url('img/toggle_right.svg'); } [data-theme-light='false'] .toc-rightarrow-img { background-image: url('img/toggle_right_darktheme.svg'); } .toc-twist-placeholder { max-width: 10px; opacity: 0; overflow: hidden; } .cm-toc-plain-span { width: 100%; white-space: pre-wrap; display: block; } .cm-toc-plain-textarea { font-size: 9px; z-index: 0; border: var(--jp-border-width) solid var(--jp-cell-editor-border-color2); border-radius: 0px; background: var(--jp-cell-editor-background); width: calc(100% - 9px); overflow: hidden; max-height: 74px; resize: none; font-family: var(--jp-code-font-family); outline: none; user-select: none; white-space: pre; padding: var(--jp-code-padding); } .cm-toc .CodeMirror-sizer { min-width: 0px !important; min-height: 0px !important; margin-bottom: 0px !important; } .cm-toc .CodeMirror-line { white-space: pre-wrap; cursor: pointer; } .cm-toc .CodeMirror-lines { cursor: pointer; } .toc-tag-dropdown { display: flex; width: 100%; } .toc-tag-dropdown-button { margin-left: auto; } .toc-tags-container { padding: 4px; border-bottom: var(--jp-border-width) solid var(--jp-border-color2); } .toc-clear-button { font-size: 12px; color: var(--jp-ui-font-color1); padding-left: 15px; /* padding-top: 7px; */ user-select: none; float: right; } .toc-clear-button:hover { font-size: 12px; color: var(--jp-ui-font-color2); padding-left: 15px; /* padding-top: 7px; */ user-select: none; } .toc-filter-button { background-color: var(--jp-layout-color1); border: solid 1px var(--jp-layout-color4); border-radius: 3px; width: fit-content; padding: 5px; padding-left: 6px; padding-right: 6px; margin-right: 17px; color: var(--jp-layout-color5); float: right; font-size: 12px; user-select: none; margin-bottom: 13px; } .toc-filter-button:hover { background-color: var(--jp-layout-color4); border: solid 1px var(--jp-layout-color4); color: var(--jp-layout-color1); } .toc-filter-button-na { background-color: var(--jp-layout-color1); border: solid 1px var(--jp-ui-font-color3); border-radius: 3px; width: fit-content; padding: 5px; padding-left: 6px; padding-right: 6px; margin-right: 17px; color: var(--jp-ui-font-color3); float: right; font-size: 12px; user-select: none; margin-bottom: 13px; } .toc-no-tags-div { font-size: 12px; padding: 3px; padding-bottom: 6px; margin: auto; color: var(--jp-layout-color4); } .toc-tags-container { width: 100%; } .jp-TableOfContents-content code { font-size: inherit; } .toc-cell-item { padding-left: 24px; } /* styles for tags */ .toc-tag-label { font-size: 11px; max-width: 100%; text-overflow: ellipsis; display: inline-block; overflow: hidden; box-sizing: border-box; padding-top: 0px; margin-top: -1px; margin-bottom: 0px; user-select: none; } .toc-tag { box-sizing: border-box; height: 24px; border-radius: 20px; padding: 10px; padding-bottom: 4px; padding-top: 5px; margin: 3px; width: fit-content; max-width: calc(100% - 25px); } .toc-selected-tag { color: white; background-color: #2196f3; outline: none; } .toc-unselected-tag { background-color: var(--jp-layout-color2); outline: none; } .toc-tag-holder { display: flex; flex-wrap: wrap; height: fit-content; padding-bottom: 6px; padding-right: 20px; padding-left: 9px; padding-top: 6px; } /* Font level sizes */ .toc-level-size-1 { font-size: 16.89px; } .toc-level-size-2 { font-size: 14.82px; } .toc-level-size-3 { font-size: 13px; } .toc-level-size-4 { font-size: 11.4px; } .toc-level-size-5 { font-size: 10px; } .toc-level-size-6 { font-size: 9px; } .toc-level-size-default { font-size: 9px; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "declaration": true, "noImplicitAny": true, "strictNullChecks": true, "skipLibCheck": true, "noEmitOnError": true, "noUnusedLocals": true, "lib": ["DOM", "ES6"], "module": "commonjs", "moduleResolution": "node", "target": "ES6", "outDir": "lib", "rootDir": "src", "jsx": "react" }, "include": ["src/**/*"] } ================================================ FILE: tslint.json ================================================ { "rulesDirectory": ["tslint-plugin-prettier"], "rules": { "prettier": [true, { "singleQuote": true }], "align": [true, "parameters", "statements"], "ban": [ true, ["_", "forEach"], ["_", "each"], ["$", "each"], ["angular", "forEach"] ], "class-name": true, "comment-format": [true, "check-space"], "curly": true, "eofline": true, "forin": false, "indent": [true, "spaces", 2], "interface-name": [true, "always-prefix"], "jsdoc-format": true, "label-position": true, "max-line-length": [false], "member-access": false, "member-ordering": [false], "new-parens": true, "no-angle-bracket-type-assertion": true, "no-any": false, "no-arg": true, "no-bitwise": true, "no-conditional-assignment": true, "no-consecutive-blank-lines": false, "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-construct": true, "no-debugger": true, "no-default-export": false, "no-duplicate-variable": true, "no-empty": true, "no-eval": true, "no-inferrable-types": false, "no-internal-module": true, "no-invalid-this": [true, "check-function-in-method"], "no-null-keyword": false, "no-reference": true, "no-require-imports": false, "no-shadowed-variable": false, "no-string-literal": false, "no-switch-case-fall-through": true, "no-trailing-whitespace": true, "no-unused-expression": true, "no-use-before-declare": false, "no-var-keyword": true, "no-var-requires": true, "object-literal-sort-keys": false, "one-line": [ true, "check-open-brace", "check-catch", "check-else", "check-finally", "check-whitespace" ], "one-variable-per-declaration": [true, "ignore-for-loop"], "quotemark": [true, "single", "avoid-escape", "jsx-double"], "radix": true, "semicolon": [true, "always", "ignore-bound-class-methods"], "switch-default": true, "trailing-comma": [ false, { "multiline": "never", "singleline": "never" } ], "triple-equals": [true, "allow-null-check", "allow-undefined-check"], "typedef": [false], "typedef-whitespace": [ false, { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" }, { "call-signature": "space", "index-signature": "space", "parameter": "space", "property-declaration": "space", "variable-declaration": "space" } ], "use-isnan": true, "use-strict": [false], "variable-name": [ true, "check-format", "allow-leading-underscore", "ban-keywords" ], "whitespace": [ true, "check-branch", "check-operator", "check-separator", "check-type" ] } }