浏览代码

Improve anchor tags (#4692)

* Add defaultRendered to widget factory options.

* Add a command to the rendermime extension to open a path, then scroll to
a header id.

* Correctly thread `defaultRendered` to ABCWidgetFactory and
MimeDocumentFactory.

* Add tests.

Prettier.

* Add the defaultRendered to the second-highest-ranked position in
preferredWidgetFactories (after the default widget factory).
Ian Rose 6 年之前
父节点
当前提交
b354907145

+ 2 - 1
packages/application/src/mimerenderers.ts

@@ -138,7 +138,8 @@ export function createRendermimePlugin(
           name: option.name,
           primaryFileType: registry.getFileType(option.primaryFileType),
           fileTypes: option.fileTypes,
-          defaultFor: option.defaultFor
+          defaultFor: option.defaultFor,
+          defaultRendered: option.defaultRendered
         });
         registry.addWidgetFactory(factory);
 

+ 10 - 0
packages/docregistry/src/default.ts

@@ -282,6 +282,7 @@ export abstract class ABCWidgetFactory<
     this._name = options.name;
     this._readOnly = options.readOnly === undefined ? false : options.readOnly;
     this._defaultFor = options.defaultFor ? options.defaultFor.slice() : [];
+    this._defaultRendered = (options.defaultRendered || []).slice();
     this._fileTypes = options.fileTypes.slice();
     this._modelName = options.modelName || 'text';
     this._preferKernel = !!options.preferKernel;
@@ -344,6 +345,14 @@ export abstract class ABCWidgetFactory<
     return this._defaultFor.slice();
   }
 
+  /**
+   * The file types for which the factory should be the default for
+   * rendering a document model, if different from editing.
+   */
+  get defaultRendered(): string[] {
+    return this._defaultRendered.slice();
+  }
+
   /**
    * Whether the widgets prefer having a kernel started.
    */
@@ -383,6 +392,7 @@ export abstract class ABCWidgetFactory<
   private _modelName: string;
   private _fileTypes: string[];
   private _defaultFor: string[];
+  private _defaultRendered: string[];
   private _widgetCreated = new Signal<DocumentRegistry.IWidgetFactory<T, U>, T>(
     this
   );

+ 63 - 4
packages/docregistry/src/registry.ts

@@ -128,6 +128,12 @@ export class DocumentRegistry implements IDisposable {
         this._defaultWidgetFactories[ft] = name;
       }
     }
+    for (let ft of factory.defaultRendered || []) {
+      if (factory.fileTypes.indexOf(ft) === -1) {
+        continue;
+      }
+      this._defaultRenderedWidgetFactories[ft] = name;
+    }
     // For convenience, store a mapping of file type name -> name
     for (let ft of factory.fileTypes) {
       if (!this._widgetFactoryExtensions[ft]) {
@@ -150,6 +156,11 @@ export class DocumentRegistry implements IDisposable {
           delete this._defaultWidgetFactories[ext];
         }
       }
+      for (let ext of Object.keys(this._defaultRenderedWidgetFactories)) {
+        if (this._defaultRenderedWidgetFactories[ext] === name) {
+          delete this._defaultRenderedWidgetFactories[ext];
+        }
+      }
       for (let ext of Object.keys(this._widgetFactoryExtensions)) {
         ArrayExt.removeFirstOf(this._widgetFactoryExtensions[ext], name);
         if (this._widgetFactoryExtensions[ext].length === 0) {
@@ -283,9 +294,10 @@ export class DocumentRegistry implements IDisposable {
    * #### Notes
    * Only the widget factories whose associated model factory have
    * been registered will be returned.
-   * The first item is considered the default. The returned iterator
+   * The first item is considered the default. The returned array
    * has widget factories in the following order:
    * - path-specific default factory
+   * - path-specific default rendered factory
    * - global default factory
    * - all other path-specific factories
    * - all other global factories
@@ -303,6 +315,13 @@ export class DocumentRegistry implements IDisposable {
       }
     });
 
+    // Add the file type default rendered factories.
+    fts.forEach(ft => {
+      if (ft.name in this._defaultRenderedWidgetFactories) {
+        factories.add(this._defaultRenderedWidgetFactories[ft.name]);
+      }
+    });
+
     // Add the global default factory.
     if (this._defaultWidgetFactory) {
       factories.add(this._defaultWidgetFactory);
@@ -342,11 +361,41 @@ export class DocumentRegistry implements IDisposable {
   }
 
   /**
-   * Get the default widget factory for an extension.
+   * Get the default rendered widget factory for a path.
    *
-   * @param ext - An optional file path to filter the results.
+   * @param path - The path to for which to find a widget factory.
    *
-   * @returns The default widget factory for an extension.
+   * @returns The default rendered widget factory for the path.
+   *
+   * ### Notes
+   * If the widget factory has registered a separate set of `defaultRendered`
+   * file types and there is a match in that set, this returns that.
+   * Otherwise, this returns the same widget factory as
+   * [[defaultWidgetFactory]].
+   */
+  defaultRenderedWidgetFactory(path: string): DocumentRegistry.WidgetFactory {
+    // Get the matching file types.
+    let fts = this.getFileTypesForPath(PathExt.basename(path));
+
+    let factory: DocumentRegistry.WidgetFactory = undefined;
+    // Find if a there is a default rendered factory for this type.
+    for (let ft of fts) {
+      if (ft.name in this._defaultRenderedWidgetFactories) {
+        factory = this._widgetFactories[
+          this._defaultRenderedWidgetFactories[ft.name]
+        ];
+        break;
+      }
+    }
+    return factory || this.defaultWidgetFactory(path);
+  }
+
+  /**
+   * Get the default widget factory for a path.
+   *
+   * @param path - An optional file path to filter the results.
+   *
+   * @returns The default widget factory for an path.
    *
    * #### Notes
    * This is equivalent to the first value in [[preferredWidgetFactories]].
@@ -557,6 +606,9 @@ export class DocumentRegistry implements IDisposable {
   private _defaultWidgetFactories: { [key: string]: string } = Object.create(
     null
   );
+  private _defaultRenderedWidgetFactories: {
+    [key: string]: string;
+  } = Object.create(null);
   private _widgetFactoryExtensions: { [key: string]: string[] } = Object.create(
     null
   );
@@ -838,6 +890,13 @@ export namespace DocumentRegistry {
      */
     readonly defaultFor?: ReadonlyArray<string>;
 
+    /**
+     * The file types for which the factory should be the default for rendering,
+     * if that is different than the default factory (which may be for editing).
+     * If undefined, then it will fall back on the default file type.
+     */
+    readonly defaultRendered?: ReadonlyArray<string>;
+
     /**
      * Whether the widget factory is read only.
      */

+ 5 - 1
packages/fileeditor-extension/src/index.ts

@@ -117,7 +117,11 @@ function activate(
   const namespace = 'editor';
   const factory = new FileEditorFactory({
     editorServices,
-    factoryOptions: { name: FACTORY, fileTypes: ['*'], defaultFor: ['*'] }
+    factoryOptions: {
+      name: FACTORY,
+      fileTypes: ['markdown', '*'], // Explicitly add the markdown fileType so
+      defaultFor: ['markdown', '*'] // it outranks the defaultRendered viewer.
+    }
   });
   const { commands, restored } = app;
   const tracker = new InstanceTracker<IDocumentWidget<FileEditor>>({

+ 2 - 1
packages/markdownviewer-extension/src/index.ts

@@ -20,7 +20,8 @@ const extension: IRenderMime.IExtension = {
   documentWidgetFactoryOptions: {
     name: FACTORY,
     primaryFileType: 'markdown',
-    fileTypes: ['markdown']
+    fileTypes: ['markdown'],
+    defaultRendered: ['markdown']
   }
 };
 

+ 1 - 0
packages/rendermime-extension/package.json

@@ -29,6 +29,7 @@
   },
   "dependencies": {
     "@jupyterlab/application": "^0.17.0-1",
+    "@jupyterlab/docmanager": "^0.17.0-1",
     "@jupyterlab/rendermime": "^0.17.0-1"
   },
   "devDependencies": {

+ 56 - 3
packages/rendermime-extension/src/index.ts

@@ -5,6 +5,8 @@
 
 import { JupyterLab, JupyterLabPlugin } from '@jupyterlab/application';
 
+import { IDocumentManager } from '@jupyterlab/docmanager';
+
 import {
   ILatexTypesetter,
   IRenderMimeRegistry,
@@ -12,11 +14,16 @@ import {
   standardRendererFactories
 } from '@jupyterlab/rendermime';
 
+namespace CommandIDs {
+  export const handleLink = 'rendermime:handle-local-link';
+}
+
 /**
  * A plugin providing a rendermime registry.
  */
 const plugin: JupyterLabPlugin<RenderMimeRegistry> = {
   id: '@jupyterlab/rendermime-extension:plugin',
+  requires: [IDocumentManager],
   optional: [ILatexTypesetter],
   provides: IRenderMimeRegistry,
   activate: activate,
@@ -31,12 +38,58 @@ export default plugin;
 /**
  * Activate the rendermine plugin.
  */
-function activate(app: JupyterLab, latexTypesetter: ILatexTypesetter) {
+function activate(
+  app: JupyterLab,
+  docManager: IDocumentManager,
+  latexTypesetter: ILatexTypesetter | null
+) {
+  app.commands.addCommand(CommandIDs.handleLink, {
+    label: 'Handle Local Link',
+    execute: args => {
+      const path = args['path'] as string | undefined | null;
+      const id = args['id'] as string | undefined | null;
+      if (!path) {
+        return;
+      }
+      // First check if the path exists on the server.
+      return docManager.services.contents
+        .get(path, { content: false })
+        .then(() => {
+          // Open the link with the default rendered widget factory,
+          // if applicable.
+          const factory = docManager.registry.defaultRenderedWidgetFactory(
+            path
+          );
+          const widget = docManager.openOrReveal(path, factory.name);
+          if (!widget) {
+            return;
+          }
+          return widget.revealed.then(() => {
+            // Once the widget is ready, attempt to scroll the hash into view
+            // if one has been provided.
+            if (!id) {
+              return;
+            }
+            // Look for the an element with the hash id in the document.
+            // This id is set automatically for headers tags when
+            // we render markdown.
+            const element = widget.node.querySelector(id);
+            if (element) {
+              element.scrollIntoView();
+            }
+            return;
+          });
+        });
+    }
+  });
   return new RenderMimeRegistry({
     initialFactories: standardRendererFactories,
     linkHandler: {
-      handleLink: (node, path) => {
-        app.commandLinker.connectNode(node, 'docmanager:open', { path: path });
+      handleLink: (node: HTMLElement, path: string, id?: string) => {
+        app.commandLinker.connectNode(node, CommandIDs.handleLink, {
+          path,
+          id
+        });
       }
     },
     latexTypesetter

+ 14 - 1
packages/rendermime-interfaces/src/index.ts

@@ -91,6 +91,13 @@ export namespace IRenderMime {
      * The file types for which the factory should be the default.
      */
     readonly defaultFor?: ReadonlyArray<string>;
+
+    /**
+     * The file types for which the factory should be the default for rendering,
+     * if that is different than the default factory (which may be for editing)
+     * If undefined, then it will fall back on the default file type.
+     */
+    readonly defaultRendered?: ReadonlyArray<string>;
   }
 
   /**
@@ -296,8 +303,14 @@ export namespace IRenderMime {
   export interface ILinkHandler {
     /**
      * Add the link handler to the node.
+     *
+     * @param node: the node for which to handle the link.
+     *
+     * @param path: the path to open when the link is clicked.
+     *
+     * @param id: an optional element id to scroll to when the path is opened.
      */
-    handleLink(node: HTMLElement, url: string): void;
+    handleLink(node: HTMLElement, path: string, id?: string): void;
   }
 
   /**

+ 1 - 1
packages/rendermime/src/renderers.ts

@@ -767,7 +767,7 @@ namespace Private {
       .then(path => {
         // Handle the click override.
         if (linkHandler) {
-          linkHandler.handleLink(anchor, path);
+          linkHandler.handleLink(anchor, path, hash);
         }
         // Get the appropriate file download path.
         return resolver.getDownloadUrl(path);

+ 19 - 0
tests/test-docregistry/src/default.spec.ts

@@ -81,6 +81,25 @@ describe('docregistry/default', () => {
       });
     });
 
+    describe('#defaultRendered', () => {
+      it('should default to an empty array', () => {
+        let factory = new WidgetFactory({
+          name: 'test',
+          fileTypes: ['text']
+        });
+        expect(factory.defaultRendered).to.eql([]);
+      });
+
+      it('should be the value passed in', () => {
+        let factory = new WidgetFactory({
+          name: 'test',
+          fileTypes: ['text'],
+          defaultRendered: ['text']
+        });
+        expect(factory.defaultRendered).to.eql(['text']);
+      });
+    });
+
     describe('#readOnly', () => {
       it('should default to false', () => {
         let factory = new WidgetFactory({

+ 52 - 2
tests/test-docregistry/src/registry.spec.ts

@@ -40,8 +40,9 @@ function createFactory(modelName?: string) {
   return new WidgetFactory({
     name: UUID.uuid4(),
     modelName: modelName || 'text',
-    fileTypes: ['text', 'foobar'],
-    defaultFor: ['text', 'foobar']
+    fileTypes: ['text', 'foobar', 'baz'],
+    defaultFor: ['text', 'foobar'],
+    defaultRendered: ['baz']
   });
 }
 
@@ -55,6 +56,10 @@ describe('docregistry/registry', () => {
         name: 'foobar',
         extensions: ['.foo.bar']
       });
+      registry.addFileType({
+        name: 'baz',
+        extensions: ['.baz']
+      });
     });
 
     afterEach(() => {
@@ -270,6 +275,26 @@ describe('docregistry/registry', () => {
         expect(toArray(factories)).to.eql([factory, gFactory]);
       });
 
+      it('should list a default rendered factory after the default factory', () => {
+        let factory = createFactory();
+        registry.addWidgetFactory(factory);
+        let gFactory = new WidgetFactory({
+          name: 'global',
+          fileTypes: ['*'],
+          defaultFor: ['*']
+        });
+        registry.addWidgetFactory(gFactory);
+        let mdFactory = new WidgetFactory({
+          name: 'markdown',
+          fileTypes: ['markdown'],
+          defaultRendered: ['markdown']
+        });
+        registry.addWidgetFactory(mdFactory);
+
+        let factories = registry.preferredWidgetFactories('a.md');
+        expect(factories).to.eql([mdFactory, gFactory]);
+      });
+
       it('should handle multi-part extensions', () => {
         let factory = createFactory();
         registry.addWidgetFactory(factory);
@@ -324,6 +349,31 @@ describe('docregistry/registry', () => {
       });
     });
 
+    describe('#defaultRenderedWidgetFactory()', () => {
+      it('should get the default rendered widget factory for a given extension', () => {
+        let factory = createFactory();
+        registry.addWidgetFactory(factory);
+        let mdFactory = new WidgetFactory({
+          name: 'markdown',
+          fileTypes: ['markdown'],
+          defaultRendered: ['markdown']
+        });
+        registry.addWidgetFactory(mdFactory);
+        expect(registry.defaultRenderedWidgetFactory('a.baz')).to.be(factory);
+        expect(registry.defaultRenderedWidgetFactory('a.md')).to.be(mdFactory);
+      });
+
+      it('should get the default widget factory if no default rendered factory is registered', () => {
+        let gFactory = new WidgetFactory({
+          name: 'global',
+          fileTypes: ['*'],
+          defaultFor: ['*']
+        });
+        registry.addWidgetFactory(gFactory);
+        expect(registry.defaultRenderedWidgetFactory('a.md')).to.be(gFactory);
+      });
+    });
+
     describe('#fileTypes()', () => {
       it('should get the registered file types', () => {
         registry = new DocumentRegistry({ initialFileTypes: [] });