mirror of
https://github.com/kovetskiy/mark.git
synced 2026-05-02 13:22:40 +00:00
Add support for d2lang
This commit is contained in:
107
d2/d2.go
Normal file
107
d2/d2.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package d2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
"github.com/chromedp/chromedp"
|
||||
|
||||
"github.com/kovetskiy/mark/attachment"
|
||||
"github.com/reconquest/pkg/log"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
|
||||
"oss.terrastruct.com/d2/d2lib"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2svg"
|
||||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
d2log "oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
var renderTimeout = 120 * time.Second
|
||||
|
||||
func ProcessD2(title string, d2Diagram []byte, scale float64) (attachment.Attachment, error) {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
|
||||
ctx = d2log.WithDefault(ctx)
|
||||
defer cancel()
|
||||
|
||||
ruler, err := textmeasure.NewRuler()
|
||||
if err != nil {
|
||||
return attachment.Attachment{}, err
|
||||
}
|
||||
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
|
||||
return d2dagrelayout.DefaultLayout, nil
|
||||
}
|
||||
renderOpts := &d2svg.RenderOpts{
|
||||
Pad: go2.Pointer(int64(5)),
|
||||
ThemeID: &d2themescatalog.GrapeSoda.ID,
|
||||
}
|
||||
compileOpts := &d2lib.CompileOptions{
|
||||
LayoutResolver: layoutResolver,
|
||||
Ruler: ruler,
|
||||
}
|
||||
|
||||
diagram, _, err := d2lib.Compile(ctx, string(d2Diagram), compileOpts, renderOpts)
|
||||
if err != nil {
|
||||
return attachment.Attachment{}, err
|
||||
}
|
||||
|
||||
out, err := d2svg.Render(diagram, renderOpts)
|
||||
if err != nil {
|
||||
return attachment.Attachment{}, err
|
||||
}
|
||||
|
||||
log.Debugf(nil, "Rendering: %q", title)
|
||||
pngBytes, boxModel, err := convertSVGtoPNG(ctx, out, scale)
|
||||
if err != nil {
|
||||
return attachment.Attachment{}, err
|
||||
}
|
||||
|
||||
checkSum, err := attachment.GetChecksum(bytes.NewReader(d2Diagram))
|
||||
log.Debugf(nil, "Checksum: %q -> %s", title, checkSum)
|
||||
|
||||
if err != nil {
|
||||
return attachment.Attachment{}, err
|
||||
}
|
||||
if title == "" {
|
||||
title = checkSum
|
||||
}
|
||||
|
||||
fileName := title + ".png"
|
||||
|
||||
return attachment.Attachment{
|
||||
ID: "",
|
||||
Name: title,
|
||||
Filename: fileName,
|
||||
FileBytes: pngBytes,
|
||||
Checksum: checkSum,
|
||||
Replace: title,
|
||||
Width: strconv.FormatInt(boxModel.Width, 10),
|
||||
Height: strconv.FormatInt(boxModel.Height, 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertSVGtoPNG(ctx context.Context, svg []byte, scale float64) (png []byte, m *dom.BoxModel, err error) {
|
||||
var (
|
||||
result []byte
|
||||
model *dom.BoxModel
|
||||
)
|
||||
ctx, cancel := chromedp.NewContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Navigate(fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(svg))),
|
||||
chromedp.ScreenshotScale(`document.querySelector("svg > svg")`, scale, &result, chromedp.ByJSPath),
|
||||
chromedp.Dimensions(`document.querySelector("svg > svg")`, &model, chromedp.ByJSPath),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return result, model, err
|
||||
}
|
||||
102
d2/d2_test.go
Normal file
102
d2/d2_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package d2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/kovetskiy/mark/attachment"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var diagram string = `d2
|
||||
vars: {
|
||||
d2-config: {
|
||||
layout-engine: elk
|
||||
# Terminal theme code
|
||||
theme-id: 300
|
||||
}
|
||||
}
|
||||
network: {
|
||||
cell tower: {
|
||||
satellites: {
|
||||
shape: stored_data
|
||||
style.multiple: true
|
||||
}
|
||||
|
||||
transmitter
|
||||
|
||||
satellites -> transmitter: send
|
||||
satellites -> transmitter: send
|
||||
satellites -> transmitter: send
|
||||
}
|
||||
|
||||
online portal: {
|
||||
ui: {shape: hexagon}
|
||||
}
|
||||
|
||||
data processor: {
|
||||
storage: {
|
||||
shape: cylinder
|
||||
style.multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
cell tower.transmitter -> data processor.storage: phone logs
|
||||
}
|
||||
|
||||
user: {
|
||||
shape: person
|
||||
width: 130
|
||||
}
|
||||
|
||||
user -> network.cell tower: make call
|
||||
user -> network.online portal.ui: access {
|
||||
style.stroke-dash: 3
|
||||
}
|
||||
|
||||
api server -> network.online portal.ui: display
|
||||
api server -> logs: persist
|
||||
logs: {shape: page; style.multiple: true}
|
||||
|
||||
network.data processor -> api server
|
||||
`
|
||||
|
||||
func TestExtractD2Image(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
markdown []byte
|
||||
scale float64
|
||||
want attachment.Attachment
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{"example", []byte(diagram), 1.0, attachment.Attachment{
|
||||
// This is only the PNG Magic Header
|
||||
FileBytes: []byte{0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa},
|
||||
Filename: "example.png",
|
||||
Name: "example",
|
||||
Replace: "example",
|
||||
Checksum: "58fa387384181445e2d8f90a8c7fda945cb75174f73e8b9853ff59b9e0103ddd",
|
||||
ID: "",
|
||||
Width: "198",
|
||||
Height: "441",
|
||||
},
|
||||
assert.NoError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ProcessD2(tt.name, tt.markdown, tt.scale)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("processD2(%v, %v)", tt.name, string(tt.markdown))) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.want.Filename, got.Filename, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
// We only test for the header as png changes based on system png library
|
||||
assert.Equal(t, tt.want.FileBytes, got.FileBytes[0:8], "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.Name, got.Name, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.Replace, got.Replace, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.Checksum, got.Checksum, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.ID, got.ID, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.Width, got.Width, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
assert.Equal(t, tt.want.Height, got.Height, "processD2(%v, %v)", tt.name, string(tt.markdown))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user