Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions modules/kernels/javascript/codedown-d3/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
declare module "codedown-d3" {
interface RenderOptions {
width?: number;
height?: number;
}

type DrawFn = (d3: any, el: any, data: any, size: { width: number; height: number }) => void;

/** Draw in the browser, keeping the chart interactive. */
export function render(fn: DrawFn, data?: any, options?: RenderOptions): void;
/** The HTML `render` would display, for embedding somewhere else. */
export function renderToHtml(fn: DrawFn, data?: any, options?: RenderOptions): string;
/** Draw in the kernel with jsdom and display the resulting SVG. */
export function renderStatic(fn: DrawFn, data?: any, options?: RenderOptions): void;
/** The SVG `renderStatic` would display. */
export function renderStaticToSvg(fn: DrawFn, data?: any, options?: RenderOptions): string;
}
93 changes: 93 additions & 0 deletions modules/kernels/javascript/codedown-d3/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use strict";

// Helpers for drawing D3 charts from a notebook cell.
//
// `render` runs the drawing code in the *browser*, so the chart keeps its interactivity
// (hover, zoom, transitions). The cell's callback is serialized into the HTML output along with
// the d3 bundle and the data, because output HTML is displayed in a sandboxed frame that can't
// reach back into the kernel or fetch a script from a CDN.
//
// `renderStatic` runs the same kind of callback against jsdom here in the kernel and emits a
// plain SVG. It loses interactivity but works in any frontend and survives export.

const fs = require("fs");
const path = require("path");

let counter = 0;
const nextId = () => "codedown-d3-" + process.pid + "-" + ++counter;

// d3's "exports" map only exposes the ESM entry point and a "umd" condition, so the browser
// bundle can't be require.resolve'd by path. Walk up from the entry point to the package root
// instead.
function d3BundleSource() {
let dir = path.dirname(require.resolve("d3"));
while (!fs.existsSync(path.join(dir, "package.json"))) {
const parent = path.dirname(dir);
if (parent === dir) throw new Error("could not locate the d3 package root");
dir = parent;
}
return fs.readFileSync(path.join(dir, "dist", "d3.min.js"), "utf8");
}

function display(mime, content) {
// tslab is always present next to us in the environment's node_modules.
require("tslab").display[mime](content);
}

function renderToHtml(fn, data, options) {
const opts = options || {};
const width = opts.width || 640;
const height = opts.height || 400;
const id = nextId();

// The bundle is inlined per output: each output may be displayed in its own frame, so there's
// no shared window to cache it on. The guard still helps when several outputs share one.
return [
'<div id="' + id + '" style="width:' + width + "px;height:" + height + 'px"></div>',
"<script>",
"(function() {",
" if (typeof window.d3 === 'undefined') {",
d3BundleSource(),
" }",
" var el = document.getElementById(" + JSON.stringify(id) + ");",
" var data = " + JSON.stringify(data === undefined ? null : data) + ";",
" var draw = " + fn.toString() + ";",
" draw(window.d3, el, data, { width: " + width + ", height: " + height + " });",
"})();",
"</script>",
].join("\n");
}

function render(fn, data, options) {
display("html", renderToHtml(fn, data, options));
}

function renderStaticToSvg(fn, data, options) {
const opts = options || {};
const width = opts.width || 640;
const height = opts.height || 400;

const { JSDOM } = require("jsdom");
const d3 = require("d3");

const dom = new JSDOM("<body></body>");
const el = dom.window.document.body;
fn(d3, el, data === undefined ? null : data, { width: width, height: height });

const svg = el.querySelector("svg");
if (svg && !svg.getAttribute("xmlns")) {
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
}
return el.innerHTML;
}

function renderStatic(fn, data, options) {
display("html", renderStaticToSvg(fn, data, options));
}

module.exports = {
render: render,
renderToHtml: renderToHtml,
renderStatic: renderStatic,
renderStaticToSvg: renderStaticToSvg,
};
8 changes: 8 additions & 0 deletions modules/kernels/javascript/codedown-d3/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "codedown-d3",
"version": "1.0.0",
"private": true,
"description": "Render D3 charts from CodeDown notebook cells",
"main": "index.js",
"types": "index.d.ts"
}
122 changes: 122 additions & 0 deletions modules/kernels/javascript/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{ lib
, callPackage
, makeWrapper
, runCommand
, symlinkJoin

, nodejs

, settings
, settingsSchema

# tslab registers two kernels from one binary: JavaScript with --js, TypeScript without.
# Everything else -- the package set, the node_modules, the language server -- is shared.
, variant ? "javascript"
}:

with { inherit (settings) packages; };
with { inherit (settings.interface) attrs extensions; };

with lib;

let
common = callPackage ../common.nix {};

npm = callPackage ./npm { inherit nodejs; };

isTypescript = variant == "typescript";

kernelName = variant;
displayName = if isTypescript then "TypeScript" else "JavaScript";

icon = if isTypescript then ../typescript/typescript-logo-64x64.png else ./javascript-logo-64x64.png;
iconMonochrome = if isTypescript then ../typescript/typescript-monochrome.svg else ./javascript-monochrome.svg;

nodeModules = npm.mkNodeModules { inherit packages; };

inherit (npm) packageOptions;
packageSearch = common.searcher' {
packageMustBeDerivation = false;
packages = packageOptions;
};

tslab = runCommand "tslab-${npm.tslabVersion}" {
version = npm.tslabVersion;
nativeBuildInputs = [makeWrapper];
meta = {
description = "Interactive JavaScript and TypeScript programming with Jupyter";
homepage = "https://github.com/yunabe/tslab";
license = licenses.asl20;
mainProgram = "tslab";
};
} ''
mkdir -p $out/bin
makeWrapper ${nodejs}/bin/node $out/bin/tslab \
--add-flags ${nodeModules}/node_modules/tslab/bin/tslab
'';

# Node's own REPL, with the environment's packages on NODE_PATH so `require("d3")` works
# there too.
nodeRepl = runCommand "codedown-node-repl" { nativeBuildInputs = [makeWrapper]; } ''
mkdir -p $out/bin
makeWrapper ${nodejs}/bin/node $out/bin/codedown-node \
--set NODE_PATH ${nodeModules}/node_modules
'';

repls = {
node = {
display_name = "Node.js " + nodejs.version;
attr = "node";
args = ["${nodeRepl}/bin/codedown-node"];
inherit icon iconMonochrome;
};
};

languageServers =
[]
++ optionals settings.lsp.typescript-language-server.enable [(callPackage ./language_server_typescript/config.nix {
inherit attrs kernelName nodeModules isTypescript icon iconMonochrome;
settings = settings.lsp.typescript-language-server;
})]
;

in

symlinkJoin {
name = kernelName;

paths = [
(callPackage ./kernel.nix {
inherit tslab nodeModules attrs extensions repls;
inherit kernelName displayName isTypescript;
version = nodejs.version;
})

nodejs
]
++ languageServers
;

passthru = {
meta = nodejs.meta // {
baseName = kernelName;
inherit displayName;
version = nodejs.version;
inherit icon iconMonochrome settingsSchema;
hasPackages = packageOptions != {};
};
inherit packageOptions packageSearch;
versions = {
nodejs = nodejs.version;
tslab = npm.tslabVersion;
};
inherit settingsSchema settings;
inherit repls;
inherit nodeModules tslab;
modes = {
inherit attrs extensions;
code_mirror_mode = "javascript";
};
languageServerNames = map (x: x.languageServerName) languageServers;
};
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions modules/kernels/javascript/javascript-monochrome.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://github.com/simple-icons/simple-icons (CC0-1.0)
62 changes: 62 additions & 0 deletions modules/kernels/javascript/kernel.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{ lib
, callPackage

, tslab
, nodeModules

, attrs
, extensions
, version
, repls ? {}

, kernelName
, displayName
, isTypescript
}:

with lib;

let
common = callPackage ../common.nix {};

in

common.makeJupyterKernel (
listToAttrs [{
name = head attrs;
value = {
inherit displayName;
language = head attrs;
argv = [
"${tslab}/bin/tslab"
"kernel"
"--config-path"
"{connection_file}"
]
++ optional (!isTypescript) "--js";
logo32 = if isTypescript then ../typescript/typescript-logo-32x32.png else ./javascript-logo-32x32.png;
logo64 = if isTypescript then ../typescript/typescript-logo-64x64.png else ./javascript-logo-64x64.png;

# The kernel's working directory is the user's notebook directory, so the environment's
# packages have to come in through the environment rather than through cwd.
env = {
NODE_PATH = "${nodeModules}/node_modules";
};

metadata = {
codedown = {
inherit attrs extensions;
language_version = version;

# tslab keeps cell state inside its own executor with no listing command, so there's
# nothing to drive an inspector with yet.
variable_inspector = null;

repls = common.replsToMetadata kernelName repls;

priority = 1;
};
};
};
}]
)
Loading
Loading