Browse Source

Add capacity to skip H1 Headers (#9994)

* feat: disable h1 numbering

* feat: enable numberingH1 in settings

* chore: Package integrity updates

* feat: support skip numbering h1 in md

* refactor: clean up useless metadata

* style: update description of user settings

Co-authored-by: Frédéric Collonval <fcollonval@gmail.com>

* fix: restore name of the plugin

Co-authored-by: Frédéric Collonval <fcollonval@gmail.com>

* perf: make numberingH1 setting optional

Co-authored-by: Frédéric Collonval <fcollonval@gmail.com>

* refactor: change `level` before calling `generateNumbering`

* refactor: mvoe back generate to avoid length diff

* fix: unset numberingH1 in notebook metadata

* doc: update docstring

* fix: make settings optional

* style: update description of user settings

Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>

* Package integrity updates

* fix: remove duplicate imports

* revert: no dependencies add at root

* fix: remove unused dependency

* perf: don't need to refresh to update settings

* Package integrity updates

* Package integrity updates

* revert: set options to const

* style: update icon of toc setting

Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>

Co-authored-by: Frédéric Collonval <fcollonval@gmail.com>
Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>
Tian Wang 3 years ago
parent
commit
dd992bae99

+ 5 - 2
packages/toc-extension/package.json

@@ -30,7 +30,8 @@
   "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}",
-    "style/index.js"
+    "style/index.js",
+    "schema/*.json"
   ],
   "scripts": {
     "build": "tsc -b",
@@ -46,6 +47,7 @@
     "@jupyterlab/markdownviewer": "^3.1.0-alpha.13",
     "@jupyterlab/notebook": "^3.1.0-alpha.13",
     "@jupyterlab/rendermime": "^3.1.0-alpha.13",
+    "@jupyterlab/settingregistry": "^3.1.0-alpha.13",
     "@jupyterlab/toc": "^5.1.0-alpha.13",
     "@jupyterlab/translation": "^3.1.0-alpha.13",
     "@jupyterlab/ui-components": "^3.1.0-alpha.13"
@@ -59,7 +61,8 @@
     "access": "public"
   },
   "jupyterlab": {
-    "extension": true
+    "extension": true,
+    "schemaDir": "schema"
   },
   "styleModule": "style/index.js"
 }

+ 16 - 0
packages/toc-extension/schema/plugin.json

@@ -0,0 +1,16 @@
+{
+  "jupyter.lab.setting-icon": "ui-components:toc",
+  "jupyter.lab.setting-icon-label": "Table of Contents",
+  "title": "Table of Contents",
+  "description": "Table of contents settings.",
+  "properties": {
+    "numberingH1": {
+      "title": "Enable h1 numbering",
+      "description": "Whether to number first level headings",
+      "type": "boolean",
+      "default": true
+    }
+  },
+  "additionalProperties": false,
+  "type": "object"
+}

+ 23 - 4
packages/toc-extension/src/index.ts

@@ -16,6 +16,7 @@ import { IEditorTracker } from '@jupyterlab/fileeditor';
 import { IMarkdownViewerTracker } from '@jupyterlab/markdownviewer';
 import { INotebookTracker } from '@jupyterlab/notebook';
 import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import {
   createLatexGenerator,
   createMarkdownGenerator,
@@ -42,6 +43,7 @@ import { tocIcon } from '@jupyterlab/ui-components';
  * @param notebookTracker - notebook tracker
  * @param rendermime - rendered MIME registry
  * @param translator - translator
+ * @param settingRegistry - setting registry
  * @returns table of contents registry
  */
 async function activateTOC(
@@ -53,7 +55,8 @@ async function activateTOC(
   markdownViewerTracker: IMarkdownViewerTracker,
   notebookTracker: INotebookTracker,
   rendermime: IRenderMimeRegistry,
-  translator: ITranslator
+  translator: ITranslator,
+  settingRegistry?: ISettingRegistry
 ): Promise<ITableOfContentsRegistry> {
   const trans = translator.load('jupyterlab');
   // Create the ToC widget:
@@ -74,12 +77,25 @@ async function activateTOC(
   // Add the ToC widget to the application restorer:
   restorer.add(toc, '@jupyterlab/toc:plugin');
 
+  // Attempt to load plugin settings:
+  let settings: ISettingRegistry.ISettings | undefined;
+  if (settingRegistry) {
+    try {
+      settings = await settingRegistry.load('@jupyterlab/toc-extension:plugin');
+    } catch (error) {
+      console.error(
+        `Failed to load settings for the Table of Contents extension.\n\n${error}`
+      );
+    }
+  }
+
   // Create a notebook generator:
   const notebookGenerator = createNotebookGenerator(
     notebookTracker,
     toc,
     rendermime.sanitizer,
-    translator
+    translator,
+    settings
   );
   registry.add(notebookGenerator);
 
@@ -88,7 +104,8 @@ async function activateTOC(
     editorTracker,
     toc,
     rendermime.sanitizer,
-    translator
+    translator,
+    settings
   );
   registry.add(markdownGenerator);
 
@@ -97,7 +114,8 @@ async function activateTOC(
     markdownViewerTracker,
     toc,
     rendermime.sanitizer,
-    translator
+    translator,
+    settings
   );
   registry.add(renderedMarkdownGenerator);
 
@@ -156,6 +174,7 @@ const extension: JupyterFrontEndPlugin<ITableOfContentsRegistry> = {
     IRenderMimeRegistry,
     ITranslator
   ],
+  optional: [ISettingRegistry],
   activate: activateTOC
 };
 

+ 3 - 0
packages/toc-extension/tsconfig.json

@@ -24,6 +24,9 @@
     {
       "path": "../rendermime"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../toc"
     },

+ 1 - 0
packages/toc/package.json

@@ -50,6 +50,7 @@
     "@jupyterlab/markdownviewer": "^3.1.0-alpha.13",
     "@jupyterlab/notebook": "^3.1.0-alpha.13",
     "@jupyterlab/rendermime": "^3.1.0-alpha.13",
+    "@jupyterlab/settingregistry": "^3.1.0-alpha.13",
     "@jupyterlab/translation": "^3.1.0-alpha.13",
     "@jupyterlab/ui-components": "^3.1.0-alpha.13",
     "@lumino/coreutils": "^1.5.3",

+ 8 - 2
packages/toc/src/generators/markdown/get_headings.ts

@@ -22,12 +22,14 @@ type onClickFactory = (line: number) => () => void;
  * @param text - input text
  * @param onClick - callback which returns a "click" handler
  * @param dict - numbering dictionary
+ * @param numberingH1 - whether first level header should be numbered
  * @returns list of headings
  */
 function getHeadings(
   text: string,
   onClick: onClickFactory,
-  dict: INumberingDictionary
+  dict: INumberingDictionary,
+  numberingH1: boolean
 ): INumberedHeading[] {
   // Split the text into lines:
   const lines = text.split('\n');
@@ -48,9 +50,13 @@ function getHeadings(
     line += lines[i + 1] ? '\n' + lines[i + 1] : '';
     const heading = parseHeading(line); // append the next line to capture alternative style Markdown headings
     if (heading) {
+      let level = heading.level;
+      if (!numberingH1) {
+        level -= 1;
+      }
       headings.push({
         text: heading.text,
-        numbering: generateNumbering(dict, heading.level),
+        numbering: generateNumbering(dict, level),
         level: heading.level,
         onClick: onClick(i)
       });

+ 7 - 3
packages/toc/src/generators/markdown/get_rendered_headings.ts

@@ -28,19 +28,21 @@ function onClick(heading: Element) {
  * @param sanitizer - HTML sanitizer
  * @param dict - numbering dictionary
  * @param numbering - boolean indicating whether to enable numbering
+ * @param numberingH1 - whether first level header should be numbered
  * @returns list of headings
  */
 function getRenderedHeadings(
   node: HTMLElement,
   sanitizer: ISanitizer,
   dict: INumberingDictionary,
-  numbering = true
+  numbering = true,
+  numberingH1 = true
 ): INumberedHeading[] {
   let nodes = node.querySelectorAll('h1, h2, h3, h4, h5, h6');
   let headings: INumberedHeading[] = [];
   for (let i = 0; i < nodes.length; i++) {
     const heading = nodes[i];
-    const level = parseInt(heading.tagName[1], 10);
+    let level = parseInt(heading.tagName[1], 10);
     let text = heading.textContent ? heading.textContent : '';
     let hide = !numbering;
 
@@ -52,8 +54,10 @@ function getRenderedHeadings(
     html = html.replace('¶', ''); // remove the anchor symbol
 
     // Generate a numbering string:
+    if (!numberingH1) {
+      level -= 1;
+    }
     let nstr = generateNumbering(dict, level);
-
     // Generate the numbering DOM element:
     let nhtml = '';
     if (!hide) {

+ 46 - 7
packages/toc/src/generators/markdown/index.ts

@@ -13,11 +13,12 @@ import { TableOfContentsRegistry as Registry } from '../../registry';
 import { TableOfContents } from '../../toc';
 import { INumberedHeading } from '../../utils/headings';
 import { isMarkdown } from '../../utils/is_markdown';
-import { getHeadings } from './get_headings';
-import { getRenderedHeadings } from './get_rendered_headings';
 import { OptionsManager } from './options_manager';
 import { render } from './render';
 import { toolbar } from './toolbar_generator';
+import { getHeadings } from './get_headings';
+import { getRenderedHeadings } from './get_rendered_headings';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
 
 /**
  * Returns a boolean indicating whether this ToC generator is enabled.
@@ -36,11 +37,24 @@ function isEnabled(editor: IDocumentWidget<FileEditor>) {
  *
  * @private
  * @param editor - editor widget
+ * @param options - manage Markdown ToC generator options
  * @returns a list of headings
  */
-function generate(editor: IDocumentWidget<FileEditor>): INumberedHeading[] {
+function generate(
+  editor: IDocumentWidget<FileEditor>,
+  options?: OptionsManager
+): INumberedHeading[] {
   let dict = {};
-  return getHeadings(editor.content.model.value.text, onClick, dict);
+  let numberingH1 = true;
+  if (options !== undefined) {
+    numberingH1 = options.numberingH1;
+  }
+  return getHeadings(
+    editor.content.model.value.text,
+    onClick,
+    dict,
+    numberingH1
+  );
 
   /**
    * Returns a "click" handler.
@@ -66,19 +80,31 @@ function generate(editor: IDocumentWidget<FileEditor>): INumberedHeading[] {
  * @param tracker - file editor tracker
  * @param widget - table of contents widget
  * @param sanitizer - HTML sanitizer
+ * @param settings - advanced settings for toc extension
  * @returns ToC generator capable of parsing Markdown files
  */
 function createMarkdownGenerator(
   tracker: IEditorTracker,
   widget: TableOfContents,
   sanitizer: ISanitizer,
-  translator?: ITranslator
+  translator?: ITranslator,
+  settings?: ISettingRegistry.ISettings
 ): Registry.IGenerator<IDocumentWidget<FileEditor>> {
+  let numberingH1 = true;
+  if (settings) {
+    numberingH1 = settings.composite.numberingH1 as boolean;
+  }
   const options = new OptionsManager(widget, {
     numbering: true,
+    numberingH1: numberingH1,
     sanitizer,
     translator: translator || nullTranslator
   });
+  if (settings) {
+    settings.changed.connect(() => {
+      options.numberingH1 = settings.composite.numberingH1 as boolean;
+    });
+  }
   return {
     tracker,
     usesLatex: true,
@@ -117,19 +143,31 @@ function createMarkdownGenerator(
  * @param tracker - Markdown viewer tracker
  * @param sanitizer - HTML sanitizer
  * @param widget - table of contents widget
+ * @param settings - advanced settings for toc extension
  * @returns ToC generator capable of parsing rendered Markdown files
  */
 function createRenderedMarkdownGenerator(
   tracker: IMarkdownViewerTracker,
   widget: TableOfContents,
   sanitizer: ISanitizer,
-  translator?: ITranslator
+  translator?: ITranslator,
+  settings?: ISettingRegistry.ISettings
 ): Registry.IGenerator<MarkdownDocument> {
+  let numberingH1 = true;
+  if (settings) {
+    numberingH1 = settings.composite.numberingH1 as boolean;
+  }
   const options = new OptionsManager(widget, {
     numbering: true,
+    numberingH1: numberingH1,
     sanitizer,
     translator: translator || nullTranslator
   });
+  if (settings) {
+    settings.changed.connect(() => {
+      options.numberingH1 = settings.composite.numberingH1 as boolean;
+    });
+  }
   return {
     tracker,
     usesLatex: true,
@@ -173,7 +211,8 @@ function createRenderedMarkdownGenerator(
       widget.content.node,
       sanitizer,
       dict,
-      options.numbering
+      options.numbering,
+      options.numberingH1
     );
   }
 }

+ 23 - 1
packages/toc/src/generators/markdown/options_manager.ts

@@ -17,6 +17,11 @@ interface IOptions {
    */
   numbering: boolean;
 
+  /**
+   * Boolean indicating whether h1 headers should be numbered.
+   */
+  numberingH1: boolean;
+
   /**
    * HTML sanitizer.
    */
@@ -44,6 +49,7 @@ class OptionsManager extends Registry.IOptionsManager {
   constructor(widget: TableOfContents, options: IOptions) {
     super();
     this._numbering = options.numbering;
+    this._numberingH1 = options.numberingH1;
     this._widget = widget;
     this.translator = options.translator || nullTranslator;
     this.sanitizer = options.sanitizer;
@@ -66,6 +72,20 @@ class OptionsManager extends Registry.IOptionsManager {
     return this._numbering;
   }
 
+  /**
+   * Gets/sets ToC generator numbering h1 headers.
+   */
+  set numberingH1(value: boolean) {
+    if (this._numberingH1 != value) {
+      this._numberingH1 = value;
+      this._widget.update();
+    }
+  }
+
+  get numberingH1() {
+    return this._numberingH1;
+  }
+
   /**
    * Initializes options.
    *
@@ -75,13 +95,15 @@ class OptionsManager extends Registry.IOptionsManager {
    *
    * @param numbering - boolean indicating whether to number items
    */
-  initializeOptions(numbering: boolean) {
+  initializeOptions(numbering: boolean, numberingH1: boolean) {
     this._numbering = numbering;
+    this._numberingH1 = numberingH1;
     this._widget.update();
   }
 
   translator: ITranslator;
   private _numbering: boolean;
+  private _numberingH1: boolean;
   private _widget: TableOfContents;
 }
 

+ 1 - 1
packages/toc/src/generators/markdown/toolbar_generator.tsx

@@ -43,7 +43,7 @@ function toolbar(options: OptionsManager) {
     constructor(props: IProperties) {
       super(props);
       this.state = { numbering: false };
-      options.initializeOptions(false);
+      options.initializeOptions(false, options.numberingH1);
       this._trans = options.translator.load('jupyterlab');
     }
 

+ 6 - 1
packages/toc/src/generators/notebook/get_rendered_html_heading.ts

@@ -26,6 +26,7 @@ type onClickFactory = (el: Element) => () => void;
  * @param dict - numbering dictionary
  * @param lastLevel - last level
  * @param numbering - boolean indicating whether to enable numbering
+ * @param numberingH1 - boolean indicating whether to enable first level headers numbering
  * @param cellRef - cell reference
  * @param index - index of referenced cell relative to other cells in the notebook
  * @returns notebook heading
@@ -37,6 +38,7 @@ function getRenderedHTMLHeadings(
   dict: INumberingDictionary,
   lastLevel: number,
   numbering = false,
+  numberingH1 = true,
   cellRef: Cell,
   index: number = -1
 ): INotebookHeading[] {
@@ -71,7 +73,10 @@ function getRenderedHTMLHeadings(
     let html = sanitizer.sanitize(el.innerHTML, sanitizerOptions);
     html = html.replace('¶', '');
 
-    const level = parseInt(el.tagName[1], 10);
+    let level = parseInt(el.tagName[1], 10);
+    if (!numberingH1) {
+      level -= 1;
+    }
     let nstr = generateNumbering(dict, level);
     if (numbering) {
       const nhtml = document.createElement('span');

+ 16 - 1
packages/toc/src/generators/notebook/index.ts

@@ -25,6 +25,7 @@ import { getRenderedHTMLHeadings } from './get_rendered_html_heading';
 import { OptionsManager } from './options_manager';
 import { render } from './render';
 import { toolbar } from './toolbar_generator';
+import { ISettingRegistry } from '@jupyterlab/settingregistry';
 
 /**
  * Returns a ToC generator for notebooks.
@@ -34,19 +35,31 @@ import { toolbar } from './toolbar_generator';
  * @param widget - table of contents widget
  * @param sanitizer - HTML sanitizer
  * @param translator - Language translator
+ * @param settings - advanced settings for toc extension
  * @returns ToC generator capable of parsing notebooks
  */
 function createNotebookGenerator(
   tracker: INotebookTracker,
   widget: TableOfContents,
   sanitizer: ISanitizer,
-  translator?: ITranslator
+  translator?: ITranslator,
+  settings?: ISettingRegistry.ISettings
 ): Registry.IGenerator<NotebookPanel> {
+  let numberingH1 = true;
+  if (settings) {
+    numberingH1 = settings.composite.numberingH1 as boolean;
+  }
   const options = new OptionsManager(widget, tracker, {
     numbering: false,
+    numberingH1: numberingH1,
     sanitizer: sanitizer,
     translator: translator || nullTranslator
   });
+  if (settings) {
+    settings.changed.connect(() => {
+      options.numberingH1 = settings.composite.numberingH1 as boolean;
+    });
+  }
   tracker.activeCellChanged.connect(
     (sender: INotebookTracker, args: Cell<ICellModel>) => {
       widget.update();
@@ -155,6 +168,7 @@ function createNotebookGenerator(
             dict,
             getLastHeadingLevel(headings),
             options.numbering,
+            options.numberingH1,
             cell,
             i
           );
@@ -198,6 +212,7 @@ function createNotebookGenerator(
             dict,
             lastLevel,
             options.numbering,
+            options.numberingH1,
             cell,
             i
           );

+ 24 - 0
packages/toc/src/generators/notebook/options_manager.ts

@@ -18,6 +18,11 @@ interface IOptions {
    */
   numbering: boolean;
 
+  /**
+   * Boolean indicating whether h1 headers should be numbered.
+   */
+  numberingH1: boolean;
+
   /**
    * HTML sanitizer.
    */
@@ -55,6 +60,7 @@ class OptionsManager extends Registry.IOptionsManager {
   ) {
     super();
     this._numbering = options.numbering;
+    this._numberingH1 = options.numberingH1;
     this._widget = widget;
     this._notebook = notebook;
     this.sanitizer = options.sanitizer;
@@ -103,6 +109,20 @@ class OptionsManager extends Registry.IOptionsManager {
     return this._numbering;
   }
 
+  /**
+   * Gets/sets ToC generator numbering h1 headers.
+   */
+  set numberingH1(value: boolean) {
+    if (this._numberingH1 != value) {
+      this._numberingH1 = value;
+      this._widget.update();
+    }
+  }
+
+  get numberingH1() {
+    return this._numberingH1;
+  }
+
   /**
    * Toggles whether to show code previews in the table of contents.
    */
@@ -200,17 +220,20 @@ class OptionsManager extends Registry.IOptionsManager {
    * -  This will **not** change notebook meta-data.
    *
    * @param numbering - boolean indicating whether to number items
+   * @param numberingH1 - boolean indicating whether to number first level items
    * @param showCode - boolean indicating whether to show code previews
    * @param showMarkdown - boolean indicating whether to show Markdown previews
    * @param showTags - boolean indicating whether to show tags
    */
   initializeOptions(
     numbering: boolean,
+    numberingH1: boolean,
     showCode: boolean,
     showMarkdown: boolean,
     showTags: boolean
   ) {
     this._numbering = numbering;
+    this._numberingH1 = numberingH1;
     this._showCode = showCode;
     this._showMarkdown = showMarkdown;
     this._showTags = showTags;
@@ -220,6 +243,7 @@ class OptionsManager extends Registry.IOptionsManager {
   private _preRenderedToolbar: any = null;
   private _filtered: string[] = [];
   private _numbering: boolean;
+  private _numberingH1: boolean;
   private _showCode = false;
   private _showMarkdown = false;
   private _showTags = false;

+ 1 - 0
packages/toc/src/generators/notebook/toolbar_generator.tsx

@@ -95,6 +95,7 @@ function toolbar(options: OptionsManager, tracker: INotebookTracker) {
             ) as boolean;
             options.initializeOptions(
               numbering || options.numbering,
+              options.numberingH1,
               showCode || options.showCode,
               showMarkdown || options.showMarkdown,
               showTags || options.showTags

+ 1 - 1
packages/toc/src/registry.ts

@@ -187,6 +187,6 @@ export namespace TableOfContentsRegistry {
      * @param widget - widget
      * @returns list of headings
      */
-    generate(widget: W): IHeading[];
+    generate(widget: W, options?: IOptionsManager): IHeading[];
   }
 }

+ 4 - 1
packages/toc/src/toc.tsx

@@ -112,7 +112,10 @@ export class TableOfContents extends Widget {
     let toc: IHeading[] = [];
     let title = this._trans.__('Table of Contents');
     if (this._current) {
-      toc = this._current.generator.generate(this._current.widget);
+      toc = this._current.generator.generate(
+        this._current.widget,
+        this._current.generator.options
+      );
       const context = this._docmanager.contextForWidget(this._current.widget);
       if (context) {
         title = PathExt.basename(context.localPath);

+ 3 - 0
packages/toc/tsconfig.json

@@ -33,6 +33,9 @@
     {
       "path": "../rendermime"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../translation"
     },

+ 6 - 0
packages/toc/tsconfig.test.json

@@ -32,6 +32,9 @@
     {
       "path": "../rendermime"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../translation"
     },
@@ -71,6 +74,9 @@
     {
       "path": "../rendermime"
     },
+    {
+      "path": "../settingregistry"
+    },
     {
       "path": "../translation"
     },