Procházet zdrojové kódy

Reevaluate output renderer on some changes

Vidar Tonaas Fauske před 5 roky
rodič
revize
de7e53bbe8

+ 1 - 0
packages/outputarea/package.json

@@ -45,6 +45,7 @@
     "@phosphor/coreutils": "^1.3.1",
     "@phosphor/disposable": "^1.2.0",
     "@phosphor/messaging": "^1.2.3",
+    "@phosphor/properties": "^1.1.3",
     "@phosphor/signaling": "^1.2.3",
     "@phosphor/widgets": "^1.8.0"
   },

+ 39 - 12
packages/outputarea/src/widget.ts

@@ -9,6 +9,8 @@ import {
 
 import { Message } from '@phosphor/messaging';
 
+import { AttachedProperty } from '@phosphor/properties';
+
 import { Signal } from '@phosphor/signaling';
 
 import { Panel, PanelLayout } from '@phosphor/widgets';
@@ -330,7 +332,19 @@ export class OutputArea extends Widget {
     let renderer = (panel.widgets
       ? panel.widgets[1]
       : panel) as IRenderMime.IRenderer;
-    if (renderer.renderModel) {
+    // Check whether it is safe to reuse renderer:
+    // - Preferred mime type has not changed
+    // - Isolation has not changed
+    let mimeType = this.rendermime.preferredMimeType(
+      model.data,
+      model.trusted ? 'any' : 'ensure'
+    );
+    if (
+      renderer.renderModel &&
+      Private.currentPreferredMimetype.get(renderer) === mimeType &&
+      OutputArea.isIsolated(mimeType, model.metadata) ===
+        renderer instanceof Private.IsolatedRenderer
+    ) {
       void renderer.renderModel(model);
     } else {
       layout.widgets[index].dispose();
@@ -388,21 +402,12 @@ export class OutputArea extends Widget {
     if (!mimeType) {
       return null;
     }
-    let metadata = model.metadata;
-    let mimeMd = metadata[mimeType] as ReadonlyJSONObject;
-    let isolated = false;
-    // mime-specific higher priority
-    if (mimeMd && mimeMd['isolated'] !== undefined) {
-      isolated = mimeMd['isolated'] as boolean;
-    } else {
-      // fallback on global
-      isolated = metadata['isolated'] as boolean;
-    }
-
     let output = this.rendermime.createRenderer(mimeType);
+    let isolated = OutputArea.isIsolated(mimeType, model.metadata);
     if (isolated === true) {
       output = new Private.IsolatedRenderer(output);
     }
+    Private.currentPreferredMimetype.set(output, mimeType);
     output.renderModel(model).catch(error => {
       // Manually append error message to output
       const pre = document.createElement('pre');
@@ -578,6 +583,20 @@ export namespace OutputArea {
     return future.done;
   }
 
+  export function isIsolated(
+    mimeType: string,
+    metadata: ReadonlyJSONObject
+  ): boolean {
+    let mimeMd = metadata[mimeType] as ReadonlyJSONObject | undefined;
+    // mime-specific higher priority
+    if (mimeMd && mimeMd['isolated'] !== undefined) {
+      return !!mimeMd['isolated'];
+    } else {
+      // fallback on global
+      return !!metadata['isolated'];
+    }
+  }
+
   /**
    * An output area widget content factory.
    *
@@ -874,4 +893,12 @@ namespace Private {
 
     private _wrapped: IRenderMime.IRenderer;
   }
+
+  export const currentPreferredMimetype = new AttachedProperty<
+    IRenderMime.IRenderer,
+    string
+  >({
+    name: 'preferredMimetype',
+    create: owner => ''
+  });
 }

+ 54 - 0
tests/test-outputarea/src/widget.spec.ts

@@ -174,6 +174,60 @@ describe('outputarea/widget', () => {
         expect(widget.methods).to.contain('onModelChanged');
         expect(widget.widgets.length).to.equal(1);
       });
+
+      it('should rerender when preferred mimetype changes', () => {
+        // Add output with both safe and unsafe types
+        widget.model.clear();
+        widget.model.add({
+          output_type: 'display_data',
+          data: {
+            'image/svg+xml':
+              '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"></svg>',
+            'text/plain': 'hello, world'
+          },
+          metadata: {}
+        });
+        expect(widget.node.innerHTML).to.contain(
+          '<img src="data:image/svg+xml'
+        );
+        widget.model.trusted = !widget.model.trusted;
+        expect(widget.node.innerHTML).to.not.contain(
+          '<img src="data:image/svg+xml'
+        );
+        widget.model.trusted = !widget.model.trusted;
+        expect(widget.node.innerHTML).to.contain(
+          '<img src="data:image/svg+xml'
+        );
+      });
+
+      it('should rerender when isolation changes', () => {
+        // Add output with both safe and unsafe types
+        widget.model.clear();
+        widget.model.add({
+          output_type: 'display_data',
+          data: {
+            'text/plain': 'hello, world'
+          }
+        });
+        expect(widget.node.innerHTML).to.not.contain('<iframe');
+        widget.model.set(0, {
+          output_type: 'display_data',
+          data: {
+            'text/plain': 'hello, world'
+          },
+          metadata: {
+            isolated: true
+          }
+        });
+        expect(widget.node.innerHTML).to.contain('<iframe');
+        widget.model.set(0, {
+          output_type: 'display_data',
+          data: {
+            'text/plain': 'hello, world'
+          }
+        });
+        expect(widget.node.innerHTML).to.not.contain('<iframe');
+      });
     });
 
     describe('.execute()', () => {