diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8deec2f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +**/lib diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..92cde39 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} \ No newline at end of file diff --git a/README.md b/README.md index adc5231..9201d0d 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,11 @@ 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") - +![Table of Contents](toc.gif 'Table of Contents') ## Prerequisites -* JupyterLab v0.33 +- JupyterLab v0.33 ## Installation @@ -32,13 +31,17 @@ 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. diff --git a/package.json b/package.json index 53c4521..3ca9e60 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "scripts": { "build": "tsc", "clean": "rimraf lib", + "precommit": "lint-staged", + "prettier": "prettier --write '**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}'", "watch": "tsc -w" }, "dependencies": { @@ -49,12 +51,24 @@ "devDependencies": { "@types/react": "~16.0.19", "@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": "~2.9.2" }, "resolutions": { "@types/react": "~16.0.19" }, + "lint-staged": { + "**/*{.ts,.tsx,.css,.json,.md}": [ + "prettier --write", + "git add" + ] + }, "jupyterlab": { "extension": "lib/extension.js" } diff --git a/src/extension.ts b/src/extension.ts index 44d9244..a65be3f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,27 +5,27 @@ import { ILayoutRestorer, IMimeDocumentTracker, JupyterLab, - JupyterLabPlugin, + JupyterLabPlugin } from '@jupyterlab/application'; -import {IDocumentManager} from '@jupyterlab/docmanager'; +import { IDocumentManager } from '@jupyterlab/docmanager'; -import {IEditorTracker} from '@jupyterlab/fileeditor'; +import { IEditorTracker } from '@jupyterlab/fileeditor'; -import {INotebookTracker} from '@jupyterlab/notebook'; +import { INotebookTracker } from '@jupyterlab/notebook'; -import {IRenderMimeRegistry} from '@jupyterlab/rendermime'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import {TableOfContents} from './toc'; +import { TableOfContents } from './toc'; import { createLatexGenerator, createNotebookGenerator, createMarkdownGenerator, - createRenderedMarkdownGenerator, + createRenderedMarkdownGenerator } from './generators'; -import {ITableOfContentsRegistry, TableOfContentsRegistry} from './registry'; +import { ITableOfContentsRegistry, TableOfContentsRegistry } from './registry'; import '../style/index.css'; @@ -42,9 +42,9 @@ const extension: JupyterLabPlugin = { ILayoutRestorer, IMimeDocumentTracker, INotebookTracker, - IRenderMimeRegistry, + IRenderMimeRegistry ], - activate: activateTOC, + activate: activateTOC }; /** @@ -57,10 +57,10 @@ function activateTOC( restorer: ILayoutRestorer, mimeDocumentTracker: IMimeDocumentTracker, notebookTracker: INotebookTracker, - rendermime: IRenderMimeRegistry, + rendermime: IRenderMimeRegistry ): ITableOfContentsRegistry { // Create the ToC widget. - const toc = new TableOfContents({docmanager, rendermime}); + const toc = new TableOfContents({ docmanager, rendermime }); // Create the ToC registry. const registry = new TableOfContentsRegistry(); @@ -68,7 +68,7 @@ function activateTOC( // Add the ToC to the left area. toc.title.label = 'Contents'; toc.id = 'table-of-contents'; - app.shell.addToLeftArea(toc, {rank: 700}); + app.shell.addToLeftArea(toc, { rank: 700 }); // Add the ToC widget to the application restorer. restorer.add(toc, 'juputerlab-toc'); @@ -76,7 +76,7 @@ function activateTOC( // Create a notebook TableOfContentsRegistry.IGenerator const notebookGenerator = createNotebookGenerator( notebookTracker, - rendermime.sanitizer, + rendermime.sanitizer ); registry.addGenerator(notebookGenerator); @@ -110,7 +110,7 @@ function activateTOC( } return; } - toc.current = {widget, generator}; + toc.current = { widget, generator }; }); return registry; diff --git a/src/generators.ts b/src/generators.ts index c07f1ef..0452586 100644 --- a/src/generators.ts +++ b/src/generators.ts @@ -1,21 +1,21 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import {IInstanceTracker, ISanitizer} from '@jupyterlab/apputils'; +import { IInstanceTracker, ISanitizer } from '@jupyterlab/apputils'; -import {CodeCell, CodeCellModel, MarkdownCell} from '@jupyterlab/cells'; +import { CodeCell, CodeCellModel, MarkdownCell } from '@jupyterlab/cells'; -import {IDocumentWidget, MimeDocument} from '@jupyterlab/docregistry'; +import { IDocumentWidget, MimeDocument } from '@jupyterlab/docregistry'; -import {FileEditor, IEditorTracker} from '@jupyterlab/fileeditor'; +import { FileEditor, IEditorTracker } from '@jupyterlab/fileeditor'; -import {INotebookTracker, NotebookPanel} from '@jupyterlab/notebook'; +import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; -import {each} from '@phosphor/algorithm'; +import { each } from '@phosphor/algorithm'; -import {TableOfContentsRegistry} from './registry'; +import { TableOfContentsRegistry } from './registry'; -import {IHeading} from './toc'; +import { IHeading } from './toc'; const VDOM_MIME_TYPE = 'application/vdom.v1+json'; @@ -30,7 +30,7 @@ const HTML_MIME_TYPE = 'text/html'; */ export function createNotebookGenerator( tracker: INotebookTracker, - sanitizer: ISanitizer, + sanitizer: ISanitizer ): TableOfContentsRegistry.IGenerator { return { tracker, @@ -49,7 +49,7 @@ export function createNotebookGenerator( const outputModel = (model as CodeCellModel).outputs.get(i); const dataTypes = Object.keys(outputModel.data); const htmlData = dataTypes.filter( - t => Private.isMarkdown(t) || Private.isDOM(t), + t => Private.isMarkdown(t) || Private.isDOM(t) ); if (!htmlData.length) { continue; @@ -66,8 +66,8 @@ export function createNotebookGenerator( Private.getRenderedHTMLHeadings( outputWidget.node, onClickFactory, - sanitizer, - ), + sanitizer + ) ); } } else if (model.type === 'markdown') { @@ -88,26 +88,26 @@ export function createNotebookGenerator( Private.getRenderedHTMLHeadings( cell.node, onClickFactory, - sanitizer, - ), + sanitizer + ) ); } else { const onClickFactory = (line: number) => { return () => { cell.node.scrollIntoView(); if (!(cell as MarkdownCell).rendered) { - cell.editor.setCursorPosition({line, column: 0}); + cell.editor.setCursorPosition({ line, column: 0 }); } }; }; headings = headings.concat( - Private.getMarkdownHeadings(model.value.text, onClickFactory), + Private.getMarkdownHeadings(model.value.text, onClickFactory) ); } } }); return headings; - }, + } }; } @@ -119,7 +119,7 @@ export function createNotebookGenerator( * @returns A TOC generator that can parse markdown files. */ export function createMarkdownGenerator( - tracker: IEditorTracker, + tracker: IEditorTracker ): TableOfContentsRegistry.IGenerator> { return { tracker, @@ -133,11 +133,11 @@ export function createMarkdownGenerator( let model = editor.content.model; let onClickFactory = (line: number) => { return () => { - editor.content.editor.setCursorPosition({line, column: 0}); + editor.content.editor.setCursorPosition({ line, column: 0 }); }; }; return Private.getMarkdownHeadings(model.value.text, onClickFactory); - }, + } }; } @@ -150,7 +150,7 @@ export function createMarkdownGenerator( */ export function createRenderedMarkdownGenerator( tracker: IInstanceTracker, - sanitizer: ISanitizer, + sanitizer: ISanitizer ): TableOfContentsRegistry.IGenerator { return { tracker, @@ -169,9 +169,9 @@ export function createRenderedMarkdownGenerator( return Private.getRenderedHTMLHeadings( widget.content.node, onClickFactory, - sanitizer, + sanitizer ); - }, + } }; } @@ -183,7 +183,7 @@ export function createRenderedMarkdownGenerator( * @returns A TOC generator that can parse LaTeX files. */ export function createLatexGenerator( - tracker: IEditorTracker, + tracker: IEditorTracker ): TableOfContentsRegistry.IGenerator> { return { tracker, @@ -202,26 +202,29 @@ export function createLatexGenerator( // 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}; + 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){(.+)}/, + /^\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}); + editor.content.editor.setCursorPosition({ + line: line.idx, + column: 0 + }); }; - headings.push({text, level, onClick}); + headings.push({ text, level, onClick }); } }); return headings; - }, + } }; } @@ -235,7 +238,7 @@ namespace Private { */ export function getMarkdownHeadings( text: string, - onClickFactory: (line: number) => (() => void), + onClickFactory: (line: number) => (() => void) ): IHeading[] { // Split the text into lines. const lines = text.split('\n'); @@ -253,7 +256,7 @@ namespace Private { const level = match[1].length; // Take special care to parse markdown links into raw text. const text = match[2].replace(/\[(.+)\]\(.+\)/g, '$1'); - headings.push({text, level, onClick}); + headings.push({ text, level, onClick }); return; } @@ -263,7 +266,7 @@ namespace Private { const level = match[1][0] === '=' ? 1 : 2; // Take special care to parse markdown links into raw text. const text = lines[idx - 1].replace(/\[(.+)\]\(.+\)/g, '$1'); - headings.push({text, level, onClick}); + headings.push({ text, level, onClick }); return; } @@ -274,7 +277,7 @@ namespace Private { if (match) { const level = parseInt(match[1]); const text = match[2]; - headings.push({text, level, onClick}); + headings.push({ text, level, onClick }); } }); return headings; @@ -287,7 +290,7 @@ namespace Private { export function getRenderedHTMLHeadings( node: HTMLElement, onClickFactory: (el: Element) => (() => void), - sanitizer: ISanitizer, + sanitizer: ISanitizer ): IHeading[] { let headings: IHeading[] = []; let headingNodes = node.querySelectorAll('h1, h2, h3, h4, h5, h6'); @@ -299,7 +302,7 @@ namespace Private { html = html.replace('ΒΆ', ''); // Remove the anchor symbol. const onClick = onClickFactory(heading); - headings.push({level, text, html, onClick}); + headings.push({ level, text, html, onClick }); } return headings; } @@ -320,10 +323,7 @@ namespace Private { * 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 - ); + return mime === VDOM_MIME_TYPE || mime === HTML_MIME_TYPE; } /** @@ -331,14 +331,14 @@ namespace Private { * levels. `part` and `chapter` are less common in my experience, * so assign them to header level 1. */ - export const latexLevels: {[label: string]: number} = { + 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, + subparagraph: 5 }; /** @@ -360,7 +360,7 @@ namespace Private { 'div', 'span', 'pre', - 'del', + 'del' ], allowedAttributes: { // Allow "class" attribute for tags. @@ -372,7 +372,7 @@ namespace Private { // Allow "class" attribute for

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

 tags.
-      pre: ['class'],
-    },
+      pre: ['class']
+    }
   };
 }
diff --git a/src/registry.ts b/src/registry.ts
index 7daad00..1a3ec67 100644
--- a/src/registry.ts
+++ b/src/registry.ts
@@ -1,25 +1,26 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {IInstanceTracker} from '@jupyterlab/apputils';
+import { IInstanceTracker } from '@jupyterlab/apputils';
 
-import {Token} from '@phosphor/coreutils';
+import { Token } from '@phosphor/coreutils';
 
-import {Widget} from '@phosphor/widgets';
+import { Widget } from '@phosphor/widgets';
 
-import {IHeading} from './toc';
+import { IHeading } from './toc';
 
 /**
  * An interface for a TableOfContentsRegistry.
  */
-export interface ITableOfContentsRegistry extends TableOfContentsRegistry {};
+export interface ITableOfContentsRegistry extends TableOfContentsRegistry {}
 
 /* tslint:disable */
 /**
  * The TableOfContentsRegistry token.
  */
-export
-const ITableOfContentsRegistry = new Token('jupyterlab-toc:ITableOfContentsRegistry');
+export const ITableOfContentsRegistry = new Token(
+  'jupyterlab-toc:ITableOfContentsRegistry'
+);
 /* tslint:enable */
 
 /**
@@ -32,7 +33,7 @@ export class TableOfContentsRegistry {
    * or undefined if none can be found.
    */
   findGeneratorForWidget(
-    widget: Widget,
+    widget: Widget
   ): TableOfContentsRegistry.IGenerator | undefined {
     let generator: TableOfContentsRegistry.IGenerator | undefined;
     this._generators.forEach(gen => {
@@ -80,7 +81,7 @@ export namespace TableOfContentsRegistry {
      * 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;
+    isEnabled?: (widget: W) => boolean;
 
     /**
      * Whether the document uses LaTeX typesetting.
diff --git a/src/toc.tsx b/src/toc.tsx
index e45d4c1..5edcd0c 100644
--- a/src/toc.tsx
+++ b/src/toc.tsx
@@ -1,17 +1,17 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {ActivityMonitor, PathExt} from '@jupyterlab/coreutils';
+import { ActivityMonitor, PathExt } from '@jupyterlab/coreutils';
 
-import {IDocumentManager} from '@jupyterlab/docmanager';
+import { IDocumentManager } from '@jupyterlab/docmanager';
 
-import {IRenderMimeRegistry} from '@jupyterlab/rendermime';
+import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
 
-import {Message} from '@phosphor/messaging';
+import { Message } from '@phosphor/messaging';
 
-import {Widget} from '@phosphor/widgets';
+import { Widget } from '@phosphor/widgets';
 
-import {TableOfContentsRegistry} from './registry';
+import { TableOfContentsRegistry } from './registry';
 
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
@@ -72,9 +72,12 @@ export class TableOfContents extends Widget {
     // Throttle the rendering rate of the table of contents.
     this._monitor = new ActivityMonitor({
       signal: context.model.contentChanged,
-      timeout: RENDER_TIMEOUT,
+      timeout: RENDER_TIMEOUT
     });
-    this._monitor.activityStopped.connect(this.update, this);
+    this._monitor.activityStopped.connect(
+      this.update,
+      this
+    );
     this.update();
   }
 
@@ -221,7 +224,7 @@ export class TOCItem extends React.Component {
     // Clamp the header level between 1 and six.
     level = Math.max(Math.min(level, 6), 1);
 
-    const paddingLeft = (level -1) * 12;
+    const paddingLeft = (level - 1) * 12;
 
     // Create an onClick handler for the TOC item
     // that scrolls the anchor into view.
@@ -241,11 +244,7 @@ export class TOCItem extends React.Component {
         />
       );
     } else {
-      content = (
-        
-          {heading.text}
-        
-      );
+      content = {heading.text};
     }
 
     return 
  • {content}
  • ;