UNPKG

@jupyterlab/docregistry

Version:
385 lines (342 loc) 10.1 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Printing, showErrorMessage } from '@jupyterlab/apputils'; import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; import { ActivityMonitor } from '@jupyterlab/coreutils'; import { IRenderMime, IRenderMimeRegistry, MimeModel } from '@jupyterlab/rendermime'; import { ITranslator, nullTranslator, TranslationBundle } from '@jupyterlab/translation'; import { JSONExt, PartialJSONObject, PromiseDelegate } from '@lumino/coreutils'; import { Message, MessageLoop } from '@lumino/messaging'; import { StackedLayout, Widget } from '@lumino/widgets'; import { ABCWidgetFactory, DocumentWidget } from './default'; import { DocumentRegistry } from './registry'; /** * A content widget for a rendered mimetype document. */ export class MimeContent extends Widget { /** * Construct a new widget. */ constructor(options: MimeContent.IOptions) { super(); this.addClass('jp-MimeDocument'); this.translator = options.translator || nullTranslator; this._trans = this.translator.load('jupyterlab'); this.mimeType = options.mimeType; this._dataType = options.dataType || 'string'; this._context = options.context; this.renderer = options.renderer; const layout = (this.layout = new StackedLayout()); layout.addWidget(this.renderer); this._context.ready .then(() => { return this._render(); }) .then(() => { // After rendering for the first time, send an activation request if we // are currently focused. if (this.node === document.activeElement) { // We want to synchronously send (not post) the activate message, while // we know this node still has focus. MessageLoop.sendMessage(this.renderer, Widget.Msg.ActivateRequest); } // Throttle the rendering rate of the widget. this._monitor = new ActivityMonitor({ signal: this._context.model.contentChanged, timeout: options.renderTimeout }); this._monitor.activityStopped.connect(this.update, this); this._ready.resolve(undefined); }) .catch(reason => { // Dispose the document if rendering fails. requestAnimationFrame(() => { this.dispose(); }); void showErrorMessage( this._trans.__('Renderer Failure: %1', this._context.path), reason ); }); } /** * The mimetype for this rendered content. */ readonly mimeType: string; /** * Print method. Deferred to the renderer. */ [Printing.symbol](): Printing.OptionalAsyncThunk { return Printing.getPrintFunction(this.renderer); } /** * A promise that resolves when the widget is ready. */ get ready(): Promise<void> { return this._ready.promise; } /** * Set URI fragment identifier. */ setFragment(fragment: string): void { this._fragment = fragment; this.update(); } /** * 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) { void this._render(); this._fragment = ''; } } /** * 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._context; const model = context.model; const data: PartialJSONObject = {}; if (this._dataType === 'string') { data[this.mimeType] = model.toString(); } else { data[this.mimeType] = model.toJSON(); } const mimeModel = new MimeModel({ data, callback: this._changeCallback, 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(); }); void showErrorMessage( this._trans.__('Renderer Failure: %1', context.path), reason ); } } /** * A bound change callback. */ private _changeCallback = ( options: IRenderMime.IMimeModel.ISetDataOptions ) => { if (!options.data || !options.data[this.mimeType]) { return; } const data = options.data[this.mimeType]; if (typeof data === 'string') { if (data !== this._context.model.toString()) { this._context.model.fromString(data); } } else if ( data !== null && data !== undefined && !JSONExt.deepEqual(data, this._context.model.toJSON()) ) { this._context.model.fromJSON(data); } }; readonly renderer: IRenderMime.IRenderer; protected translator: ITranslator; private _trans: TranslationBundle; private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>; private _fragment = ''; private _monitor: ActivityMonitor<DocumentRegistry.IModel, void> | null; private _ready = new PromiseDelegate<void>(); private _dataType: 'string' | 'json'; private _isRendering = false; private _renderRequested = false; } /** * The namespace for MimeDocument class statics. */ export namespace MimeContent { /** * The options used to initialize a MimeDocument. */ export interface IOptions { /** * Context */ context: DocumentRegistry.IContext<DocumentRegistry.IModel>; /** * The renderer instance. */ renderer: IRenderMime.IRenderer; /** * The mime type. */ mimeType: string; /** * The render timeout. */ renderTimeout: number; /** * Preferred data type from the model. */ dataType?: 'string' | 'json'; /** * The application language translator. */ translator?: ITranslator; } } /** * A document widget for mime content. */ export class MimeDocument extends DocumentWidget<MimeContent> { setFragment(fragment: string): void { this.content.setFragment(fragment); } } /** * An implementation of a widget factory for a rendered mimetype document. */ export class MimeDocumentFactory extends ABCWidgetFactory<MimeDocument> { /** * Construct a new mimetype widget factory. */ constructor(options: MimeDocumentFactory.IOptions<MimeDocument>) { super(Private.createRegistryOptions(options)); this._rendermime = options.rendermime; this._renderTimeout = options.renderTimeout || 1000; this._dataType = options.dataType || 'string'; this._fileType = options.primaryFileType; this._factory = options.factory; } /** * Create a new widget given a context. */ protected createNewWidget(context: DocumentRegistry.Context): MimeDocument { const ft = this._fileType; const mimeType = ft?.mimeTypes.length ? ft.mimeTypes[0] : IEditorMimeTypeService.defaultMimeType; const rendermime = this._rendermime.clone({ resolver: context.urlResolver }); let renderer: IRenderMime.IRenderer; if (this._factory && this._factory.mimeTypes.includes(mimeType)) { renderer = this._factory.createRenderer({ mimeType, resolver: rendermime.resolver, sanitizer: rendermime.sanitizer, linkHandler: rendermime.linkHandler, latexTypesetter: rendermime.latexTypesetter, markdownParser: rendermime.markdownParser }); } else { renderer = rendermime.createRenderer(mimeType); } const content = new MimeContent({ context, renderer, mimeType, renderTimeout: this._renderTimeout, dataType: this._dataType }); content.title.icon = ft?.icon; content.title.iconClass = ft?.iconClass ?? ''; content.title.iconLabel = ft?.iconLabel ?? ''; const widget = new MimeDocument({ content, context }); return widget; } private _rendermime: IRenderMimeRegistry; private _renderTimeout: number; private _dataType: 'string' | 'json'; private _fileType: DocumentRegistry.IFileType | undefined; private _factory: IRenderMime.IRendererFactory | undefined; } /** * The namespace for MimeDocumentFactory class statics. */ export namespace MimeDocumentFactory { /** * The options used to initialize a MimeDocumentFactory. */ export interface IOptions<T extends MimeDocument> extends DocumentRegistry.IWidgetFactoryOptions<T> { /** * The primary file type associated with the document. */ primaryFileType: DocumentRegistry.IFileType | undefined; /** * The rendermime instance. */ rendermime: IRenderMimeRegistry; /** * The render timeout. */ renderTimeout?: number; /** * Preferred data type from the model. */ dataType?: 'string' | 'json'; /** * Optional renderer to use (overriding the renderer in the registry) */ factory?: IRenderMime.IRendererFactory; } } /** * The namespace for the module implementation details. */ namespace Private { /** * Create the document registry options. */ export function createRegistryOptions( options: MimeDocumentFactory.IOptions<MimeDocument> ): DocumentRegistry.IWidgetFactoryOptions<MimeDocument> { return { ...options, readOnly: true } as DocumentRegistry.IWidgetFactoryOptions<MimeDocument>; } }