From c83a55f26b08f1827dba925ff7513ae84113bef1 Mon Sep 17 00:00:00 2001 From: Gavin Nishizawa Date: Mon, 7 Nov 2022 15:18:16 -0800 Subject: [PATCH] markdown code measurement --- d2renderers/d2svg/github-markdown.css | 16 +++-- d2renderers/textmeasure/markdown.go | 94 ++++++++++++++++++++------- e2etests/todo_test.go | 2 +- 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/d2renderers/d2svg/github-markdown.css b/d2renderers/d2svg/github-markdown.css index 1511228ab..eae0e3009 100644 --- a/d2renderers/d2svg/github-markdown.css +++ b/d2renderers/d2svg/github-markdown.css @@ -16,6 +16,10 @@ font-size: 1em; } +.md { + tab-size: 8; +} + /* based on https://github.com/sindresorhus/github-markdown-css */ @media (prefers-color-scheme: dark) { .md { @@ -341,15 +345,15 @@ margin-left: 0; } -.md tt, +/* .md tt, .md code { font-size: 12px; -} +} */ .md pre { margin-top: 0; margin-bottom: 0; - font-size: 12px; + /* font-size: 12px; */ word-wrap: normal; } @@ -635,7 +639,7 @@ .md tt { padding: 0.2em 0.4em; margin: 0; - font-size: 85%; + /* font-size: 85%; */ background-color: var(--color-neutral-muted); border-radius: 6px; } @@ -675,8 +679,8 @@ .md pre { padding: 16px; overflow: auto; - font-size: 85%; - line-height: 1.45; + /* font-size: 85%; + line-height: 1.45; */ background-color: var(--color-canvas-subtle); border-radius: 6px; } diff --git a/d2renderers/textmeasure/markdown.go b/d2renderers/textmeasure/markdown.go index c25907da6..b1ddb4b90 100644 --- a/d2renderers/textmeasure/markdown.go +++ b/d2renderers/textmeasure/markdown.go @@ -4,6 +4,7 @@ import ( "bytes" "math" "strings" + "unicode" "unicode/utf8" "github.com/PuerkitoBio/goquery" @@ -39,6 +40,9 @@ const ( Padding_pre = 16 MarginBottom_pre = 16 + PaddingTopBottom_code_em = 0.2 + PaddingLeftRight_code_em = 0.4 + PaddingLR_blockquote_em = 1. MarginBottom_blockquote = 16 BorderLeft_blockquote_em = 0.25 @@ -185,6 +189,11 @@ func hasAncestorElement(n *html.Node, elType string) bool { // measures node dimensions to match rendering with styles in github-markdown.css func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (width, height, marginTop, marginBottom float64) { + var parentElementType string + if n.Parent != nil && n.Parent.Type == html.ElementNode { + parentElementType = n.Parent.Data + } + switch n.Type { case html.TextNode: if strings.TrimSpace(n.Data) == "" { @@ -196,32 +205,63 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid spaceRune, _ := utf8.DecodeRuneInString(" ") // measure will not include leading or trailing whitespace, so we have to add in the space width spaceWidth := ruler.atlases[font].glyph(spaceRune).advance + tabWidth := 8 * spaceWidth str := n.Data - hasCodeParent := n.Parent != nil && n.Parent.Type == html.ElementNode && (n.Parent.Data == "pre" || n.Parent.Data == "code") - if !hasCodeParent { - str = strings.ReplaceAll(n.Data, "\n", " ") + + htmlWhitespace := true + switch parentElementType { + case "pre", "code": + htmlWhitespace = false } - if strings.HasPrefix(str, " ") { - str = strings.TrimPrefix(str, " ") - if hasPrev(n) { - spaceWidths += spaceWidth + + if htmlWhitespace { + str = strings.ReplaceAll(str, "\n", " ") + str = strings.ReplaceAll(str, "\t", " ") + if strings.HasPrefix(str, " ") { + str = strings.TrimPrefix(str, " ") + if hasPrev(n) { + spaceWidths += spaceWidth + } } - } - if strings.HasSuffix(str, " ") { - str = strings.TrimSuffix(str, " ") - if hasNext(n) { - spaceWidths += spaceWidth + if strings.HasSuffix(str, " ") { + str = strings.TrimSuffix(str, " ") + if hasNext(n) { + spaceWidths += spaceWidth + } + } + } else { + isNotSpace := func(r rune) bool { + return !unicode.IsSpace(r) + } + + startIndex := strings.IndexFunc(str, isNotSpace) + endIndex := strings.LastIndexFunc(str, isNotSpace) + + if startIndex != -1 && endIndex != -1 { + for i, r := range str { + // skip over runes in middle + if i >= startIndex && i <= endIndex { + continue + } + + // measure width of leading/trailing whitespace + switch r { + case ' ': + spaceWidths += spaceWidth + case '\t': + spaceWidths += tabWidth + } + } + + str = str[startIndex : endIndex+1] } } w, h := ruler.MeasurePrecise(font, str) - w += spaceWidths - // fmt.Printf("%d:%s width %v height %v fontStyle %s\n", depth, n.Data, w, h, font.Style) - if h > 0 && h < MarkdownLineHeightPx { - h = MarkdownLineHeightPx - } - return w, h, 0, 0 + // fmt.Printf("%d:'%s' width %v (%v) height %v fontStyle %s fontSize %v family %v\n", depth, n.Data, w, w+spaceWidths, h, font.Style, font.Size, font.Family) + + return w + spaceWidths, h, 0, 0 case html.ElementNode: // fmt.Printf("%d: %v node\n", depth, n.Data) switch n.Data { @@ -237,7 +277,8 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid case "b", "strong": font.Style = d2fonts.FONT_STYLE_BOLD case "pre", "code": - // TODO monospaced font + font.Family = d2fonts.SourceCodePro + font.Style = d2fonts.FONT_STYLE_REGULAR } if n.FirstChild != nil { @@ -284,10 +325,10 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid switch n.Data { case "blockquote": - width += float64(font.Size) * (2*PaddingLR_blockquote_em + BorderLeft_blockquote_em) + width += (2*PaddingLR_blockquote_em + BorderLeft_blockquote_em) * float64(font.Size) marginBottom = go2.Max(marginBottom, MarginBottom_blockquote) case "p": - if n.Parent != nil && n.Parent.Type == html.ElementNode && n.Parent.Data == "li" { + if parentElementType == "li" { marginTop = go2.Max(marginTop, MarginTop_li_p) } marginBottom = go2.Max(marginBottom, MarginBottom_p) @@ -296,7 +337,7 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid marginBottom = go2.Max(marginBottom, MarginBottom_h) switch n.Data { case "h1", "h2": - height += float64(HeaderToFontSize[n.Data]) * PaddingBottom_h1_h2_em + height += PaddingBottom_h1_h2_em * float64(font.Size) } case "li": width += PaddingLeft_ul_ol @@ -314,11 +355,20 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid width += 2 * Padding_pre height += 2 * Padding_pre marginBottom = go2.Max(marginBottom, MarginBottom_pre) + case "code": + if parentElementType != "pre" { + width += 2 * PaddingLeftRight_code_em * float64(font.Size) + height += 2 * PaddingTopBottom_code_em * float64(font.Size) + } case "hr": height += Height_hr marginTop = go2.Max(marginTop, MarginTopBottom_hr) marginBottom = go2.Max(marginBottom, MarginTopBottom_hr) } + + if height > 0 && height < MarkdownLineHeightPx { + height = MarkdownLineHeightPx + } // fmt.Printf("%d:%s width %v height %v mt %v mb %v\n", depth, n.Data, width, height, marginTop, marginBottom) } return width, height, marginTop, marginBottom diff --git a/e2etests/todo_test.go b/e2etests/todo_test.go index 6e522a236..b311aef33 100644 --- a/e2etests/todo_test.go +++ b/e2etests/todo_test.go @@ -13,7 +13,7 @@ func testTodo(t *testing.T) { skip: false, name: "md_code_inline", script: `md: |md - ` + "`" + "code`" + ` +` + "`code`" + ` | a -> md -> b `,