Merge pull request #2293 from alixander/trim-wasm-size
d2js: trim wasm size
This commit is contained in:
commit
ac7dd4177a
27 changed files with 106714 additions and 166 deletions
28
ci/peek-wasm-size.sh
Executable file
28
ci/peek-wasm-size.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
|
||||
OUTPUT_FILE="main.wasm"
|
||||
SOURCE_PACKAGE="./d2js"
|
||||
|
||||
echo "Building WASM file..."
|
||||
GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o "$OUTPUT_FILE" "$SOURCE_PACKAGE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful."
|
||||
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
FILE_SIZE_BYTES=$(stat -f%z "$OUTPUT_FILE")
|
||||
FILE_SIZE_MB=$(echo "scale=2; $FILE_SIZE_BYTES / 1024 / 1024" | bc)
|
||||
|
||||
echo "File size of $OUTPUT_FILE: $FILE_SIZE_MB MB"
|
||||
else
|
||||
echo "File $OUTPUT_FILE not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deleting $OUTPUT_FILE..."
|
||||
rm "$OUTPUT_FILE"
|
||||
echo "File deleted."
|
||||
else
|
||||
echo "Build failed."
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -107,6 +107,52 @@ func GetRefRanges(args []js.Value) (interface{}, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func GetELKGraph(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||
}
|
||||
var input CompileRequest
|
||||
if err := json.Unmarshal([]byte(args[0].String()), &input); err != nil {
|
||||
return nil, &WASMError{Message: "invalid JSON input", Code: 400}
|
||||
}
|
||||
|
||||
if input.FS == nil {
|
||||
return nil, &WASMError{Message: "missing 'fs' field in input JSON", Code: 400}
|
||||
}
|
||||
|
||||
if _, ok := input.FS["index"]; !ok {
|
||||
return nil, &WASMError{Message: "missing 'index' file in input fs", Code: 400}
|
||||
}
|
||||
|
||||
fs, err := memfs.New(input.FS)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("invalid fs input: %s", err.Error()), Code: 400}
|
||||
}
|
||||
|
||||
g, _, err := d2compiler.Compile("", strings.NewReader(input.FS["index"]), &d2compiler.CompileOptions{
|
||||
UTF16Pos: true,
|
||||
FS: fs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: fmt.Sprintf("text ruler cannot be initialized: %s", err.Error()), Code: 500}
|
||||
}
|
||||
err = g.SetDimensions(nil, ruler, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
elk, err := d2elklayout.ConvertGraph(context.Background(), g, nil)
|
||||
if err != nil {
|
||||
return nil, &WASMError{Message: err.Error(), Code: 400}
|
||||
}
|
||||
return elk, nil
|
||||
}
|
||||
|
||||
func Compile(args []js.Value) (interface{}, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ func main() {
|
|||
api.Register("getParentID", d2wasm.GetParentID)
|
||||
api.Register("getObjOrder", d2wasm.GetObjOrder)
|
||||
api.Register("getRefRanges", d2wasm.GetRefRanges)
|
||||
api.Register("getELKGraph", d2wasm.GetELKGraph)
|
||||
api.Register("compile", d2wasm.Compile)
|
||||
api.Register("render", d2wasm.Render)
|
||||
api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ async function buildAndCopy(buildType) {
|
|||
resolve(ROOT_DIR, "wasm/wasm_exec.js"),
|
||||
join(config.outdir, "wasm_exec.js")
|
||||
);
|
||||
await copyFile(resolve(ROOT_DIR, "src/elk.js"), join(config.outdir, "elk.js"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,16 @@
|
|||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.layout-toggle {
|
||||
.options-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.layout-toggle,
|
||||
.sketch-toggle {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
|
@ -33,7 +42,8 @@
|
|||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.radio-label {
|
||||
.radio-label,
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
|
@ -66,16 +76,24 @@
|
|||
<body>
|
||||
<div class="controls">
|
||||
<textarea id="input">x -> y</textarea>
|
||||
<div class="layout-toggle">
|
||||
<span>Layout:</span>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="dagre" checked />
|
||||
Dagre
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="elk" />
|
||||
ELK
|
||||
<div class="options-group">
|
||||
<div class="layout-toggle">
|
||||
<span>Layout:</span>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="dagre" checked />
|
||||
Dagre
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="layout" value="elk" />
|
||||
ELK
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sketch-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="sketch" />
|
||||
Sketch mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -88,9 +106,10 @@
|
|||
window.compile = async () => {
|
||||
const input = document.getElementById("input").value;
|
||||
const layout = document.querySelector('input[name="layout"]:checked').value;
|
||||
const sketch = document.getElementById("sketch").checked;
|
||||
try {
|
||||
const result = await d2.compile(input, { layout });
|
||||
const svg = await d2.render(result.diagram);
|
||||
const result = await d2.compile(input, { layout, sketch });
|
||||
const svg = await d2.render(result.diagram, { sketch });
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@terrastruct/d2",
|
||||
"author": "Terrastruct, Inc.",
|
||||
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/terrastruct/d2.git",
|
||||
|
|
|
|||
105805
d2js/js/src/elk.js
Normal file
105805
d2js/js/src/elk.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -42,6 +42,7 @@ export class D2 {
|
|||
async init() {
|
||||
this.worker = await createWorker();
|
||||
|
||||
const elkContent = await loadFile("./elk.js");
|
||||
const wasmExecContent = await loadFile("./wasm_exec.js");
|
||||
const wasmBinary = await loadFile("./d2.wasm");
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ export class D2 {
|
|||
data: {
|
||||
wasm: wasmBinary,
|
||||
wasmExecContent: isNode ? wasmExecContent.toString() : null,
|
||||
elkContent: isNode ? elkContent.toString() : null,
|
||||
wasmExecUrl: isNode
|
||||
? null
|
||||
: URL.createObjectURL(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { wasmBinary, wasmExecJs } from "./wasm-loader.browser.js";
|
||||
import workerScript from "./worker.js" with { type: "text" };
|
||||
import elkScript from "./elk.js" with { type: "text" };
|
||||
|
||||
// For the browser version, we build the wasm files into a file (wasm-loader.browser.js)
|
||||
// and loading a file just reads the text, so there's no external dependency calls
|
||||
export async function loadFile(path) {
|
||||
if (path === "./d2.wasm") {
|
||||
return wasmBinary.buffer;
|
||||
|
|
@ -8,11 +11,11 @@ export async function loadFile(path) {
|
|||
if (path === "./wasm_exec.js") {
|
||||
return new TextEncoder().encode(wasmExecJs).buffer;
|
||||
}
|
||||
throw new Error(`Unexpected file request: ${path}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createWorker() {
|
||||
let blob = new Blob([wasmExecJs, workerScript], {
|
||||
let blob = new Blob([wasmExecJs, elkScript, workerScript], {
|
||||
type: "text/javascript;charset=utf-8",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
|
@ -14,6 +15,7 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
|||
eval(data.wasmExecContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
|
|
@ -22,6 +24,19 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
|||
|
||||
case "compile":
|
||||
try {
|
||||
// We use Go to get the intermediate ELK graph
|
||||
// Then natively run elk layout
|
||||
// This is due to elk.layout being an async function, which a
|
||||
// single-threaded WASM call cannot complete without giving control back
|
||||
// So we compute it, store it here, then during elk layout, instead
|
||||
// of computing again, we use this variable (and unset it for next call)
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
|
|
|
|||
|
|
@ -1 +1,72 @@
|
|||
// Replaced at build time
|
||||
import { parentPort } from "node:worker_threads";
|
||||
|
||||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
||||
const handleMessage = async (e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
switch (type) {
|
||||
case "init":
|
||||
try {
|
||||
if (isNode) {
|
||||
eval(data.wasmExecContent);
|
||||
eval(data.elkContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "compile":
|
||||
try {
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: response.data });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
|
||||
case "render":
|
||||
try {
|
||||
const result = await d2.render(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
currentPort.postMessage({ type: "result", data: atob(response.data) });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (isNode) {
|
||||
port.on("message", handleMessage);
|
||||
} else {
|
||||
port.onmessage = (e) => handleMessage(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
async function initWasmNode(wasmBinary) {
|
||||
const go = new Go();
|
||||
const result = await WebAssembly.instantiate(wasmBinary, go.importObject);
|
||||
go.run(result.instance);
|
||||
return global.d2;
|
||||
}
|
||||
|
||||
setupMessageHandler(true, parentPort, initWasmNode);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { parentPort } from "node:worker_threads";
|
|||
|
||||
let currentPort;
|
||||
let d2;
|
||||
let elk;
|
||||
|
||||
export function setupMessageHandler(isNode, port, initWasm) {
|
||||
currentPort = port;
|
||||
|
|
@ -14,8 +15,10 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
|||
try {
|
||||
if (isNode) {
|
||||
eval(data.wasmExecContent);
|
||||
eval(data.elkContent);
|
||||
}
|
||||
d2 = await initWasm(data.wasm);
|
||||
elk = new ELK();
|
||||
currentPort.postMessage({ type: "ready" });
|
||||
} catch (err) {
|
||||
currentPort.postMessage({ type: "error", error: err.message });
|
||||
|
|
@ -24,6 +27,12 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
|||
|
||||
case "compile":
|
||||
try {
|
||||
if (data.options.layout === "elk") {
|
||||
const elkGraph = await d2.getELKGraph(JSON.stringify(data));
|
||||
const elkGraph2 = JSON.parse(elkGraph).data;
|
||||
const layout = await elk.layout(elkGraph2);
|
||||
globalThis.elkResult = layout;
|
||||
}
|
||||
const result = await d2.compile(JSON.stringify(data));
|
||||
const response = JSON.parse(result);
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ describe("D2 Unit Tests", () => {
|
|||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("elk layout works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y", { layout: "elk" });
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
|
|
@ -18,6 +25,25 @@ describe("D2 Unit Tests", () => {
|
|||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("sketch render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y", { sketch: true });
|
||||
const svg = await d2.render(result.diagram, { sketch: true });
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
expect(svg).toContain("sketch-overlay");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("latex works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x: |latex \\frac{f(x+h)-f(x)}{h} |");
|
||||
const svg = await d2.render(result.diagram);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("handles syntax errors correctly", async () => {
|
||||
const d2 = new D2();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import (
|
|||
|
||||
"log/slog"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -20,6 +18,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
|
|
@ -80,11 +79,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
defer xdefer.Errorf(&err, "failed to dagre layout")
|
||||
|
||||
debugJS := false
|
||||
vm := goja.New()
|
||||
if _, err := vm.RunString(dagreJS); err != nil {
|
||||
runner := jsrunner.NewJSRunner()
|
||||
if _, err := runner.RunString(dagreJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +135,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
configJS := setGraphAttrs(rootAttrs)
|
||||
if _, err := vm.RunString(configJS); err != nil {
|
||||
if _, err := runner.RunString(configJS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -183,11 +182,11 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
log.Debug(ctx, "script", slog.Any("all", setupJS+configJS+loadScript))
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(loadScript); err != nil {
|
||||
if _, err := runner.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
|
||||
if _, err := runner.RunString(`dagre.layout(g)`); err != nil {
|
||||
if debugJS {
|
||||
log.Warn(ctx, "layout error", slog.Any("err", err))
|
||||
}
|
||||
|
|
@ -195,7 +194,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
for i := range g.Objects {
|
||||
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
|
||||
val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.node(g.nodes()[%d]))", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -216,7 +215,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
for i, edge := range g.Edges {
|
||||
val, err := vm.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
||||
val, err := runner.RunString(fmt.Sprintf("JSON.stringify(g.edge(g.edges()[%d]))", i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
10
d2layouts/d2elklayout/elk.go
Normal file
10
d2layouts/d2elklayout/elk.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//go:build !js && !wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed elk.js
|
||||
var elkJS string
|
||||
33
d2layouts/d2elklayout/elk.js
vendored
33
d2layouts/d2elklayout/elk.js
vendored
|
|
@ -5,15 +5,7 @@
|
|||
define([], f);
|
||||
} else {
|
||||
var g;
|
||||
if (typeof window !== "undefined") {
|
||||
g = window;
|
||||
} else if (typeof global !== "undefined") {
|
||||
g = global;
|
||||
} else if (typeof self !== "undefined") {
|
||||
g = self;
|
||||
} else {
|
||||
g = this;
|
||||
}
|
||||
g = this;
|
||||
g.ELK = f();
|
||||
}
|
||||
})(function () {
|
||||
|
|
@ -337,9 +329,6 @@
|
|||
|
||||
// -------------- FAKE ELEMENTS GWT ASSUMES EXIST --------------
|
||||
var $wnd = { Error: {} };
|
||||
if (typeof window !== "undefined") $wnd = window;
|
||||
else if (typeof global !== "undefined") $wnd = global; // nodejs
|
||||
else if (typeof self !== "undefined") $wnd = self; // web worker
|
||||
|
||||
var $moduleName, $moduleBase;
|
||||
|
||||
|
|
@ -59795,13 +59784,8 @@
|
|||
}, 0);
|
||||
};
|
||||
}
|
||||
if (typeof document === uke && typeof self !== uke) {
|
||||
var i = new h(self);
|
||||
self.onmessage = i.saveDispatch;
|
||||
} else if (typeof module !== uke && module.exports) {
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
module.exports = { default: j, Worker: j };
|
||||
}
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
module.exports = { default: j, Worker: j };
|
||||
}
|
||||
function aae(a) {
|
||||
if (a.N) return;
|
||||
|
|
@ -105682,16 +105666,7 @@
|
|||
// -------------- RUN GWT INITIALIZATION CODE --------------
|
||||
gwtOnLoad(null, "elk", null);
|
||||
}.call(this));
|
||||
}.call(
|
||||
this,
|
||||
typeof global !== "undefined"
|
||||
? global
|
||||
: typeof self !== "undefined"
|
||||
? self
|
||||
: typeof window !== "undefined"
|
||||
? window
|
||||
: {}
|
||||
));
|
||||
}.call(this, {}));
|
||||
},
|
||||
{},
|
||||
],
|
||||
|
|
|
|||
6
d2layouts/d2elklayout/elk_js.go
Normal file
6
d2layouts/d2elklayout/elk_js.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
// No embed, since this is already bundled in the js worker
|
||||
var elkJS string
|
||||
|
|
@ -8,15 +8,12 @@ import (
|
|||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -24,13 +21,11 @@ import (
|
|||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
)
|
||||
|
||||
//go:embed elk.js
|
||||
var elkJS string
|
||||
|
||||
//go:embed setup.js
|
||||
var setupJS string
|
||||
|
||||
|
|
@ -162,18 +157,20 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
defer xdefer.Errorf(&err, "failed to ELK layout")
|
||||
|
||||
vm := goja.New()
|
||||
runner := jsrunner.NewJSRunner()
|
||||
|
||||
console := vm.NewObject()
|
||||
if err := vm.Set("console", console); err != nil {
|
||||
return err
|
||||
}
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
console := runner.NewObject()
|
||||
if err := runner.Set("console", console); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(elkJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return err
|
||||
if _, err := runner.RunString(elkJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
elkGraph := &ELKGraph{
|
||||
|
|
@ -443,41 +440,30 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
return err
|
||||
}
|
||||
|
||||
loadScript := fmt.Sprintf(`var graph = %s`, raw)
|
||||
var val jsrunner.JSValue
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
loadScript := fmt.Sprintf(`var graph = %s`, raw)
|
||||
|
||||
if _, err := vm.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runner.RunString(loadScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := vm.RunString(`elk.layout(graph)
|
||||
val, err = runner.RunString(`elk.layout(graph)
|
||||
.then(s => s)
|
||||
.catch(err => err.message)
|
||||
`)
|
||||
|
||||
} else {
|
||||
val, err = runner.MustGet("elkResult")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := val.Export()
|
||||
result, err := runner.WaitPromise(ctx, val)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ELK layout error: %v", err)
|
||||
}
|
||||
|
||||
promise := p.(*goja.Promise)
|
||||
|
||||
for promise.State() == goja.PromiseStatePending {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if promise.State() == goja.PromiseStateRejected {
|
||||
return errors.New("ELK: something went wrong")
|
||||
}
|
||||
|
||||
result := promise.Result().Export()
|
||||
|
||||
var jsonOut map[string]interface{}
|
||||
switch out := result.(type) {
|
||||
case string:
|
||||
|
|
|
|||
286
d2layouts/d2elklayout/wasm.go
Normal file
286
d2layouts/d2elklayout/wasm.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package d2elklayout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
)
|
||||
|
||||
// This is mostly copy paste from Layout until elk.layout step
|
||||
func ConvertGraph(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (_ *ELKGraph, err error) {
|
||||
if opts == nil {
|
||||
opts = &DefaultOpts
|
||||
}
|
||||
defer xdefer.Errorf(&err, "failed to ELK layout")
|
||||
|
||||
elkGraph := &ELKGraph{
|
||||
ID: "",
|
||||
LayoutOptions: &elkOpts{
|
||||
Thoroughness: 8,
|
||||
EdgeEdgeBetweenLayersSpacing: 50,
|
||||
EdgeNode: edge_node_spacing,
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
FixedAlignment: "BALANCED",
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
|
||||
NodeSizeConstraints: "MINIMUM_SIZE",
|
||||
ContentAlignment: "H_CENTER V_CENTER",
|
||||
ConfigurableOpts: ConfigurableOpts{
|
||||
Algorithm: opts.Algorithm,
|
||||
NodeSpacing: opts.NodeSpacing,
|
||||
EdgeNodeSpacing: opts.EdgeNodeSpacing,
|
||||
SelfLoopSpacing: opts.SelfLoopSpacing,
|
||||
},
|
||||
},
|
||||
}
|
||||
if elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
|
||||
// +5 for a tiny bit of padding
|
||||
elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(g.Root, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
|
||||
}
|
||||
switch g.Root.Direction.Value {
|
||||
case "down":
|
||||
elkGraph.LayoutOptions.Direction = Down
|
||||
case "up":
|
||||
elkGraph.LayoutOptions.Direction = Up
|
||||
case "right":
|
||||
elkGraph.LayoutOptions.Direction = Right
|
||||
case "left":
|
||||
elkGraph.LayoutOptions.Direction = Left
|
||||
default:
|
||||
elkGraph.LayoutOptions.Direction = Down
|
||||
}
|
||||
|
||||
// set label and icon positions for ELK
|
||||
for _, obj := range g.Objects {
|
||||
positionLabelsIcons(obj)
|
||||
}
|
||||
|
||||
adjustments := make(map[*d2graph.Object]geo.Spacing)
|
||||
elkNodes := make(map[*d2graph.Object]*ELKNode)
|
||||
elkEdges := make(map[*d2graph.Edge]*ELKEdge)
|
||||
|
||||
// BFS
|
||||
var walk func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object))
|
||||
walk = func(obj, parent *d2graph.Object, fn func(*d2graph.Object, *d2graph.Object)) {
|
||||
if obj.Parent != nil {
|
||||
fn(obj, parent)
|
||||
}
|
||||
for _, ch := range obj.ChildrenArray {
|
||||
walk(ch, obj, fn)
|
||||
}
|
||||
}
|
||||
|
||||
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
|
||||
incoming := 0.
|
||||
outgoing := 0.
|
||||
for _, e := range g.Edges {
|
||||
if e.Src == obj {
|
||||
outgoing++
|
||||
}
|
||||
if e.Dst == obj {
|
||||
incoming++
|
||||
}
|
||||
}
|
||||
if incoming >= 2 || outgoing >= 2 {
|
||||
switch g.Root.Direction.Value {
|
||||
case "right", "left":
|
||||
if obj.Attributes.HeightAttr == nil {
|
||||
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
|
||||
}
|
||||
default:
|
||||
if obj.Attributes.WidthAttr == nil {
|
||||
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.HasLabel() && obj.HasIcon() {
|
||||
// this gives shapes extra height for their label if they also have an icon
|
||||
obj.Height += float64(obj.LabelDimensions.Height + label.PADDING)
|
||||
}
|
||||
|
||||
margin, _ := obj.SpacingOpt(label.PADDING, label.PADDING, false)
|
||||
width := margin.Left + obj.Width + margin.Right
|
||||
height := margin.Top + obj.Height + margin.Bottom
|
||||
adjustments[obj] = margin
|
||||
|
||||
n := &ELKNode{
|
||||
ID: obj.AbsID(),
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
n.LayoutOptions = &elkOpts{
|
||||
ForceNodeModelOrder: true,
|
||||
Thoroughness: 8,
|
||||
EdgeEdgeBetweenLayersSpacing: 50,
|
||||
HierarchyHandling: "INCLUDE_CHILDREN",
|
||||
FixedAlignment: "BALANCED",
|
||||
EdgeNode: edge_node_spacing,
|
||||
ConsiderModelOrder: "NODES_AND_EDGES",
|
||||
CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
|
||||
NodeSizeConstraints: "MINIMUM_SIZE",
|
||||
ContentAlignment: "H_CENTER V_CENTER",
|
||||
ConfigurableOpts: ConfigurableOpts{
|
||||
NodeSpacing: opts.NodeSpacing,
|
||||
EdgeNodeSpacing: opts.EdgeNodeSpacing,
|
||||
SelfLoopSpacing: opts.SelfLoopSpacing,
|
||||
Padding: opts.Padding,
|
||||
},
|
||||
}
|
||||
if n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
|
||||
n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(obj, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
|
||||
}
|
||||
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Down, Up:
|
||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
|
||||
case Right, Left:
|
||||
n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
|
||||
}
|
||||
} else {
|
||||
n.LayoutOptions = &elkOpts{
|
||||
SelfLoopDistribution: "EQUALLY",
|
||||
}
|
||||
}
|
||||
|
||||
if obj.IsContainer() {
|
||||
padding := parsePadding(opts.Padding)
|
||||
padding = adjustPadding(obj, width, height, padding)
|
||||
n.LayoutOptions.Padding = padding.String()
|
||||
}
|
||||
|
||||
if obj.HasLabel() {
|
||||
n.Labels = append(n.Labels, &ELKLabel{
|
||||
Text: obj.Label.Value,
|
||||
Width: float64(obj.LabelDimensions.Width),
|
||||
Height: float64(obj.LabelDimensions.Height),
|
||||
})
|
||||
}
|
||||
|
||||
if parent == g.Root {
|
||||
elkGraph.Children = append(elkGraph.Children, n)
|
||||
} else {
|
||||
elkNodes[parent].Children = append(elkNodes[parent].Children, n)
|
||||
}
|
||||
|
||||
if obj.SQLTable != nil {
|
||||
n.LayoutOptions.PortConstraints = "FIXED_POS"
|
||||
columns := obj.SQLTable.Columns
|
||||
colHeight := n.Height / float64(len(columns)+1)
|
||||
n.Ports = make([]*ELKPort, 0, len(columns)*2)
|
||||
var srcSide, dstSide PortSide
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Left:
|
||||
srcSide, dstSide = West, East
|
||||
default:
|
||||
srcSide, dstSide = East, West
|
||||
}
|
||||
for i, col := range columns {
|
||||
n.Ports = append(n.Ports, &ELKPort{
|
||||
ID: srcPortID(obj, col.Name.Label),
|
||||
Y: float64(i+1)*colHeight + colHeight/2,
|
||||
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||
})
|
||||
n.Ports = append(n.Ports, &ELKPort{
|
||||
ID: dstPortID(obj, col.Name.Label),
|
||||
Y: float64(i+1)*colHeight + colHeight/2,
|
||||
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
elkNodes[obj] = n
|
||||
})
|
||||
|
||||
var srcSide, dstSide PortSide
|
||||
switch elkGraph.LayoutOptions.Direction {
|
||||
case Up:
|
||||
srcSide, dstSide = North, South
|
||||
default:
|
||||
srcSide, dstSide = South, North
|
||||
}
|
||||
|
||||
ports := map[struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}][]*ELKPort{}
|
||||
|
||||
for ei, edge := range g.Edges {
|
||||
var src, dst string
|
||||
|
||||
switch {
|
||||
case edge.SrcTableColumnIndex != nil:
|
||||
src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
|
||||
case edge.Src.SQLTable != nil:
|
||||
p := &ELKPort{
|
||||
ID: fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
|
||||
LayoutOptions: &elkOpts{PortSide: srcSide},
|
||||
}
|
||||
src = p.ID
|
||||
elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
|
||||
k := struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}{edge.Src, srcSide}
|
||||
ports[k] = append(ports[k], p)
|
||||
default:
|
||||
src = edge.Src.AbsID()
|
||||
}
|
||||
|
||||
switch {
|
||||
case edge.DstTableColumnIndex != nil:
|
||||
dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
|
||||
case edge.Dst.SQLTable != nil:
|
||||
p := &ELKPort{
|
||||
ID: fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
|
||||
LayoutOptions: &elkOpts{PortSide: dstSide},
|
||||
}
|
||||
dst = p.ID
|
||||
elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
|
||||
k := struct {
|
||||
obj *d2graph.Object
|
||||
side PortSide
|
||||
}{edge.Dst, dstSide}
|
||||
ports[k] = append(ports[k], p)
|
||||
default:
|
||||
dst = edge.Dst.AbsID()
|
||||
}
|
||||
|
||||
e := &ELKEdge{
|
||||
ID: edge.AbsID(),
|
||||
Sources: []string{src},
|
||||
Targets: []string{dst},
|
||||
}
|
||||
if edge.Label.Value != "" {
|
||||
e.Labels = append(e.Labels, &ELKLabel{
|
||||
Text: edge.Label.Value,
|
||||
Width: float64(edge.LabelDimensions.Width),
|
||||
Height: float64(edge.LabelDimensions.Height),
|
||||
LayoutOptions: &elkOpts{
|
||||
InlineEdgeLabels: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
elkGraph.Edges = append(elkGraph.Edges, e)
|
||||
elkEdges[edge] = e
|
||||
}
|
||||
|
||||
for k, ports := range ports {
|
||||
width := elkNodes[k.obj].Width
|
||||
spacing := width / float64(len(ports)+1)
|
||||
for i, p := range ports {
|
||||
p.X = float64(i+1) * spacing
|
||||
}
|
||||
}
|
||||
return elkGraph, nil
|
||||
}
|
||||
|
|
@ -7,8 +7,7 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/util-go/xdefer"
|
||||
)
|
||||
|
||||
|
|
@ -29,21 +28,24 @@ var svgRe = regexp.MustCompile(`<svg[^>]+width="([0-9\.]+)ex" height="([0-9\.]+)
|
|||
|
||||
func Render(s string) (_ string, err error) {
|
||||
defer xdefer.Errorf(&err, "latex failed to parse")
|
||||
vm := goja.New()
|
||||
runner := jsrunner.NewJSRunner()
|
||||
|
||||
if _, err := vm.RunString(polyfillsJS); err != nil {
|
||||
if _, err := runner.RunString(polyfillsJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(mathjaxJS); err != nil {
|
||||
if _, err := runner.RunString(mathjaxJS); err != nil {
|
||||
// Known issue that a harmless error occurs in JS: https://github.com/mathjax/MathJax/issues/3289
|
||||
if runner.Engine() == jsrunner.Goja {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
val, err := vm.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
||||
val, err := runner.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
||||
em: %d,
|
||||
ex: %d,
|
||||
}))`, s, pxPerEx*2, pxPerEx))
|
||||
|
|
|
|||
|
|
@ -4,3 +4,8 @@ const html = MathJax._.mathjax.mathjax.document('', {
|
|||
InputJax: new MathJax._.input.tex_ts.TeX({ packages: ['base', 'mathtools', 'ams', 'amscd', 'braket', 'cancel', 'cases', 'color', 'gensymb', 'mhchem', 'physics'] }),
|
||||
OutputJax: new MathJax._.output.svg_ts.SVG(),
|
||||
});
|
||||
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.adaptor = adaptor;
|
||||
globalThis.html = html;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ const root = {
|
|||
};
|
||||
const rc = rough.svg(root, { seed: 1 });
|
||||
let node;
|
||||
|
||||
if (typeof globalThis !== "undefined") {
|
||||
globalThis.rc = rc;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import (
|
|||
|
||||
_ "embed"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
|
|
@ -29,8 +28,6 @@ var setupJS string
|
|||
//go:embed streaks.txt
|
||||
var streaks string
|
||||
|
||||
type Runner goja.Runtime
|
||||
|
||||
var baseRoughProps = `fillWeight: 2.0,
|
||||
hachureGap: 16,
|
||||
fillStyle: "solid",
|
||||
|
|
@ -44,21 +41,14 @@ const (
|
|||
FG_COLOR = color.N1
|
||||
)
|
||||
|
||||
func (r *Runner) run(js string) (goja.Value, error) {
|
||||
vm := (*goja.Runtime)(r)
|
||||
return vm.RunString(js)
|
||||
}
|
||||
|
||||
func InitSketchVM() (*Runner, error) {
|
||||
vm := goja.New()
|
||||
if _, err := vm.RunString(roughJS); err != nil {
|
||||
return nil, err
|
||||
func LoadJS(runner jsrunner.JSRunner) error {
|
||||
if _, err := runner.RunString(roughJS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := vm.RunString(setupJS); err != nil {
|
||||
return nil, err
|
||||
if _, err := runner.RunString(setupJS); err != nil {
|
||||
return err
|
||||
}
|
||||
r := Runner(*vm)
|
||||
return &r, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
||||
|
|
@ -83,7 +73,7 @@ func defineFillPattern(buf *bytes.Buffer, source string, luminanceCategory, fill
|
|||
}
|
||||
}
|
||||
|
||||
func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Rect(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -119,7 +109,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func DoubleRect(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -179,7 +169,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Oval(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -218,7 +208,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func DoubleOval(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||
fill: "#000",
|
||||
stroke: "#000",
|
||||
|
|
@ -281,7 +271,7 @@ func DoubleOval(r *Runner, shape d2target.Shape) (string, error) {
|
|||
}
|
||||
|
||||
// TODO need to personalize this per shape like we do in Terrastruct app
|
||||
func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
||||
func Paths(r jsrunner.JSRunner, shape d2target.Shape, paths []string) (string, error) {
|
||||
output := ""
|
||||
for _, path := range paths {
|
||||
js := fmt.Sprintf(`node = rc.path("%s", {
|
||||
|
|
@ -320,7 +310,7 @@ func Paths(r *Runner, shape d2target.Shape, paths []string) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Connection(r *Runner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
func Connection(r jsrunner.JSRunner, connection d2target.Connection, path, attrs string) (string, error) {
|
||||
animatedClass := ""
|
||||
if connection.Animated {
|
||||
animatedClass = " animated-connection"
|
||||
|
|
@ -388,7 +378,7 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
|
|||
}
|
||||
|
||||
// TODO cleanup
|
||||
func Table(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
|
|
@ -530,7 +520,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
func Class(r *Runner, shape d2target.Shape) (string, error) {
|
||||
func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||
output := ""
|
||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||
fill: "#000",
|
||||
|
|
@ -681,8 +671,8 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
|||
return output
|
||||
}
|
||||
|
||||
func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
||||
if _, err := r.run(js); err != nil {
|
||||
func computeRoughPathData(r jsrunner.JSRunner, js string) ([]string, error) {
|
||||
if _, err := r.RunString(js); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roughPaths, err := extractRoughPaths(r)
|
||||
|
|
@ -692,8 +682,8 @@ func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
|||
return extractPathData(roughPaths)
|
||||
}
|
||||
|
||||
func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
|
||||
if _, err := r.run(js); err != nil {
|
||||
func computeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) {
|
||||
if _, err := r.RunString(js); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractRoughPaths(r)
|
||||
|
|
@ -722,8 +712,8 @@ func (rp roughPath) StyleCSS() string {
|
|||
return style
|
||||
}
|
||||
|
||||
func extractRoughPaths(r *Runner) ([]roughPath, error) {
|
||||
val, err := r.run("JSON.stringify(node.children, null, ' ')")
|
||||
func extractRoughPaths(r jsrunner.JSRunner) ([]roughPath, error) {
|
||||
val, err := r.RunString("JSON.stringify(node.children, null, ' ')")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -756,7 +746,7 @@ func extractPathData(roughPaths []roughPath) ([]string, error) {
|
|||
return paths, nil
|
||||
}
|
||||
|
||||
func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
|
||||
func ArrowheadJS(r jsrunner.JSRunner, arrowhead d2target.Arrowhead, stroke string, strokeWidth int) (arrowJS, extraJS string) {
|
||||
// Note: selected each seed that looks the good for consistent renders
|
||||
switch arrowhead {
|
||||
case d2target.ArrowArrowhead:
|
||||
|
|
@ -854,7 +844,7 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
|||
return
|
||||
}
|
||||
|
||||
func Arrowheads(r *Runner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
|
||||
func Arrowheads(r jsrunner.JSRunner, connection d2target.Connection, srcAdj, dstAdj *geo.Point) (string, error) {
|
||||
arrowPaths := []string{}
|
||||
|
||||
if connection.SrcArrow != d2target.NoArrowhead {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/color"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
"oss.terrastruct.com/d2/lib/svg"
|
||||
|
|
@ -496,7 +497,7 @@ func makeLabelMask(labelTL *geo.Point, width, height int, opacity float64) strin
|
|||
)
|
||||
}
|
||||
|
||||
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, sketchRunner *d2sketch.Runner, inlineTheme *d2themes.Theme) (labelMask string, _ error) {
|
||||
func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Connection, markers map[string]struct{}, idToShape map[string]d2target.Shape, jsRunner jsrunner.JSRunner, inlineTheme *d2themes.Theme) (labelMask string, _ error) {
|
||||
opacityStyle := ""
|
||||
if connection.Opacity != 1.0 {
|
||||
opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
|
||||
|
|
@ -552,15 +553,15 @@ func drawConnection(writer io.Writer, labelMaskID string, connection d2target.Co
|
|||
path := pathData(connection, srcAdj, dstAdj)
|
||||
mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID)
|
||||
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Connection(sketchRunner, connection, path, mask)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Connection(jsRunner, connection, path, mask)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprint(writer, out)
|
||||
|
||||
// render sketch arrowheads separately
|
||||
arrowPaths, err := d2sketch.Arrowheads(sketchRunner, connection, srcAdj, dstAdj)
|
||||
arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -957,7 +958,7 @@ func render3DHexagon(targetShape d2target.Shape, inlineTheme *d2themes.Theme) st
|
|||
return borderMask + mainShapeRendered + renderedSides + renderedBorder
|
||||
}
|
||||
|
||||
func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, sketchRunner *d2sketch.Runner, inlineTheme *d2themes.Theme) (labelMask string, err error) {
|
||||
func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape d2target.Shape, jsRunner jsrunner.JSRunner, inlineTheme *d2themes.Theme) (labelMask string, err error) {
|
||||
closingTag := "</g>"
|
||||
if targetShape.Link != "" {
|
||||
|
||||
|
|
@ -1021,8 +1022,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
|
||||
switch targetShape.Type {
|
||||
case d2target.ShapeClass:
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Class(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Class(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1035,8 +1036,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
fmt.Fprint(writer, closingTag)
|
||||
return labelMask, nil
|
||||
case d2target.ShapeSQLTable:
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Table(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Table(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1053,8 +1054,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
if targetShape.Multiple {
|
||||
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
||||
}
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.DoubleOval(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.DoubleOval(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1066,8 +1067,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
if targetShape.Multiple {
|
||||
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
||||
}
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Oval(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Oval(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1111,8 +1112,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
el.Rx = borderRadius
|
||||
fmt.Fprint(writer, el.Render())
|
||||
}
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Rect(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Rect(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1155,8 +1156,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
el.Rx = borderRadius
|
||||
fmt.Fprint(writer, el.Render())
|
||||
}
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.DoubleRect(sketchRunner, targetShape)
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.DoubleRect(jsRunner, targetShape)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1203,8 +1204,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
}
|
||||
}
|
||||
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1235,8 +1236,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
|||
}
|
||||
}
|
||||
|
||||
if sketchRunner != nil {
|
||||
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
|
||||
if jsRunner != nil {
|
||||
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1846,7 +1847,7 @@ func appendOnTrigger(buf *bytes.Buffer, source string, triggers []string, newCon
|
|||
var DEFAULT_DARK_THEME *int64 = nil // no theme selected
|
||||
|
||||
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||
var sketchRunner *d2sketch.Runner
|
||||
var jsRunner jsrunner.JSRunner
|
||||
pad := DEFAULT_PADDING
|
||||
themeID := d2themescatalog.NeutralDefault.ID
|
||||
darkThemeID := DEFAULT_DARK_THEME
|
||||
|
|
@ -1856,8 +1857,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
|||
pad = int(*opts.Pad)
|
||||
}
|
||||
if opts.Sketch != nil && *opts.Sketch {
|
||||
var err error
|
||||
sketchRunner, err = d2sketch.InitSketchVM()
|
||||
jsRunner = jsrunner.NewJSRunner()
|
||||
err := d2sketch.LoadJS(jsRunner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1941,7 +1942,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
|||
}
|
||||
for _, obj := range allObjects {
|
||||
if c, is := obj.(d2target.Connection); is {
|
||||
labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, sketchRunner, inlineTheme)
|
||||
labelMask, err := drawConnection(buf, isolatedDiagramHash, c, markers, idToShape, jsRunner, inlineTheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1949,7 +1950,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
|||
labelMasks = append(labelMasks, labelMask)
|
||||
}
|
||||
} else if s, is := obj.(d2target.Shape); is {
|
||||
labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, sketchRunner, inlineTheme)
|
||||
labelMask, err := drawShape(buf, appendixItemBuf, diagramHash, s, jsRunner, inlineTheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if labelMask != "" {
|
||||
|
|
@ -2003,7 +2004,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
|||
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
|
||||
}
|
||||
|
||||
if sketchRunner != nil {
|
||||
if jsRunner != nil {
|
||||
d2sketch.DefineFillPatterns(upperBuf)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
113
lib/jsrunner/goja.go
Normal file
113
lib/jsrunner/goja.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//go:build !js && !wasm
|
||||
|
||||
package jsrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type gojaRunner struct {
|
||||
vm *goja.Runtime
|
||||
}
|
||||
|
||||
type gojaValue struct {
|
||||
val goja.Value
|
||||
vm *goja.Runtime
|
||||
}
|
||||
|
||||
func NewJSRunner() JSRunner {
|
||||
return &gojaRunner{vm: goja.New()}
|
||||
}
|
||||
|
||||
// UNUSED
|
||||
func (g *gojaRunner) MustGet(key string) (JSValue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *gojaRunner) Engine() Engine {
|
||||
return Goja
|
||||
}
|
||||
|
||||
func (g *gojaRunner) RunString(code string) (JSValue, error) {
|
||||
val, err := g.vm.RunString(code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &gojaValue{val: val, vm: g.vm}, nil
|
||||
}
|
||||
|
||||
func (v *gojaValue) String() string {
|
||||
return v.val.String()
|
||||
}
|
||||
|
||||
func (v *gojaValue) Export() interface{} {
|
||||
return v.val.Export()
|
||||
}
|
||||
|
||||
func (g *gojaRunner) NewObject() JSObject {
|
||||
return &gojaValue{val: g.vm.NewObject(), vm: g.vm}
|
||||
}
|
||||
|
||||
func (g *gojaRunner) Set(name string, value interface{}) error {
|
||||
if name == "console" {
|
||||
console, err := g.createConsole()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.vm.Set(name, console)
|
||||
}
|
||||
return g.vm.Set(name, value)
|
||||
}
|
||||
|
||||
func (g *gojaRunner) WaitPromise(ctx context.Context, val JSValue) (interface{}, error) {
|
||||
gVal := val.(*gojaValue)
|
||||
p := gVal.val.Export()
|
||||
promise := p.(*goja.Promise)
|
||||
|
||||
for promise.State() == goja.PromiseStatePending {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if promise.State() == goja.PromiseStateRejected {
|
||||
return nil, errors.New("Promise rejected")
|
||||
}
|
||||
|
||||
return promise.Result().Export(), nil
|
||||
}
|
||||
|
||||
func (g *gojaRunner) createConsole() (*goja.Object, error) {
|
||||
vm := g.vm
|
||||
console := vm.NewObject()
|
||||
|
||||
if err := console.Set("log", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
args := make([]interface{}, len(call.Arguments))
|
||||
for i, arg := range call.Arguments {
|
||||
args[i] = arg.Export()
|
||||
}
|
||||
fmt.Println(args...)
|
||||
return nil
|
||||
})); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := console.Set("error", vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
args := make([]interface{}, len(call.Arguments))
|
||||
for i, arg := range call.Arguments {
|
||||
args[i] = arg.Export()
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
return nil
|
||||
})); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return console, nil
|
||||
}
|
||||
117
lib/jsrunner/js.go
Normal file
117
lib/jsrunner/js.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package jsrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
var (
|
||||
instance JSRunner
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
type jsRunner struct {
|
||||
global js.Value
|
||||
}
|
||||
|
||||
type jsValue struct {
|
||||
val js.Value
|
||||
}
|
||||
|
||||
func NewJSRunner() JSRunner {
|
||||
once.Do(func() {
|
||||
instance = &jsRunner{global: js.Global()}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func (j *jsRunner) Engine() Engine {
|
||||
return Native
|
||||
}
|
||||
|
||||
func (j *jsRunner) MustGet(key string) (JSValue, error) {
|
||||
result := j.global.Get("elkResult")
|
||||
if result.IsUndefined() {
|
||||
return nil, fmt.Errorf("key %q not found in global scope", key)
|
||||
}
|
||||
defer j.global.Set("elkResult", js.Undefined())
|
||||
return &jsValue{val: result}, nil
|
||||
}
|
||||
|
||||
func (j *jsRunner) RunString(code string) (_ JSValue, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
result := j.global.Call("eval", code)
|
||||
return &jsValue{val: result}, nil
|
||||
}
|
||||
|
||||
func (v *jsValue) String() string {
|
||||
switch v.val.Type() {
|
||||
case js.TypeString:
|
||||
return v.val.String()
|
||||
default:
|
||||
return v.val.Call("toString").String()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *jsValue) Export() interface{} {
|
||||
switch v.val.Type() {
|
||||
case js.TypeString:
|
||||
return v.val.String()
|
||||
case js.TypeNumber:
|
||||
return v.val.Float()
|
||||
case js.TypeBoolean:
|
||||
return v.val.Bool()
|
||||
case js.TypeObject:
|
||||
if v.val.InstanceOf(js.Global().Get("Array")) {
|
||||
length := v.val.Length()
|
||||
arr := make([]interface{}, length)
|
||||
for i := 0; i < length; i++ {
|
||||
arr[i] = (&jsValue{val: v.val.Index(i)}).Export()
|
||||
}
|
||||
return arr
|
||||
}
|
||||
obj := make(map[string]interface{})
|
||||
keys := js.Global().Get("Object").Call("keys", v.val)
|
||||
length := keys.Length()
|
||||
for i := 0; i < length; i++ {
|
||||
key := keys.Index(i).String()
|
||||
val := v.val.Get(key)
|
||||
obj[key] = (&jsValue{val: val}).Export()
|
||||
}
|
||||
return obj
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO probably don't need
|
||||
func (j *jsRunner) NewObject() JSObject {
|
||||
return &jsValue{val: js.Global().Get("Object").New()}
|
||||
}
|
||||
|
||||
func (j *jsRunner) Set(name string, value interface{}) error {
|
||||
if name == "console" {
|
||||
js.Global().Set("console", js.Global().Get("console"))
|
||||
return nil
|
||||
}
|
||||
if jsObj, ok := value.(JSObject); ok {
|
||||
jsVal := jsObj.(*jsValue)
|
||||
js.Global().Set(name, jsVal.val)
|
||||
return nil
|
||||
}
|
||||
|
||||
js.Global().Set(name, js.ValueOf(value))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jsRunner) WaitPromise(ctx context.Context, val JSValue) (interface{}, error) {
|
||||
return val.Export(), nil
|
||||
}
|
||||
28
lib/jsrunner/jsrunner.go
Normal file
28
lib/jsrunner/jsrunner.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package jsrunner
|
||||
|
||||
import "context"
|
||||
|
||||
type Engine int
|
||||
|
||||
const (
|
||||
Goja Engine = iota
|
||||
Native
|
||||
)
|
||||
|
||||
type JSRunner interface {
|
||||
RunString(code string) (JSValue, error)
|
||||
NewObject() JSObject
|
||||
Set(name string, value interface{}) error
|
||||
WaitPromise(ctx context.Context, val JSValue) (interface{}, error)
|
||||
Engine() Engine
|
||||
MustGet(string) (JSValue, error)
|
||||
}
|
||||
|
||||
type JSValue interface {
|
||||
String() string
|
||||
Export() interface{}
|
||||
}
|
||||
|
||||
type JSObject interface {
|
||||
JSValue
|
||||
}
|
||||
Loading…
Reference in a new issue