// 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/d2parser"
"oss.terrastruct.com/d2/d2renderers/d2fonts"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/lib/color"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
)
// ┌──────────────┐
// │ │
// │ DIAGRAM │
// │ │
// PAD_ │ │
// SIDES │ │
// │ │ │
// │ └──────────────┘
// ▼ ◄────── PAD_TOP
//
// ─────────────────────────
//
//
// 1. asdfasdf
//
// ◄──── SPACER
// 2. qwerqwer
//
//
const (
PAD_TOP = 50
PAD_SIDES = 40
SPACER = 20
FONT_SIZE = 16
ICON_RADIUS = 16
)
var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`)
var widthRegex = regexp.MustCompile(`width=\"([.0-9]+)\"`)
var heightRegex = regexp.MustCompile(`height=\"([.0-9]+)\"`)
var svgRegex = regexp.MustCompile(`")
svg = svg[:closingIndex] + appendix + svg[closingIndex:]
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++
}
if s.Link != "" {
svg = strings.Replace(svg, d2svg.LinkIcon, generateNumberedIcon(i, 0, ICON_RADIUS), 1)
i++
}
}
return []byte(svg)
}
// transformInternalLink turns
// "root.layers.x.layers.y"
// into
// "root > x > y"
func transformInternalLink(link string) string {
if link == "" || !strings.HasPrefix(link, "root") {
return link
}
mk, err := d2parser.ParseMapKey(link)
if err != nil {
return ""
}
key := d2graph.Key(mk.Key)
if len(key) > 1 {
for i := 1; i < len(key); i += 2 {
key[i] = ">"
}
}
return strings.Join(key, " ")
}
func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) {
tl, br := diagram.BoundingBox()
maxWidth, totalHeight := 0, 0
var lines []string
i := 1
for _, s := range diagram.Shapes {
for _, txt := range []string{s.Tooltip, transformInternalLink(s.Link)} {
if txt != "" {
line, w, h := generateLine(i, br.Y+(PAD_TOP*2)+totalHeight, txt, ruler)
i++
lines = append(lines, line)
maxWidth = go2.IntMax(maxWidth, w)
totalHeight += h + SPACER
}
}
}
if len(lines) == 0 {
return "", 0, 0
}
totalHeight += SPACER
return fmt.Sprintf(`%s
`, tl.X, br.Y, (br.X - tl.X), strings.Join(lines, "\n")), maxWidth, totalHeight
}
func generateNumberedIcon(i, x, y int) string {
line := fmt.Sprintf(``,
x+ICON_RADIUS, y, ICON_RADIUS)
line += fmt.Sprintf(`%d`,
x+ICON_RADIUS, y+5, FONT_SIZE, i)
return line
}
func generateLine(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)
line := fmt.Sprintf(`%s`,
0, y, generateNumberedIcon(i, 0, 0))
line += fmt.Sprintf(`%s`,
ICON_RADIUS*3, y+5, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
return line, dims.Width + ICON_RADIUS*3, go2.IntMax(dims.Height, ICON_RADIUS*2)
}