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
|
}, 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) {
|
func Compile(args []js.Value) (interface{}, error) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
return nil, &WASMError{Message: "missing JSON argument", Code: 400}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ func main() {
|
||||||
api.Register("getParentID", d2wasm.GetParentID)
|
api.Register("getParentID", d2wasm.GetParentID)
|
||||||
api.Register("getObjOrder", d2wasm.GetObjOrder)
|
api.Register("getObjOrder", d2wasm.GetObjOrder)
|
||||||
api.Register("getRefRanges", d2wasm.GetRefRanges)
|
api.Register("getRefRanges", d2wasm.GetRefRanges)
|
||||||
|
api.Register("getELKGraph", d2wasm.GetELKGraph)
|
||||||
api.Register("compile", d2wasm.Compile)
|
api.Register("compile", d2wasm.Compile)
|
||||||
api.Register("render", d2wasm.Render)
|
api.Register("render", d2wasm.Render)
|
||||||
api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition)
|
api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition)
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ async function buildAndCopy(buildType) {
|
||||||
resolve(ROOT_DIR, "wasm/wasm_exec.js"),
|
resolve(ROOT_DIR, "wasm/wasm_exec.js"),
|
||||||
join(config.outdir, "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;
|
border-radius: 4px;
|
||||||
font-family: monospace;
|
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;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -33,7 +42,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.radio-label {
|
.radio-label,
|
||||||
|
.checkbox-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -66,16 +76,24 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<textarea id="input">x -> y</textarea>
|
<textarea id="input">x -> y</textarea>
|
||||||
<div class="layout-toggle">
|
<div class="options-group">
|
||||||
<span>Layout:</span>
|
<div class="layout-toggle">
|
||||||
<div class="radio-group">
|
<span>Layout:</span>
|
||||||
<label class="radio-label">
|
<div class="radio-group">
|
||||||
<input type="radio" name="layout" value="dagre" checked />
|
<label class="radio-label">
|
||||||
Dagre
|
<input type="radio" name="layout" value="dagre" checked />
|
||||||
</label>
|
Dagre
|
||||||
<label class="radio-label">
|
</label>
|
||||||
<input type="radio" name="layout" value="elk" />
|
<label class="radio-label">
|
||||||
ELK
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,9 +106,10 @@
|
||||||
window.compile = async () => {
|
window.compile = async () => {
|
||||||
const input = document.getElementById("input").value;
|
const input = document.getElementById("input").value;
|
||||||
const layout = document.querySelector('input[name="layout"]:checked').value;
|
const layout = document.querySelector('input[name="layout"]:checked').value;
|
||||||
|
const sketch = document.getElementById("sketch").checked;
|
||||||
try {
|
try {
|
||||||
const result = await d2.compile(input, { layout });
|
const result = await d2.compile(input, { layout, sketch });
|
||||||
const svg = await d2.render(result.diagram);
|
const svg = await d2.render(result.diagram, { sketch });
|
||||||
document.getElementById("output").innerHTML = svg;
|
document.getElementById("output").innerHTML = svg;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@terrastruct/d2",
|
"name": "@terrastruct/d2",
|
||||||
"author": "Terrastruct, Inc.",
|
"author": "Terrastruct, Inc.",
|
||||||
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/terrastruct/d2.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() {
|
async init() {
|
||||||
this.worker = await createWorker();
|
this.worker = await createWorker();
|
||||||
|
|
||||||
|
const elkContent = await loadFile("./elk.js");
|
||||||
const wasmExecContent = await loadFile("./wasm_exec.js");
|
const wasmExecContent = await loadFile("./wasm_exec.js");
|
||||||
const wasmBinary = await loadFile("./d2.wasm");
|
const wasmBinary = await loadFile("./d2.wasm");
|
||||||
|
|
||||||
|
|
@ -63,6 +64,7 @@ export class D2 {
|
||||||
data: {
|
data: {
|
||||||
wasm: wasmBinary,
|
wasm: wasmBinary,
|
||||||
wasmExecContent: isNode ? wasmExecContent.toString() : null,
|
wasmExecContent: isNode ? wasmExecContent.toString() : null,
|
||||||
|
elkContent: isNode ? elkContent.toString() : null,
|
||||||
wasmExecUrl: isNode
|
wasmExecUrl: isNode
|
||||||
? null
|
? null
|
||||||
: URL.createObjectURL(
|
: URL.createObjectURL(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { wasmBinary, wasmExecJs } from "./wasm-loader.browser.js";
|
import { wasmBinary, wasmExecJs } from "./wasm-loader.browser.js";
|
||||||
import workerScript from "./worker.js" with { type: "text" };
|
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) {
|
export async function loadFile(path) {
|
||||||
if (path === "./d2.wasm") {
|
if (path === "./d2.wasm") {
|
||||||
return wasmBinary.buffer;
|
return wasmBinary.buffer;
|
||||||
|
|
@ -8,11 +11,11 @@ export async function loadFile(path) {
|
||||||
if (path === "./wasm_exec.js") {
|
if (path === "./wasm_exec.js") {
|
||||||
return new TextEncoder().encode(wasmExecJs).buffer;
|
return new TextEncoder().encode(wasmExecJs).buffer;
|
||||||
}
|
}
|
||||||
throw new Error(`Unexpected file request: ${path}`);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorker() {
|
export async function createWorker() {
|
||||||
let blob = new Blob([wasmExecJs, workerScript], {
|
let blob = new Blob([wasmExecJs, elkScript, workerScript], {
|
||||||
type: "text/javascript;charset=utf-8",
|
type: "text/javascript;charset=utf-8",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
let currentPort;
|
let currentPort;
|
||||||
let d2;
|
let d2;
|
||||||
|
let elk;
|
||||||
|
|
||||||
export function setupMessageHandler(isNode, port, initWasm) {
|
export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
currentPort = port;
|
currentPort = port;
|
||||||
|
|
@ -14,6 +15,7 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
eval(data.wasmExecContent);
|
eval(data.wasmExecContent);
|
||||||
}
|
}
|
||||||
d2 = await initWasm(data.wasm);
|
d2 = await initWasm(data.wasm);
|
||||||
|
elk = new ELK();
|
||||||
currentPort.postMessage({ type: "ready" });
|
currentPort.postMessage({ type: "ready" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
currentPort.postMessage({ type: "error", error: err.message });
|
currentPort.postMessage({ type: "error", error: err.message });
|
||||||
|
|
@ -22,6 +24,19 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
|
|
||||||
case "compile":
|
case "compile":
|
||||||
try {
|
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 result = await d2.compile(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
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 currentPort;
|
||||||
let d2;
|
let d2;
|
||||||
|
let elk;
|
||||||
|
|
||||||
export function setupMessageHandler(isNode, port, initWasm) {
|
export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
currentPort = port;
|
currentPort = port;
|
||||||
|
|
@ -14,8 +15,10 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
try {
|
try {
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
eval(data.wasmExecContent);
|
eval(data.wasmExecContent);
|
||||||
|
eval(data.elkContent);
|
||||||
}
|
}
|
||||||
d2 = await initWasm(data.wasm);
|
d2 = await initWasm(data.wasm);
|
||||||
|
elk = new ELK();
|
||||||
currentPort.postMessage({ type: "ready" });
|
currentPort.postMessage({ type: "ready" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
currentPort.postMessage({ type: "error", error: err.message });
|
currentPort.postMessage({ type: "error", error: err.message });
|
||||||
|
|
@ -24,6 +27,12 @@ export function setupMessageHandler(isNode, port, initWasm) {
|
||||||
|
|
||||||
case "compile":
|
case "compile":
|
||||||
try {
|
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 result = await d2.compile(JSON.stringify(data));
|
||||||
const response = JSON.parse(result);
|
const response = JSON.parse(result);
|
||||||
if (response.error) throw new Error(response.error.message);
|
if (response.error) throw new Error(response.error.message);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ describe("D2 Unit Tests", () => {
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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 () => {
|
test("render works", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
const result = await d2.compile("x -> y");
|
const result = await d2.compile("x -> y");
|
||||||
|
|
@ -18,6 +25,25 @@ describe("D2 Unit Tests", () => {
|
||||||
await d2.worker.terminate();
|
await d2.worker.terminate();
|
||||||
}, 20000);
|
}, 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 () => {
|
test("handles syntax errors correctly", async () => {
|
||||||
const d2 = new D2();
|
const d2 = new D2();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import (
|
||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xdefer"
|
"oss.terrastruct.com/util-go/xdefer"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
@ -20,6 +18,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/log"
|
"oss.terrastruct.com/d2/lib/log"
|
||||||
"oss.terrastruct.com/d2/lib/shape"
|
"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")
|
defer xdefer.Errorf(&err, "failed to dagre layout")
|
||||||
|
|
||||||
debugJS := false
|
debugJS := false
|
||||||
vm := goja.New()
|
runner := jsrunner.NewJSRunner()
|
||||||
if _, err := vm.RunString(dagreJS); err != nil {
|
if _, err := runner.RunString(dagreJS); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := vm.RunString(setupJS); err != nil {
|
if _, err := runner.RunString(setupJS); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +135,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
|
|
||||||
configJS := setGraphAttrs(rootAttrs)
|
configJS := setGraphAttrs(rootAttrs)
|
||||||
if _, err := vm.RunString(configJS); err != nil {
|
if _, err := runner.RunString(configJS); err != nil {
|
||||||
return err
|
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))
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := vm.RunString(`dagre.layout(g)`); err != nil {
|
if _, err := runner.RunString(`dagre.layout(g)`); err != nil {
|
||||||
if debugJS {
|
if debugJS {
|
||||||
log.Warn(ctx, "layout error", slog.Any("err", err))
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -216,7 +215,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, edge := range g.Edges {
|
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 {
|
if err != nil {
|
||||||
return err
|
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);
|
define([], f);
|
||||||
} else {
|
} else {
|
||||||
var g;
|
var g;
|
||||||
if (typeof window !== "undefined") {
|
g = this;
|
||||||
g = window;
|
|
||||||
} else if (typeof global !== "undefined") {
|
|
||||||
g = global;
|
|
||||||
} else if (typeof self !== "undefined") {
|
|
||||||
g = self;
|
|
||||||
} else {
|
|
||||||
g = this;
|
|
||||||
}
|
|
||||||
g.ELK = f();
|
g.ELK = f();
|
||||||
}
|
}
|
||||||
})(function () {
|
})(function () {
|
||||||
|
|
@ -337,9 +329,6 @@
|
||||||
|
|
||||||
// -------------- FAKE ELEMENTS GWT ASSUMES EXIST --------------
|
// -------------- FAKE ELEMENTS GWT ASSUMES EXIST --------------
|
||||||
var $wnd = { Error: {} };
|
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;
|
var $moduleName, $moduleBase;
|
||||||
|
|
||||||
|
|
@ -59795,13 +59784,8 @@
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (typeof document === uke && typeof self !== uke) {
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
var i = new h(self);
|
module.exports = { default: j, Worker: j };
|
||||||
self.onmessage = i.saveDispatch;
|
|
||||||
} else if (typeof module !== uke && module.exports) {
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
module.exports = { default: j, Worker: j };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function aae(a) {
|
function aae(a) {
|
||||||
if (a.N) return;
|
if (a.N) return;
|
||||||
|
|
@ -105682,16 +105666,7 @@
|
||||||
// -------------- RUN GWT INITIALIZATION CODE --------------
|
// -------------- RUN GWT INITIALIZATION CODE --------------
|
||||||
gwtOnLoad(null, "elk", null);
|
gwtOnLoad(null, "elk", null);
|
||||||
}.call(this));
|
}.call(this));
|
||||||
}.call(
|
}.call(this, {}));
|
||||||
this,
|
|
||||||
typeof global !== "undefined"
|
|
||||||
? global
|
|
||||||
: typeof self !== "undefined"
|
|
||||||
? self
|
|
||||||
: typeof window !== "undefined"
|
|
||||||
? window
|
|
||||||
: {}
|
|
||||||
));
|
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
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"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xdefer"
|
"oss.terrastruct.com/util-go/xdefer"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
@ -24,13 +21,11 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2graph"
|
"oss.terrastruct.com/d2/d2graph"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/shape"
|
"oss.terrastruct.com/d2/lib/shape"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed elk.js
|
|
||||||
var elkJS string
|
|
||||||
|
|
||||||
//go:embed setup.js
|
//go:embed setup.js
|
||||||
var setupJS string
|
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")
|
defer xdefer.Errorf(&err, "failed to ELK layout")
|
||||||
|
|
||||||
vm := goja.New()
|
runner := jsrunner.NewJSRunner()
|
||||||
|
|
||||||
console := vm.NewObject()
|
if runner.Engine() == jsrunner.Goja {
|
||||||
if err := vm.Set("console", console); err != nil {
|
console := runner.NewObject()
|
||||||
return err
|
if err := runner.Set("console", console); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := vm.RunString(elkJS); err != nil {
|
if _, err := runner.RunString(elkJS); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := vm.RunString(setupJS); err != nil {
|
if _, err := runner.RunString(setupJS); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
elkGraph := &ELKGraph{
|
elkGraph := &ELKGraph{
|
||||||
|
|
@ -443,41 +440,30 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
||||||
return 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 {
|
if _, err := runner.RunString(loadScript); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := vm.RunString(`elk.layout(graph)
|
val, err = runner.RunString(`elk.layout(graph)
|
||||||
.then(s => s)
|
.then(s => s)
|
||||||
.catch(err => err.message)
|
.catch(err => err.message)
|
||||||
`)
|
`)
|
||||||
|
} else {
|
||||||
|
val, err = runner.MustGet("elkResult")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := val.Export()
|
result, err := runner.WaitPromise(ctx, val)
|
||||||
if err != nil {
|
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{}
|
var jsonOut map[string]interface{}
|
||||||
switch out := result.(type) {
|
switch out := result.(type) {
|
||||||
case string:
|
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"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
|
|
||||||
"oss.terrastruct.com/util-go/xdefer"
|
"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) {
|
func Render(s string) (_ string, err error) {
|
||||||
defer xdefer.Errorf(&err, "latex failed to parse")
|
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
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := vm.RunString(setupJS); err != nil {
|
val, err := runner.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := vm.RunString(fmt.Sprintf(`adaptor.innerHTML(html.convert(`+"`"+"%s`"+`, {
|
|
||||||
em: %d,
|
em: %d,
|
||||||
ex: %d,
|
ex: %d,
|
||||||
}))`, s, pxPerEx*2, pxPerEx))
|
}))`, 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'] }),
|
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(),
|
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 });
|
const rc = rough.svg(root, { seed: 1 });
|
||||||
let node;
|
let node;
|
||||||
|
|
||||||
|
if (typeof globalThis !== "undefined") {
|
||||||
|
globalThis.rc = rc;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ import (
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/d2themes"
|
"oss.terrastruct.com/d2/d2themes"
|
||||||
"oss.terrastruct.com/d2/lib/color"
|
"oss.terrastruct.com/d2/lib/color"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/svg"
|
"oss.terrastruct.com/d2/lib/svg"
|
||||||
"oss.terrastruct.com/util-go/go2"
|
"oss.terrastruct.com/util-go/go2"
|
||||||
|
|
@ -29,8 +28,6 @@ var setupJS string
|
||||||
//go:embed streaks.txt
|
//go:embed streaks.txt
|
||||||
var streaks string
|
var streaks string
|
||||||
|
|
||||||
type Runner goja.Runtime
|
|
||||||
|
|
||||||
var baseRoughProps = `fillWeight: 2.0,
|
var baseRoughProps = `fillWeight: 2.0,
|
||||||
hachureGap: 16,
|
hachureGap: 16,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
|
|
@ -44,21 +41,14 @@ const (
|
||||||
FG_COLOR = color.N1
|
FG_COLOR = color.N1
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Runner) run(js string) (goja.Value, error) {
|
func LoadJS(runner jsrunner.JSRunner) error {
|
||||||
vm := (*goja.Runtime)(r)
|
if _, err := runner.RunString(roughJS); err != nil {
|
||||||
return vm.RunString(js)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
func InitSketchVM() (*Runner, error) {
|
|
||||||
vm := goja.New()
|
|
||||||
if _, err := vm.RunString(roughJS); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if _, err := vm.RunString(setupJS); err != nil {
|
if _, err := runner.RunString(setupJS); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
r := Runner(*vm)
|
return nil
|
||||||
return &r, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefineFillPatterns adds reusable patterns that are overlayed on shapes with
|
// 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, {
|
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
|
|
@ -119,7 +109,7 @@ func Rect(r *Runner, shape d2target.Shape) (string, error) {
|
||||||
return output, nil
|
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, {
|
jsBigRect := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
|
|
@ -179,7 +169,7 @@ func DoubleRect(r *Runner, shape d2target.Shape) (string, error) {
|
||||||
return output, nil
|
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, {
|
js := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
stroke: "#000",
|
stroke: "#000",
|
||||||
|
|
@ -218,7 +208,7 @@ func Oval(r *Runner, shape d2target.Shape) (string, error) {
|
||||||
return output, nil
|
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, {
|
jsBigCircle := fmt.Sprintf(`node = rc.ellipse(%d, %d, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
stroke: "#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
|
// 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 := ""
|
output := ""
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
js := fmt.Sprintf(`node = rc.path("%s", {
|
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
|
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 := ""
|
animatedClass := ""
|
||||||
if connection.Animated {
|
if connection.Animated {
|
||||||
animatedClass = " animated-connection"
|
animatedClass = " animated-connection"
|
||||||
|
|
@ -388,7 +378,7 @@ func Connection(r *Runner, connection d2target.Connection, path, attrs string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO cleanup
|
// TODO cleanup
|
||||||
func Table(r *Runner, shape d2target.Shape) (string, error) {
|
func Table(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||||
output := ""
|
output := ""
|
||||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
|
|
@ -530,7 +520,7 @@ func Table(r *Runner, shape d2target.Shape) (string, error) {
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Class(r *Runner, shape d2target.Shape) (string, error) {
|
func Class(r jsrunner.JSRunner, shape d2target.Shape) (string, error) {
|
||||||
output := ""
|
output := ""
|
||||||
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
js := fmt.Sprintf(`node = rc.rectangle(0, 0, %d, %d, {
|
||||||
fill: "#000",
|
fill: "#000",
|
||||||
|
|
@ -681,8 +671,8 @@ func classRow(shape d2target.Shape, box *geo.Box, prefix, nameText, typeText str
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
func computeRoughPathData(r jsrunner.JSRunner, js string) ([]string, error) {
|
||||||
if _, err := r.run(js); err != nil {
|
if _, err := r.RunString(js); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
roughPaths, err := extractRoughPaths(r)
|
roughPaths, err := extractRoughPaths(r)
|
||||||
|
|
@ -692,8 +682,8 @@ func computeRoughPathData(r *Runner, js string) ([]string, error) {
|
||||||
return extractPathData(roughPaths)
|
return extractPathData(roughPaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeRoughPaths(r *Runner, js string) ([]roughPath, error) {
|
func computeRoughPaths(r jsrunner.JSRunner, js string) ([]roughPath, error) {
|
||||||
if _, err := r.run(js); err != nil {
|
if _, err := r.RunString(js); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return extractRoughPaths(r)
|
return extractRoughPaths(r)
|
||||||
|
|
@ -722,8 +712,8 @@ func (rp roughPath) StyleCSS() string {
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractRoughPaths(r *Runner) ([]roughPath, error) {
|
func extractRoughPaths(r jsrunner.JSRunner) ([]roughPath, error) {
|
||||||
val, err := r.run("JSON.stringify(node.children, null, ' ')")
|
val, err := r.RunString("JSON.stringify(node.children, null, ' ')")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -756,7 +746,7 @@ func extractPathData(roughPaths []roughPath) ([]string, error) {
|
||||||
return paths, nil
|
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
|
// Note: selected each seed that looks the good for consistent renders
|
||||||
switch arrowhead {
|
switch arrowhead {
|
||||||
case d2target.ArrowArrowhead:
|
case d2target.ArrowArrowhead:
|
||||||
|
|
@ -854,7 +844,7 @@ func ArrowheadJS(r *Runner, arrowhead d2target.Arrowhead, stroke string, strokeW
|
||||||
return
|
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{}
|
arrowPaths := []string{}
|
||||||
|
|
||||||
if connection.SrcArrow != d2target.NoArrowhead {
|
if connection.SrcArrow != d2target.NoArrowhead {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||||
"oss.terrastruct.com/d2/lib/color"
|
"oss.terrastruct.com/d2/lib/color"
|
||||||
"oss.terrastruct.com/d2/lib/geo"
|
"oss.terrastruct.com/d2/lib/geo"
|
||||||
|
"oss.terrastruct.com/d2/lib/jsrunner"
|
||||||
"oss.terrastruct.com/d2/lib/label"
|
"oss.terrastruct.com/d2/lib/label"
|
||||||
"oss.terrastruct.com/d2/lib/shape"
|
"oss.terrastruct.com/d2/lib/shape"
|
||||||
"oss.terrastruct.com/d2/lib/svg"
|
"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 := ""
|
opacityStyle := ""
|
||||||
if connection.Opacity != 1.0 {
|
if connection.Opacity != 1.0 {
|
||||||
opacityStyle = fmt.Sprintf(" style='opacity:%f'", connection.Opacity)
|
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)
|
path := pathData(connection, srcAdj, dstAdj)
|
||||||
mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID)
|
mask := fmt.Sprintf(`mask="url(#%s)"`, labelMaskID)
|
||||||
|
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Connection(sketchRunner, connection, path, mask)
|
out, err := d2sketch.Connection(jsRunner, connection, path, mask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprint(writer, out)
|
fmt.Fprint(writer, out)
|
||||||
|
|
||||||
// render sketch arrowheads separately
|
// render sketch arrowheads separately
|
||||||
arrowPaths, err := d2sketch.Arrowheads(sketchRunner, connection, srcAdj, dstAdj)
|
arrowPaths, err := d2sketch.Arrowheads(jsRunner, connection, srcAdj, dstAdj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -957,7 +958,7 @@ func render3DHexagon(targetShape d2target.Shape, inlineTheme *d2themes.Theme) st
|
||||||
return borderMask + mainShapeRendered + renderedSides + renderedBorder
|
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>"
|
closingTag := "</g>"
|
||||||
if targetShape.Link != "" {
|
if targetShape.Link != "" {
|
||||||
|
|
||||||
|
|
@ -1021,8 +1022,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
|
|
||||||
switch targetShape.Type {
|
switch targetShape.Type {
|
||||||
case d2target.ShapeClass:
|
case d2target.ShapeClass:
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Class(sketchRunner, targetShape)
|
out, err := d2sketch.Class(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1035,8 +1036,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
fmt.Fprint(writer, closingTag)
|
fmt.Fprint(writer, closingTag)
|
||||||
return labelMask, nil
|
return labelMask, nil
|
||||||
case d2target.ShapeSQLTable:
|
case d2target.ShapeSQLTable:
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Table(sketchRunner, targetShape)
|
out, err := d2sketch.Table(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1053,8 +1054,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
if targetShape.Multiple {
|
if targetShape.Multiple {
|
||||||
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
fmt.Fprint(writer, renderDoubleOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
||||||
}
|
}
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.DoubleOval(sketchRunner, targetShape)
|
out, err := d2sketch.DoubleOval(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1066,8 +1067,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
if targetShape.Multiple {
|
if targetShape.Multiple {
|
||||||
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
fmt.Fprint(writer, renderOval(multipleTL, width, height, fill, "", stroke, style, inlineTheme))
|
||||||
}
|
}
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Oval(sketchRunner, targetShape)
|
out, err := d2sketch.Oval(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1111,8 +1112,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
el.Rx = borderRadius
|
el.Rx = borderRadius
|
||||||
fmt.Fprint(writer, el.Render())
|
fmt.Fprint(writer, el.Render())
|
||||||
}
|
}
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Rect(sketchRunner, targetShape)
|
out, err := d2sketch.Rect(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1155,8 +1156,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
el.Rx = borderRadius
|
el.Rx = borderRadius
|
||||||
fmt.Fprint(writer, el.Render())
|
fmt.Fprint(writer, el.Render())
|
||||||
}
|
}
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.DoubleRect(sketchRunner, targetShape)
|
out, err := d2sketch.DoubleRect(jsRunner, targetShape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1203,8 +1204,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
|
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -1235,8 +1236,8 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
out, err := d2sketch.Paths(sketchRunner, targetShape, s.GetSVGPathData())
|
out, err := d2sketch.Paths(jsRunner, targetShape, s.GetSVGPathData())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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
|
var DEFAULT_DARK_THEME *int64 = nil // no theme selected
|
||||||
|
|
||||||
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
var sketchRunner *d2sketch.Runner
|
var jsRunner jsrunner.JSRunner
|
||||||
pad := DEFAULT_PADDING
|
pad := DEFAULT_PADDING
|
||||||
themeID := d2themescatalog.NeutralDefault.ID
|
themeID := d2themescatalog.NeutralDefault.ID
|
||||||
darkThemeID := DEFAULT_DARK_THEME
|
darkThemeID := DEFAULT_DARK_THEME
|
||||||
|
|
@ -1856,8 +1857,8 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
pad = int(*opts.Pad)
|
pad = int(*opts.Pad)
|
||||||
}
|
}
|
||||||
if opts.Sketch != nil && *opts.Sketch {
|
if opts.Sketch != nil && *opts.Sketch {
|
||||||
var err error
|
jsRunner = jsrunner.NewJSRunner()
|
||||||
sketchRunner, err = d2sketch.InitSketchVM()
|
err := d2sketch.LoadJS(jsRunner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1941,7 +1942,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
}
|
}
|
||||||
for _, obj := range allObjects {
|
for _, obj := range allObjects {
|
||||||
if c, is := obj.(d2target.Connection); is {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -1949,7 +1950,7 @@ func Render(diagram *d2target.Diagram, opts *RenderOpts) ([]byte, error) {
|
||||||
labelMasks = append(labelMasks, labelMask)
|
labelMasks = append(labelMasks, labelMask)
|
||||||
}
|
}
|
||||||
} else if s, is := obj.(d2target.Shape); is {
|
} 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if labelMask != "" {
|
} 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)
|
fmt.Fprintf(upperBuf, `<style type="text/css">%s</style>`, css)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sketchRunner != nil {
|
if jsRunner != nil {
|
||||||
d2sketch.DefineFillPatterns(upperBuf)
|
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