Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform Plugin API #1699

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 44 additions & 7 deletions docs/PluginAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## HydrogenProvider

Version: 1.0.0
Version: 1.3.0

The Plugin API allows you to make Hydrogen awesome.
You will be able to interact with this class in your Hydrogen Plugin using
Expand All @@ -20,7 +20,7 @@ Calls your callback when the kernel has changed.

### Params:

* **Function** *Callback*
* **Function** *Callback*

## getActiveKernel()

Expand All @@ -39,6 +39,44 @@ Get the `atom$Range` that will run if `hydrogen:run-cell` is called.

* **Class** `atom$Range`

## registerTransform(transform, key)

Registers a new transform for `display_data` and `execute_result` outputs.

* If the provided key already exists, no change will be made.
* `undefined` is returned if the registry fails.
* A `Symbol(react.element)` is returned else-wise.

The following keys are registered by default:
`["vega3", "vega2", "plotly", "vegalite2", "vegalite1", "json", "js", "html", "markdown", "latex", "svg", "gif", "jpeg", "png", "plain"]`
It is recommended you don't delete those, since custom transforms will take priority in displaying.

### Params:

* **String** *key*
* **Function | Class<any>** *transform*
* **Note:** This must be compatible with `React.createElement(transform)`.
* **Note:** You must set your `defaultProps` to have a `mediaType`.
* **Ex:** `defaultProps = { mediaType: "text/markdown"}`.
* **Note:** Data is passed in via a prop called `data`.

### Return:
* **Symbol(react.element) | undefined** *transform for key*

## unregisterTransform(key)

Unregisters a transform for `display_data` and `execute_result` outputs.
* `false` is returned if the key does not exist.
* `true` is returned else-wise.

### Params:

* **String** *key*

### Return:
* **Boolean** *didKeyExist*


--------

<!-- End lib/plugin-api/hydrogen-provider.js -->
Expand All @@ -52,11 +90,11 @@ and exposes a small set of methods that should be usable by plugins.

## language

The language of the kernel, as specified in its kernelspec
The language of the kernel, as specified in its kernelspec.

## displayName

The display name of the kernel, as specified in its kernelspec
The display name of the kernel, as specified in its kernelspec.

## addMiddleware(middleware)

Expand All @@ -68,15 +106,15 @@ If the methods of a `middleware` object are added/modified/deleted after

### Params:

* **HydrogenKernelMiddleware** *middleware*
* **HydrogenKernelMiddleware** *middleware*

## onDidDestroy(Callback)

Calls your callback when the kernel has been destroyed.

### Params:

* **Function** *Callback*
* **Function** *Callback*

## getConnectionFile()

Expand All @@ -87,4 +125,3 @@ Get the [connection file](http://jupyter-notebook.readthedocs.io/en/latest/examp
* **String** Path to connection file.

<!-- End lib/plugin-api/hydrogen-kernel.js -->

29 changes: 4 additions & 25 deletions lib/components/result-view/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,11 @@ import {
import PlotlyTransform from "@nteract/transform-plotly";
import { VegaLite1, VegaLite2, Vega2, Vega3 } from "@nteract/transform-vega";

import Markdown from "./markdown";

// All supported media types for output go here
export const supportedMediaTypes = (
<RichMedia>
<Vega3 />
<Vega2 />
<PlotlyTransform />
<VegaLite2 />
<VegaLite1 />
<Media.Json />
<Media.JavaScript />
<Media.HTML />
<Markdown />
<Media.LaTeX />
<Media.SVG />
<Media.Image mediaType="image/gif" />
<Media.Image mediaType="image/jpeg" />
<Media.Image mediaType="image/png" />
<Media.Plain />
</RichMedia>
);
import transforms from "./../transforms";

export function isTextOutputOnly(data: Object) {
const supported = React.Children.map(
supportedMediaTypes.props.children,
transforms.components,
mediaComponent => mediaComponent.props.mediaType
);
const bundleMediaTypes = [...Object.keys(data)].filter(mediaType =>
Expand All @@ -57,8 +36,8 @@ class Display extends React.Component<{ output: any }> {
render() {
return (
<Output output={toJS(this.props.output)}>
<ExecuteResult expanded>{supportedMediaTypes}</ExecuteResult>
<DisplayData expanded>{supportedMediaTypes}</DisplayData>
<ExecuteResult expanded>{transforms.components}</ExecuteResult>
<DisplayData expanded>{transforms.components}</DisplayData>
<StreamText expanded />
<KernelOutputError expanded />
</Output>
Expand Down
74 changes: 74 additions & 0 deletions lib/components/transforms/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* @flow */

import { observable, action, computed } from "mobx";

import React from "react";
import {
DisplayData,
ExecuteResult,
StreamText,
KernelOutputError,
Output,
Media,
RichMedia
} from "@nteract/outputs";
import PlotlyTransform from "@nteract/transform-plotly";
import { VegaLite1, VegaLite2, Vega2, Vega3 } from "@nteract/transform-vega";

import Markdown from "./markdown";

export class TransformManager {
@observable
transforms: Map<string, React$Element<any>> = new Map([
//If you add to this list update the following:
// - Defaults in the Plugin API docs
// - Defaults in the transforms specs
["vega3", <Vega3 />],
["vega2", <Vega2 />],
["plotly", <PlotlyTransform />],
["vegalite2", <VegaLite2 />],
["vegalite1", <VegaLite1 />],
["json", <Media.Json />],
["js", <Media.JavaScript />],
["html", <Media.HTML />],
["markdown", <Markdown />],
["latex", <Media.LaTeX />],
["svg", <Media.SVG />],
["gif", <Media.Image mediaType="image/gif" />],
["jpeg", <Media.Image mediaType="image/jpeg" />],
["png", <Media.Image mediaType="image/png" />],
["plain", <Media.Plain />]
]);

@action
addTransform(
key: string,
transform: Function | Class<any>
): React$Element<any> | void {
//Add new transform to the beginning giving it priority in `Display`
//Using Map.protoype.set() adds to the bottom, so we can't use that
//Original components will always override a new transform with same keys
//This forces delete before adding same key values.
this.transforms = new Map([
[key, React.createElement(transform)],
...this.transforms
]);
return this.transforms.get(key);
}

@action
deleteTransform(key: string): boolean {
return this.transforms.delete(key);
}

@computed
get components(): Array<React$Element<any>> {
return Array.from(this.transforms.values());
}
}

const transformManager = new TransformManager();
export default transformManager;

//For debugging.
window.transformManager = transformManager;
37 changes: 31 additions & 6 deletions lib/plugin-api/hydrogen-provider.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* @flow */

import store from "./../store";
import transformManager from "./../components/transforms";
import type Kernel from "./../kernel";
import type ZMQKernel from "./../zmq-kernel.js";
import { getCurrentCell } from "./../code-manager";
/**
* @version 1.0.0
* @version 1.3.0
*
*
* The Plugin API allows you to make Hydrogen awesome.
Expand All @@ -25,9 +26,9 @@ export default class HydrogenProvider {
this._hydrogen = _hydrogen;
}

/*
/**
* Calls your callback when the kernel has changed.
* @param {Function} Callback
* @param {Function} callback
*/
onDidChangeKernel(callback: Function) {
this._hydrogen.emitter.on("did-change-kernel", (kernel: ?Kernel) => {
Expand All @@ -38,7 +39,7 @@ export default class HydrogenProvider {
});
}

/*
/**
* Get the `HydrogenKernel` of the currently active text editor.
* @return {Class} `HydrogenKernel`
*/
Expand All @@ -51,16 +52,40 @@ export default class HydrogenProvider {
return store.kernel.getPluginWrapper();
}

/*
/**
* Get the `atom$Range` that will run if `hydrogen:run-cell` is called.
* `null` is returned if no active text editor.
* @return {Class} `atom$Range`
* @return {Class} atom$Range
*/
getCellRange(editor: ?atom$TextEditor) {
if (!store.editor) return null;
return getCurrentCell(store.editor);
}

/**
* Registers a new transform for `display_data` and `execute_result` outputs.
* If the provided key already exists, no change will be made.
* `void` is returned if the registry fails.
* `React$Element<any>` is returned else-wise.
* @param {string} key
* @param {Function | Class<any>} transform
* @return {Symbol(react.element) | undefined} Transform For Key
*/
registerTransform(key: string, transform: Function | Class<any>) {
return transformManager.addTransform(key, transform);
}

/**
* Unregisters a transform for `display_data` and `execute_result` outputs.
* `false` is returned if the key does not exist.
* `true` is returned else-wise.
* @param {string} key
* @return {boolean}
*/
unregisterTransform(key: string) {
return transformManager.deleteTransform(key);
}

/*
*--------
*/
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
"hydrogen.provider": {
"versions": {
"1.1.0": "provideHydrogen",
"1.2.0": "provideHydrogen"
"1.2.0": "provideHydrogen",
"1.3.0": "provideHydrogen"
}
}
},
Expand Down
66 changes: 66 additions & 0 deletions spec/components/transforms/index-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use babel";

import transformManager from "./../../../lib/components/transforms";
import Markdown from "./../../../lib/components/transforms/markdown";

describe("Transforms", () => {
it("should contain default transforms", () => {
expect(Array.from(transformManager.transforms.keys())).toEqual([
"vega3",
"vega2",
"plotly",
"vegalite2",
"vegalite1",
"json",
"js",
"html",
"markdown",
"latex",
"svg",
"gif",
"jpeg",
"png",
"plain"
]);
});

describe("addTransform", () => {
it("should add a new transform", () => {
expect(transformManager.addTransform("mark", Markdown)).toBeTruthy();
expect(transformManager.transforms.has("mark")).toBeTruthy();
expect(transformManager.transforms.get("mark").type).toEqual(Markdown);
});

it("should not override an existing transform", () => {
expect(transformManager.addTransform("svg", Markdown)).toBeTruthy();
expect(transformManager.transforms.get("svg").type).not.toEqual(Markdown);
});
});

describe("deleteTransform", () => {
it("should remove a transform", () => {
expect(transformManager.deleteTransform("mark")).toBeTruthy();
expect(!transformManager.transforms.has("mark")).toBeTruthy();
});

it("should return false for non-existent key", () => {
//Previously a transform
expect(!transformManager.deleteTransform("mark")).toBeTruthy();
//Never a transform
expect(!transformManager.deleteTransform("non-existent")).toBeTruthy();
});
});

describe("components", () => {
it("should return array of react elements only", () => {
let allReactElements = true;
for (let element of transformManager.components) {
if (element.$$typeof != Symbol.for("react.element")) {
allReactElements = false;
break;
}
}
expect(allReactElements).toBeTruthy();
});
});
});