d2/d2renderers/d2svg/appendix/appendix.go

180 lines
5.2 KiB
Go
Raw Normal View History

2022-12-28 04:29:51 +00:00
// appendix.go writes appendices/footnotes to SVG
// Intended to be run only for static exports, like PNG or PDF.
// SVG exports are already interactive.
package appendix
import (
"fmt"
"regexp"
"strconv"
"strings"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
)
2022-12-28 22:54:04 +00:00
// ┌──────────────┐
// │ │
// │ DIAGRAM │
// │ │
// PAD_ │ │
// SIDES │ │
// │ │ │
// │ └──────────────┘
// ▼ ◄────── PAD_TOP
//
// ─────────────────────────
//
//
// 1. asdfasdf
//
// ◄──── SPACER
// 2. qwerqwer
//
//
2022-12-28 04:29:51 +00:00
const (
2022-12-28 22:54:04 +00:00
PAD_TOP = 50
PAD_SIDES = 40
SPACER = 20
2022-12-28 19:39:20 +00:00
FONT_SIZE = 16
ICON_RADIUS = 16
2022-12-28 04:29:51 +00:00
)
var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`)
var widthRegex = regexp.MustCompile(`width=\"([0-9]+)\"`)
var heightRegex = regexp.MustCompile(`height=\"([0-9]+)\"`)
func AppendTooltips(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte {
svg := string(in)
appendix, w, h := generateTooltipAppendix(diagram, ruler, svg)
if h == 0 {
return in
}
viewboxMatch := viewboxRegex.FindStringSubmatch(svg)
viewboxRaw := viewboxMatch[1]
viewboxSlice := strings.Split(viewboxRaw, " ")
viewboxPadLeft, _ := strconv.Atoi(viewboxSlice[0])
viewboxWidth, _ := strconv.Atoi(viewboxSlice[2])
viewboxHeight, _ := strconv.Atoi(viewboxSlice[3])
tl, br := diagram.BoundingBox()
seperator := fmt.Sprintf(`<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#0A0F25" />`,
2022-12-28 19:39:20 +00:00
tl.X-PAD_SIDES, br.Y+PAD_TOP, go2.IntMax(w, br.X)+PAD_SIDES, br.Y+PAD_TOP)
2022-12-28 04:29:51 +00:00
appendix = seperator + appendix
w -= viewboxPadLeft
w += PAD_SIDES * 2
if viewboxWidth < w {
2022-12-28 19:39:20 +00:00
viewboxWidth = w
2022-12-28 04:29:51 +00:00
}
viewboxHeight += h + PAD_TOP
newViewbox := fmt.Sprintf(`viewBox="%s %s %s %s"`, viewboxSlice[0], viewboxSlice[1], strconv.Itoa(viewboxWidth), strconv.Itoa(viewboxHeight))
widthMatch := widthRegex.FindStringSubmatch(svg)
heightMatch := heightRegex.FindStringSubmatch(svg)
newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth))
newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight))
2022-12-28 22:54:04 +00:00
2022-12-28 04:29:51 +00:00
svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1)
svg = strings.Replace(svg, widthMatch[0], newWidth, 1)
svg = strings.Replace(svg, heightMatch[0], newHeight, 1)
if !strings.Contains(svg, `font-family: "font-regular"`) {
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
.text {
font-family: "font-regular";
}
@font-face {
font-family: font-regular;
src: url("%s");
}
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)])
}
2022-12-28 19:39:20 +00:00
if !strings.Contains(svg, `font-family: "font-bold"`) {
appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
.text-bold {
font-family: "font-bold";
}
@font-face {
font-family: font-bold;
src: url("%s");
}
]]></style>`, d2fonts.FontEncodings[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)])
}
2022-12-28 04:29:51 +00:00
closingIndex := strings.LastIndex(svg, "</svg>")
svg = svg[:closingIndex] + appendix + svg[closingIndex:]
2022-12-28 20:07:01 +00:00
i := 1
for _, s := range diagram.Shapes {
if s.Tooltip != "" {
// The clip-path has a unique ID, so this won't replace any user icons
// In the existing SVG, the transform places it top-left, so we adjust
svg = strings.Replace(svg, d2svg.TooltipIcon, generateNumberedIcon(i, 0, ICON_RADIUS), 1)
i++
}
}
2022-12-28 04:29:51 +00:00
return []byte(svg)
}
func generateTooltipAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) {
tl, br := diagram.BoundingBox()
maxWidth, totalHeight := 0, 0
var tooltipLines []string
i := 1
for _, s := range diagram.Shapes {
if s.Tooltip != "" {
line, w, h := generateTooltipLine(i, br.Y+(PAD_TOP*2)+totalHeight, s.Tooltip, ruler)
i++
tooltipLines = append(tooltipLines, line)
maxWidth = go2.IntMax(maxWidth, w)
totalHeight += h + SPACER
}
}
return fmt.Sprintf(`<g x="%d" y="%d" width="%d" height="100%%">%s</g>
`, tl.X, br.Y, (br.X - tl.X), strings.Join(tooltipLines, "\n")), maxWidth, totalHeight
}
2022-12-28 20:07:01 +00:00
func generateNumberedIcon(i, x, y int) string {
line := fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" fill="white" stroke="#DEE1EB" />`,
x+ICON_RADIUS, y, ICON_RADIUS)
line += fmt.Sprintf(`<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;text-anchor:middle;">%d</text>`,
x+ICON_RADIUS, y+5, FONT_SIZE, i)
return line
}
2022-12-28 04:29:51 +00:00
func generateTooltipLine(i, y int, text string, ruler *textmeasure.Ruler) (string, int, int) {
mtext := &d2target.MText{
Text: text,
FontSize: FONT_SIZE,
}
dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
2022-12-28 23:15:38 +00:00
line := fmt.Sprintf(`<g transform="translate(%d %d)" class="tooltip-icon">%s</g>`,
2022-12-28 20:07:01 +00:00
0, y, generateNumberedIcon(i, 0, 0))
2022-12-28 04:29:51 +00:00
line += fmt.Sprintf(`<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
2022-12-28 19:39:20 +00:00
ICON_RADIUS*3, y, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
2022-12-28 04:29:51 +00:00
2022-12-28 19:39:20 +00:00
return line, dims.Width + ICON_RADIUS*3, dims.Height
2022-12-28 04:29:51 +00:00
}