Browse Source

Rewrite `markdownviewer` as a standard extension.
Add a section of the Settings Editor dedicated to it.

Raffaele De Feo 6 years ago
parent
commit
3aeacc2e5d

+ 8 - 3
packages/markdownviewer-extension/package.json

@@ -11,8 +11,7 @@
   "files": [
     "lib/*.d.ts",
     "lib/*.js.map",
-    "lib/*.js",
-    "style/*.css"
+    "lib/*.js"
   ],
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -31,6 +30,11 @@
     "watch": "tsc -b --watch"
   },
   "dependencies": {
+    "@jupyterlab/application": "^0.19.1",
+    "@jupyterlab/apputils": "^0.19.1",
+    "@jupyterlab/coreutils": "^2.2.1",
+    "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/markdownviewer": "^0.19.1",
     "@jupyterlab/rendermime": "^0.19.1"
   },
   "devDependencies": {
@@ -42,6 +46,7 @@
     "access": "public"
   },
   "jupyterlab": {
-    "mimeExtension": true
+    "extension": true,
+    "schemaDir": "schema"
   }
 }

+ 68 - 0
packages/markdownviewer-extension/schema/plugin.json

@@ -0,0 +1,68 @@
+{
+  "jupyter.lab.setting-icon-class": "jp-MarkdownIcon",
+  "jupyter.lab.setting-icon-label": "Markdown Viewer",
+  "title": "Markdown Viewer",
+  "description": "Markdown viewer settings.",
+  "definitions": {
+    "fontFamily": {
+      "type": ["string", "null"]
+    },
+    "fontSize": {
+      "type": ["integer", "null"],
+      "minimum": 1,
+      "maximum": 100
+    },
+    "lineHeight": {
+      "type": ["number", "null"]
+    },
+    "lineWidth": {
+      "type": ["number", "null"]
+    },
+    "hideFrontMatter": {
+      "type": "boolean"
+    },
+    "renderTimeout": {
+      "type": "number"
+    }
+  },
+  "properties": {
+    "fontFamily": {
+      "title": "Font Family",
+      "description": "The font family used to render markdown.\nIf `null`, value from current theme is used.",
+      "$ref": "#/definitions/fontFamily",
+      "default": null
+    },
+    "fontSize": {
+      "title": "Font Size",
+      "description": "The size in pixel of the font used to render markdown.\nIf `null`, value from current theme is used.",
+      "$ref": "#/definitions/fontSize",
+      "default": null
+    },
+    "lineHeight": {
+      "title": "Line Height",
+      "description": "The line height used to render markdown.\nIf `null`, value from current theme is used.",
+      "$ref": "#/definitions/lineHeight",
+      "default": null
+    },
+    "lineWidth": {
+      "title": "Line Width",
+      "description": "The text line width expressed in CSS ch units.\nIf `null`, lines fit the viewport width.",
+      "$ref": "#/definitions/lineWidth",
+      "default": null
+    },
+    "hideFrontMatter": {
+      "title": "Hide Front Matter",
+      "description": "Whether to hide YALM front matter.\nThe YALM front matter must be placed at the top of the document,\nstarted by a line of three dashes (---) and ended by a line of\nthree dashes (---) or three points (...).",
+      "$ref": "#/definitions/hideFrontMatter",
+      "default": true
+    },
+    "renderTimeout": {
+      "title": "Render Timeout",
+      "description": "The render timeout in milliseconds.",
+      "$ref": "#/definitions/renderTimeout",
+      "default": 1000
+    }
+  },
+  "additionalProperties": false,
+  "type": "object"
+}

+ 112 - 14
packages/markdownviewer-extension/src/index.ts

@@ -1,31 +1,129 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { IRenderMime, markdownRendererFactory } from '@jupyterlab/rendermime';
+import {
+  ILayoutRestorer,
+  JupyterLab,
+  JupyterLabPlugin
+} from '@jupyterlab/application';
 
-import '../style/index.css';
+import { InstanceTracker } from '@jupyterlab/apputils';
+
+import { ISettingRegistry } from '@jupyterlab/coreutils';
+
+import {
+  IRenderMimeRegistry,
+  markdownRendererFactory
+} from '@jupyterlab/rendermime';
+
+import {
+  MarkdownViewer,
+  MarkdownViewerFactory,
+  MarkdownDocument,
+  IMarkdownViewerTracker
+} from '@jupyterlab/markdownviewer';
 
 /**
- * The name of the factory that creates markdown widgets.
+ * The name of the factory that creates markdown viewer widgets.
  */
 const FACTORY = 'Markdown Preview';
 
 /**
- * The markdown mime renderer extension.
+ * The markdown viewer plugin.
+ */
+const plugin: JupyterLabPlugin<IMarkdownViewerTracker> = {
+  activate,
+  id: '@jupyterlab/markdownviewer-extension:plugin',
+  provides: IMarkdownViewerTracker,
+  requires: [ILayoutRestorer, IRenderMimeRegistry, ISettingRegistry],
+  autoStart: true
+};
+
+/**
+ * Activate the markdown viewer plugin.
  */
-const extension: IRenderMime.IExtension = {
-  id: '@jupyterlab/markdownviewer-extension:factory',
-  rendererFactory: markdownRendererFactory,
-  dataType: 'string',
-  documentWidgetFactoryOptions: {
+function activate(
+  app: JupyterLab,
+  restorer: ILayoutRestorer,
+  rendermime: IRenderMimeRegistry,
+  settingRegistry: ISettingRegistry
+): IMarkdownViewerTracker {
+  const registry = app.docRegistry;
+
+  // Add the markdown renderer factory.
+  rendermime.addFactory(markdownRendererFactory);
+
+  const namespace = 'markdownviewer-widget';
+  const tracker = new InstanceTracker<MarkdownDocument>({
+    namespace
+  });
+
+  let config: Partial<MarkdownViewer.IConfig> = {
+    ...MarkdownViewer.defaultConfig
+  };
+
+  /**
+   * Update the settings of a widget.
+   */
+  function updateWidget(widget: MarkdownViewer): void {
+    Object.keys(config).forEach((k: keyof MarkdownViewer.IConfig) => {
+      widget.setOption(k, config[k]);
+    });
+  }
+
+  /**
+   * Update the setting values.
+   */
+  function updateSettings(settings: ISettingRegistry.ISettings) {
+    config = settings.composite as Partial<MarkdownViewer.IConfig>;
+    tracker.forEach(widget => {
+      updateWidget(widget.content);
+    });
+  }
+
+  // Fetch the initial state of the settings.
+  settingRegistry
+    .load(plugin.id)
+    .then((settings: ISettingRegistry.ISettings) => {
+      settings.changed.connect(() => {
+        updateSettings(settings);
+      });
+      updateSettings(settings);
+    })
+    .catch((reason: Error) => {
+      console.error(reason.message);
+    });
+
+  // Register the MarkdownViewer factory.
+  const factory = new MarkdownViewerFactory({
+    rendermime,
     name: FACTORY,
-    primaryFileType: 'markdown',
+    primaryFileType: registry.getFileType('markdown'),
     fileTypes: ['markdown'],
     defaultRendered: ['markdown']
-  }
-};
+  });
+  factory.widgetCreated.connect((sender, widget) => {
+    // Notify the instance tracker if restore data needs to update.
+    widget.context.pathChanged.connect(() => {
+      tracker.save(widget);
+    });
+    // Handle the settings of new widgets.
+    updateWidget(widget.content);
+    tracker.add(widget);
+  });
+  registry.addWidgetFactory(factory);
+
+  // Handle state restoration.
+  restorer.restore(tracker, {
+    command: 'docmanager:open',
+    args: widget => ({ path: widget.context.path, factory: FACTORY }),
+    name: widget => widget.context.path
+  });
+
+  return tracker;
+}
 
 /**
- * Export the extension as default.
+ * Export the plugin as default.
  */
-export default extension;
+export default plugin;

+ 15 - 0
packages/markdownviewer-extension/tsconfig.json

@@ -6,6 +6,21 @@
   },
   "include": ["src/*"],
   "references": [
+    {
+      "path": "../application"
+    },
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../docregistry"
+    },
+    {
+      "path": "../markdownviewer"
+    },
     {
       "path": "../rendermime"
     }

+ 3 - 0
packages/markdownviewer/README.md

@@ -0,0 +1,3 @@
+# @jupyterlab/markdownviewer
+
+A JupyterLab package which provides a markdown viewer.

+ 50 - 0
packages/markdownviewer/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "@jupyterlab/markdownviewer",
+  "version": "0.19.1",
+  "description": "JupyterLab - Markdown viewer Widget",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "files": [
+    "lib/*.d.ts",
+    "lib/*.js.map",
+    "lib/*.js",
+    "style/*.css"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "directories": {
+    "lib": "lib/"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "scripts": {
+    "build": "tsc -b",
+    "clean": "rimraf lib",
+    "docs": "typedoc --options tdoptions.json --theme ../../typedoc-theme src",
+    "prepublishOnly": "npm run build",
+    "watch": "tsc -b --watch"
+  },
+  "dependencies": {
+    "@jupyterlab/apputils": "^0.19.1",
+    "@jupyterlab/coreutils": "^2.2.1",
+    "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/rendermime": "^0.19.1",
+    "@phosphor/coreutils": "^1.3.0",
+    "@phosphor/messaging": "^1.2.2",
+    "@phosphor/widgets": "^1.6.0"
+  },
+  "devDependencies": {
+    "rimraf": "~2.6.2",
+    "typedoc": "~0.12.0",
+    "typescript": "~3.1.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 25 - 0
packages/markdownviewer/src/index.ts

@@ -0,0 +1,25 @@
+// 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 { MarkdownDocument } from './widget';
+
+import '../style/index.css';
+
+export * from './widget';
+
+/**
+ * A class that tracks markdown viewer widgets.
+ */
+export interface IMarkdownViewerTracker
+  extends IInstanceTracker<MarkdownDocument> {}
+
+/**
+ * The markdownviewer tracker token.
+ */
+export const IMarkdownViewerTracker = new Token<IMarkdownViewerTracker>(
+  '@jupyterlab/markdownviewer:IMarkdownViewerTracker'
+);

+ 370 - 0
packages/markdownviewer/src/widget.ts

@@ -0,0 +1,370 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { showErrorMessage } from '@jupyterlab/apputils';
+
+import { ActivityMonitor } from '@jupyterlab/coreutils';
+
+import {
+  ABCWidgetFactory,
+  DocumentRegistry,
+  DocumentWidget
+} from '@jupyterlab/docregistry';
+
+import {
+  IRenderMime,
+  RenderMimeRegistry,
+  MimeModel
+} from '@jupyterlab/rendermime';
+
+import { PromiseDelegate } from '@phosphor/coreutils';
+
+import { Message } from '@phosphor/messaging';
+
+import { JSONObject } from '@phosphor/coreutils';
+
+import { StackedLayout, Widget } from '@phosphor/widgets';
+
+/**
+ * The class name added to a markdown viewer.
+ */
+const MARKDOWNVIEWER_CLASS = 'jp-MarkdownViewer';
+
+/**
+ * The markdown MIME type.
+ */
+const MIMETYPE = 'text/markdown';
+
+/**
+ * A widget for markdown documents.
+ */
+export class MarkdownViewer extends Widget {
+  /**
+   * Construct a new markdown viewer widget.
+   */
+  constructor(options: MarkdownViewer.IOptions) {
+    super();
+    this.context = options.context;
+    this.renderer = options.renderer;
+    this.node.tabIndex = -1;
+    this.addClass(MARKDOWNVIEWER_CLASS);
+
+    const layout = (this.layout = new StackedLayout());
+    layout.addWidget(this.renderer);
+
+    this.context.ready.then(async () => {
+      await this._render();
+
+      // Throttle the rendering rate of the widget.
+      this._monitor = new ActivityMonitor({
+        signal: this.context.model.contentChanged,
+        timeout: this._config.renderTimeout
+      });
+      this._monitor.activityStopped.connect(
+        this.update,
+        this
+      );
+
+      this._ready.resolve(undefined);
+    });
+  }
+
+  /**
+   * A promise that resolves when the markdown viewer is ready.
+   */
+  get ready(): Promise<void> {
+    return this._ready.promise;
+  }
+
+  /**
+   * Set URI fragment identifier.
+   */
+  setFragment(fragment: string) {
+    this._fragment = fragment;
+    this.update();
+  }
+
+  /**
+   * Set a config option for the markdown viewer.
+   */
+  setOption<K extends keyof MarkdownViewer.IConfig>(
+    option: K,
+    value: MarkdownViewer.IConfig[K]
+  ): void {
+    if (this._config[option] === value) {
+      return;
+    }
+    this._config[option] = value;
+    const { style } = this.renderer.node;
+    switch (option) {
+      case 'fontFamily':
+        style.fontFamily = value as string | null;
+        break;
+      case 'fontSize':
+        style.fontSize = value ? value + 'px' : null;
+        break;
+      case 'hideFrontMatter':
+        this.update();
+        break;
+      case 'lineHeight':
+        style.lineHeight = value ? value.toString() : null;
+        break;
+      case 'lineWidth':
+        const padding = value ? `calc(50% - ${(value as number) / 2}ch)` : null;
+        style.paddingLeft = padding;
+        style.paddingRight = padding;
+        break;
+      case 'renderTimeout':
+        this._monitor.timeout = value as number;
+        break;
+    }
+  }
+
+  /**
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    if (this._monitor) {
+      this._monitor.dispose();
+    }
+    this._monitor = null;
+    super.dispose();
+  }
+
+  /**
+   * Handle an `update-request` message to the widget.
+   */
+  protected onUpdateRequest(msg: Message): void {
+    if (this.context.isReady && !this.isDisposed) {
+      this._render();
+      this._fragment = '';
+    }
+  }
+
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this.node.focus();
+  }
+
+  /**
+   * Render the mime content.
+   */
+  private async _render(): Promise<void> {
+    if (this.isDisposed) {
+      return;
+    }
+
+    // Since rendering is async, we note render requests that happen while we
+    // actually are rendering for a future rendering.
+    if (this._isRendering) {
+      this._renderRequested = true;
+      return;
+    }
+
+    // Set up for this rendering pass.
+    this._renderRequested = false;
+    const { context } = this;
+    const { model } = context;
+    const source = model.toString();
+    const data: JSONObject = {};
+    // If `hideFrontMatter`is true remove front matter.
+    data[MIMETYPE] = this._config.hideFrontMatter
+      ? Private.removeFrontMatter(source)
+      : source;
+    const mimeModel = new MimeModel({
+      data,
+      metadata: { fragment: this._fragment }
+    });
+
+    try {
+      // Do the rendering asynchronously.
+      this._isRendering = true;
+      await this.renderer.renderModel(mimeModel);
+      this._isRendering = false;
+
+      // If there is an outstanding request to render, go ahead and render
+      if (this._renderRequested) {
+        return this._render();
+      }
+    } catch (reason) {
+      // Dispose the document if rendering fails.
+      requestAnimationFrame(() => {
+        this.dispose();
+      });
+      showErrorMessage(`Renderer Failure: ${context.path}`, reason);
+    }
+  }
+
+  readonly context: DocumentRegistry.Context;
+  readonly renderer: IRenderMime.IRenderer;
+
+  private _config = { ...MarkdownViewer.defaultConfig };
+  private _fragment = '';
+  private _monitor: ActivityMonitor<any, any> | null;
+  private _ready = new PromiseDelegate<void>();
+  private _isRendering = false;
+  private _renderRequested = false;
+}
+
+/**
+ * The namespace for MarkdownViewer class statics.
+ */
+export namespace MarkdownViewer {
+  /**
+   * The options used to initialize a MarkdownViewer.
+   */
+  export interface IOptions {
+    /**
+     * Context
+     */
+    context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
+
+    /**
+     * The renderer instance.
+     */
+    renderer: IRenderMime.IRenderer;
+  }
+
+  export interface IConfig {
+    /**
+     * User preferred font family for markdown viewer.
+     */
+    fontFamily: string | null;
+
+    /**
+     * User preferred size in pixel of the font used in markdown viewer.
+     */
+    fontSize: number | null;
+
+    /**
+     * User preferred text line height, as a multiplier of font size.
+     */
+    lineHeight: number | null;
+
+    /**
+     * User preferred text line width expressed in CSS ch units.
+     */
+    lineWidth: number | null;
+
+    /**
+     * Whether to hide the YALM front matter.
+     */
+    hideFrontMatter: boolean;
+
+    /**
+     * The render timeout.
+     */
+    renderTimeout: number;
+  }
+
+  /**
+   * The default configuration options for an editor.
+   */
+  export const defaultConfig: MarkdownViewer.IConfig = {
+    fontFamily: null,
+    fontSize: null,
+    lineHeight: null,
+    lineWidth: null,
+    hideFrontMatter: true,
+    renderTimeout: 1000
+  };
+}
+
+/**
+ * A document widget for markdown content.
+ */
+export class MarkdownDocument extends DocumentWidget<MarkdownViewer> {
+  setFragment(fragment: string): void {
+    this.content.setFragment(fragment);
+  }
+}
+
+/**
+ * A widget factory for markdown viewers.
+ */
+export class MarkdownViewerFactory extends ABCWidgetFactory<MarkdownDocument> {
+  /**
+   * Construct a new mimetype widget factory.
+   */
+  constructor(options: MarkdownViewerFactory.IOptions) {
+    super(Private.createRegistryOptions(options));
+    this._fileType = options.primaryFileType;
+    this._rendermime = options.rendermime;
+  }
+
+  /**
+   * Create a new widget given a context.
+   */
+  protected createNewWidget(
+    context: DocumentRegistry.Context
+  ): MarkdownDocument {
+    const rendermime = this._rendermime.clone({
+      resolver: context.urlResolver
+    });
+    const renderer = rendermime.createRenderer(MIMETYPE);
+    const content = new MarkdownViewer({ context, renderer });
+    content.title.iconClass = this._fileType.iconClass;
+    content.title.iconLabel = this._fileType.iconLabel;
+    const widget = new DocumentWidget({ content, context });
+
+    return widget;
+  }
+
+  private _fileType: DocumentRegistry.IFileType;
+  private _rendermime: RenderMimeRegistry;
+}
+
+/**
+ * The namespace for MimeDocumentFactory class statics.
+ */
+export namespace MarkdownViewerFactory {
+  /**
+   * The options used to initialize a MimeDocumentFactory.
+   */
+  export interface IOptions extends DocumentRegistry.IWidgetFactoryOptions {
+    /**
+     * The primary file type associated with the document.
+     */
+    primaryFileType: DocumentRegistry.IFileType;
+
+    /**
+     * The rendermime instance.
+     */
+    rendermime: RenderMimeRegistry;
+  }
+}
+
+/**
+ * A namespace for markdown viewer widget private data.
+ */
+namespace Private {
+  /**
+   * Create the document registry options.
+   */
+  export function createRegistryOptions(
+    options: MarkdownViewerFactory.IOptions
+  ): DocumentRegistry.IWidgetFactoryOptions {
+    return {
+      ...options,
+      readOnly: true
+    } as DocumentRegistry.IWidgetFactoryOptions;
+  }
+
+  /**
+   * Remove YALM front matter from source.
+   */
+  export function removeFrontMatter(source: string): string {
+    const re = /^---\n[^]*?\n(---|...)\n/;
+    const match = source.match(re);
+    if (!match) {
+      return source;
+    }
+    const { length } = match[0];
+    return source.slice(length);
+  }
+}

+ 2 - 2
packages/markdownviewer-extension/style/index.css → packages/markdownviewer/style/index.css

@@ -7,7 +7,7 @@
   --jp-private-markdownviewer-padding: 32px;
 }
 
-.jp-Document .jp-MimeDocument .jp-RenderedMarkdown {
+.jp-Document .jp-MarkdownViewer .jp-RenderedMarkdown {
   padding-top: var(--jp-private-markdownviewer-padding);
   padding-right: var(--jp-private-markdownviewer-padding);
   padding-bottom: var(--jp-private-markdownviewer-padding);
@@ -19,7 +19,7 @@
 | Presentation Mode (.jp-mod-presentationMode)
 |----------------------------------------------------------------------------*/
 
-.jp-mod-presentationMode .jp-MimeDocument .jp-RenderedHTMLCommon {
+.jp-mod-presentationMode .jp-MarkdownViewer .jp-RenderedHTMLCommon {
   --jp-content-font-size1: var(--jp-content-presentation-font-size1);
   --jp-code-font-size: var(--jp-code-presentation-font-size);
 }

+ 20 - 0
packages/markdownviewer/tdoptions.json

@@ -0,0 +1,20 @@
+{
+  "excludeNotExported": true,
+  "mode": "file",
+  "target": "es5",
+  "module": "es5",
+  "lib": [
+    "lib.es2015.d.ts",
+    "lib.es2015.collection.d.ts",
+    "lib.es2015.promise.d.ts",
+    "lib.dom.d.ts"
+  ],
+  "out": "../../docs/api/imageviewer",
+  "baseUrl": ".",
+  "paths": {
+    "@jupyterlab/*": ["../packages/*"]
+  },
+  "esModuleInterop": true,
+  "jsx": "react",
+  "types": []
+}

+ 22 - 0
packages/markdownviewer/tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../docregistry"
+    },
+    {
+      "path": "../rendermime"
+    }
+  ]
+}

+ 1 - 0
packages/metapackage/package.json

@@ -67,6 +67,7 @@
     "@jupyterlab/launcher-extension": "^0.19.1",
     "@jupyterlab/mainmenu": "^0.8.1",
     "@jupyterlab/mainmenu-extension": "^0.8.1",
+    "@jupyterlab/markdownviewer": "^0.19.1",
     "@jupyterlab/markdownviewer-extension": "^0.19.1",
     "@jupyterlab/mathjax2": "^0.7.1",
     "@jupyterlab/mathjax2-extension": "^0.7.1",

+ 1 - 0
packages/metapackage/src/index.ts

@@ -38,6 +38,7 @@ import '@jupyterlab/launcher';
 import '@jupyterlab/launcher-extension';
 import '@jupyterlab/mainmenu';
 import '@jupyterlab/mainmenu-extension';
+import '@jupyterlab/markdownviewer';
 import '@jupyterlab/markdownviewer-extension';
 import '@jupyterlab/mathjax2';
 import '@jupyterlab/mathjax2-extension';

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -117,6 +117,9 @@
     {
       "path": "../mainmenu-extension"
     },
+    {
+      "path": "../markdownviewer"
+    },
     {
       "path": "../markdownviewer-extension"
     },