migrate sketch

This commit is contained in:
Alexander Wang 2025-01-15 16:30:17 -07:00
parent e723881dfe
commit 9c19637fff
No known key found for this signature in database
GPG key ID: BE3937D0D52D8927
6 changed files with 107 additions and 74 deletions

View file

@ -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);

View file

@ -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);

View file

@ -25,6 +25,16 @@ 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("handles syntax errors correctly", async () => { test("handles syntax errors correctly", async () => {
const d2 = new D2(); const d2 = new D2();
try { try {

View file

@ -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;
}

View file

@ -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 {

View file

@ -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)
} }
} }