1
2
3
4
5 package appendix
6
7 import (
8 "fmt"
9 "regexp"
10 "sort"
11 "strconv"
12 "strings"
13
14 "oss.terrastruct.com/d2/d2graph"
15 "oss.terrastruct.com/d2/d2renderers/d2fonts"
16 "oss.terrastruct.com/d2/d2renderers/d2svg"
17 "oss.terrastruct.com/d2/d2target"
18 "oss.terrastruct.com/d2/d2themes"
19 "oss.terrastruct.com/d2/lib/color"
20 "oss.terrastruct.com/d2/lib/textmeasure"
21 "oss.terrastruct.com/util-go/go2"
22 )
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 const (
45 PAD_TOP = 50
46 PAD_SIDES = 40
47 SPACER = 20
48
49 FONT_SIZE = 16
50 ICON_RADIUS = 16
51 )
52
53 var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`)
54 var widthRegex = regexp.MustCompile(`width=\"([.0-9]+)\"`)
55 var heightRegex = regexp.MustCompile(`height=\"([.0-9]+)\"`)
56 var svgRegex = regexp.MustCompile(`<svg(.*?)>`)
57
58 func FindViewboxSlice(svg []byte) []string {
59 viewboxMatches := viewboxRegex.FindAllStringSubmatch(string(svg), 2)
60 viewboxMatch := viewboxMatches[1]
61 viewboxRaw := viewboxMatch[1]
62 return strings.Split(viewboxRaw, " ")
63 }
64
65 func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte {
66 svg := string(in)
67
68 appendix, w, h := generateAppendix(diagram, ruler, svg)
69
70 if h == 0 {
71 return in
72 }
73
74
75 viewboxMatches := viewboxRegex.FindAllStringSubmatch(svg, 2)
76 viewboxMatch := viewboxMatches[1]
77 viewboxRaw := viewboxMatch[1]
78 viewboxSlice := strings.Split(viewboxRaw, " ")
79 viewboxPadLeft, _ := strconv.Atoi(viewboxSlice[0])
80 viewboxWidth, _ := strconv.Atoi(viewboxSlice[2])
81 viewboxHeight, _ := strconv.Atoi(viewboxSlice[3])
82
83 tl, br := diagram.BoundingBox()
84 separatorEl := d2themes.NewThemableElement("line")
85 separatorEl.X1 = float64(tl.X - PAD_SIDES)
86 separatorEl.Y1 = float64(br.Y + PAD_TOP)
87 separatorEl.X2 = float64(go2.IntMax(w, br.X) + PAD_SIDES)
88 separatorEl.Y2 = float64(br.Y + PAD_TOP)
89 separatorEl.Stroke = color.B2
90 appendix = separatorEl.Render() + appendix
91
92 w -= viewboxPadLeft
93 w += PAD_SIDES * 2
94 if viewboxWidth < w {
95 viewboxWidth = w
96 }
97
98 viewboxHeight += h + PAD_TOP
99
100 newOuterViewbox := fmt.Sprintf(`viewBox="0 0 %d %d"`, viewboxWidth, viewboxHeight)
101 newViewbox := fmt.Sprintf(`viewBox="%s %s %s %s"`, viewboxSlice[0], viewboxSlice[1], strconv.Itoa(viewboxWidth), strconv.Itoa(viewboxHeight))
102
103 dimensionsToUpdate := 2
104 outerSVG := svgRegex.FindString(svg)
105
106 if widthRegex.FindString(outerSVG) != "" {
107 dimensionsToUpdate++
108 }
109
110
111 widthMatches := widthRegex.FindAllStringSubmatch(svg, dimensionsToUpdate)
112 heightMatches := heightRegex.FindAllStringSubmatch(svg, dimensionsToUpdate)
113 newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth))
114 newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight))
115
116 svg = strings.Replace(svg, viewboxMatches[0][0], newOuterViewbox, 1)
117 svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1)
118 for i := 0; i < dimensionsToUpdate; i++ {
119 svg = strings.Replace(svg, widthMatches[i][0], newWidth, 1)
120 svg = strings.Replace(svg, heightMatches[i][0], newHeight, 1)
121 }
122
123 if !strings.Contains(svg, `font-family: "font-regular"`) {
124 appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
125 .text {
126 font-family: "font-regular";
127 }
128 @font-face {
129 font-family: font-regular;
130 src: url("%s");
131 }
132 ]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)))
133 }
134 if !strings.Contains(svg, `font-family: "font-bold"`) {
135 appendix += fmt.Sprintf(`<style type="text/css"><![CDATA[
136 .text-bold {
137 font-family: "font-bold";
138 }
139 @font-face {
140 font-family: font-bold;
141 src: url("%s");
142 }
143 ]]></style>`, d2fonts.FontEncodings.Get(d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)))
144 }
145
146 closingIndex := strings.LastIndex(svg, "</svg></svg>")
147 svg = svg[:closingIndex] + appendix + svg[closingIndex:]
148
149
150
151 type appendixIcon struct {
152 number int
153 isTooltip bool
154 shape d2target.Shape
155 }
156 var renderOrder []appendixIcon
157
158 i := 1
159 for _, s := range diagram.Shapes {
160 if s.Tooltip != "" {
161 renderOrder = append(renderOrder, appendixIcon{i, true, s})
162 i++
163 }
164 if s.Link != "" {
165 renderOrder = append(renderOrder, appendixIcon{i, false, s})
166 i++
167 }
168 }
169
170 sort.SliceStable(renderOrder, func(i, j int) bool {
171 iZIndex := renderOrder[i].shape.GetZIndex()
172 jZIndex := renderOrder[j].shape.GetZIndex()
173 if iZIndex != jZIndex {
174 return iZIndex < jZIndex
175 }
176 return renderOrder[i].shape.Level < renderOrder[j].shape.Level
177 })
178
179
180 for _, icon := range renderOrder {
181
182
183 var iconStr string
184 if icon.isTooltip {
185 iconStr = d2svg.TooltipIcon
186 } else {
187 iconStr = d2svg.LinkIcon
188 }
189 svg = strings.Replace(svg, iconStr, generateNumberedIcon(icon.number, 0, ICON_RADIUS), 1)
190 }
191
192 return []byte(svg)
193 }
194
195 func generateAppendix(diagram *d2target.Diagram, ruler *textmeasure.Ruler, svg string) (string, int, int) {
196 tl, br := diagram.BoundingBox()
197
198 maxWidth, totalHeight := 0, 0
199
200 var lines []string
201 i := 1
202
203 for _, s := range diagram.Shapes {
204 for _, txt := range []string{s.Tooltip, s.PrettyLink} {
205 if txt != "" {
206 line, w, h := generateLine(i, br.Y+(PAD_TOP*2)+totalHeight, txt, ruler)
207 i++
208 lines = append(lines, line)
209 maxWidth = go2.IntMax(maxWidth, w)
210 totalHeight += h + SPACER
211 }
212 }
213 }
214 if len(lines) == 0 {
215 return "", 0, 0
216 }
217 totalHeight += SPACER
218
219 return fmt.Sprintf(`<g class="appendix" x="%d" y="%d" width="%d" height="100%%">%s</g>
220 `, tl.X, br.Y, (br.X - tl.X), strings.Join(lines, "\n")), maxWidth, totalHeight
221 }
222
223 func generateNumberedIcon(i, x, y int) string {
224 line := fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" fill="white" stroke="#DEE1EB" />`,
225 x+ICON_RADIUS, y, ICON_RADIUS)
226
227 line += fmt.Sprintf(`<text class="text-bold" x="%d" y="%d" style="font-size: %dpx;text-anchor:middle;">%d</text>`,
228 x+ICON_RADIUS, y+5, FONT_SIZE, i)
229
230 return line
231 }
232
233 func generateLine(i, y int, text string, ruler *textmeasure.Ruler) (string, int, int) {
234 mtext := &d2target.MText{
235 Text: text,
236 FontSize: FONT_SIZE,
237 }
238
239 dims := d2graph.GetTextDimensions(nil, ruler, mtext, nil)
240
241 line := fmt.Sprintf(`<g transform="translate(%d %d)" class="appendix-icon">%s</g>`,
242 0, y, generateNumberedIcon(i, 0, 0))
243
244 line += fmt.Sprintf(`<text class="text" x="%d" y="%d" style="font-size: %dpx;">%s</text>`,
245 ICON_RADIUS*3, y+5, FONT_SIZE, d2svg.RenderText(text, ICON_RADIUS*3, float64(dims.Height)))
246
247 return line, dims.Width + ICON_RADIUS*3, go2.IntMax(dims.Height, ICON_RADIUS*2)
248 }
249
View as plain text