Merge pull request #214 from gavin-ts/fix-md-newlines

render: fix markdown newlines
This commit is contained in:
gavin-ts 2022-11-25 16:24:00 -08:00 committed by GitHub
commit 781e155a88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 3611 additions and 55 deletions

View file

@ -11,3 +11,5 @@
[#187](https://github.com/terrastruct/d2/pull/187) [#187](https://github.com/terrastruct/d2/pull/187)
- System dark mode was incorrectly applying to markdown in renders. - System dark mode was incorrectly applying to markdown in renders.
[#159](https://github.com/terrastruct/d2/issues/159) [#159](https://github.com/terrastruct/d2/issues/159)
- Fixes markdown newlines created with a trailing double space or backslash.
[#214](https://github.com/terrastruct/d2/pull/214)

View file

@ -85,6 +85,7 @@ func init() {
markdownRenderer = goldmark.New( markdownRenderer = goldmark.New(
goldmark.WithRendererOptions( goldmark.WithRendererOptions(
goldmarkHtml.WithUnsafe(), goldmarkHtml.WithUnsafe(),
goldmarkHtml.WithXHTML(),
), ),
) )
} }
@ -114,9 +115,9 @@ func MeasureMarkdown(mdText string, ruler *Ruler) (width, height int, err error)
// TODO consider setting a max width + (manual) text wrapping // TODO consider setting a max width + (manual) text wrapping
bodyNode := doc.Find("body").First().Nodes[0] bodyNode := doc.Find("body").First().Nodes[0]
bodyWidth, bodyHeight, _, _ := ruler.measureNode(0, bodyNode, font) bodyAttrs := ruler.measureNode(0, bodyNode, font)
return int(math.Ceil(bodyWidth)), int(math.Ceil(bodyHeight)), nil return int(math.Ceil(bodyAttrs.width)), int(math.Ceil(bodyAttrs.height)), nil
} }
func hasPrev(n *html.Node) bool { func hasPrev(n *html.Node) bool {
@ -191,8 +192,16 @@ func hasAncestorElement(n *html.Node, elType string) bool {
return hasAncestorElement(n.Parent, elType) return hasAncestorElement(n.Parent, elType)
} }
type blockAttrs struct {
width, height, marginTop, marginBottom float64
}
func (b *blockAttrs) isNotEmpty() bool {
return b != nil && *b != blockAttrs{}
}
// measures node dimensions to match rendering with styles in github-markdown.css // 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) { func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) blockAttrs {
var parentElementType string var parentElementType string
if n.Parent != nil && n.Parent.Type == html.ElementNode { if n.Parent != nil && n.Parent.Type == html.ElementNode {
parentElementType = n.Parent.Data parentElementType = n.Parent.Data
@ -201,7 +210,7 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid
switch n.Type { switch n.Type {
case html.TextNode: case html.TextNode:
if strings.TrimSpace(n.Data) == "" { if strings.TrimSpace(n.Data) == "" {
return return blockAttrs{}
} }
spaceWidths := 0. spaceWidths := 0.
@ -242,8 +251,9 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid
w *= FontSize_pre_code_em w *= FontSize_pre_code_em
h *= FontSize_pre_code_em h *= FontSize_pre_code_em
} }
return w + spaceWidths, h, 0, 0 return blockAttrs{w + spaceWidths, h, 0, 0}
case html.ElementNode: case html.ElementNode:
isCode := false
switch n.Data { switch n.Data {
case "h1", "h2", "h3", "h4", "h5", "h6": case "h1", "h2", "h3", "h4", "h5", "h6":
font = HeaderFonts[n.Data] font = HeaderFonts[n.Data]
@ -259,96 +269,138 @@ func (ruler *Ruler) measureNode(depth int, n *html.Node, font d2fonts.Font) (wid
case "pre", "code": case "pre", "code":
font.Family = d2fonts.SourceCodePro font.Family = d2fonts.SourceCodePro
font.Style = d2fonts.FONT_STYLE_REGULAR font.Style = d2fonts.FONT_STYLE_REGULAR
isCode = true
} }
block := blockAttrs{}
if n.FirstChild != nil { if n.FirstChild != nil {
first := getNext(n.FirstChild) first := getNext(n.FirstChild)
last := getPrev(n.LastChild) last := getPrev(n.LastChild)
var prevMarginBottom float64 var blocks []blockAttrs
var current *blockAttrs
// first create blocks from combined inline elements, then combine all blocks
// current will be non-nil while inline elements are being combined into a block
for child := n.FirstChild; child != nil; child = child.NextSibling { for child := n.FirstChild; child != nil; child = child.NextSibling {
childWidth, childHeight, childMarginTop, childMarginBottom := ruler.measureNode(depth+1, child, font) childBlock := ruler.measureNode(depth+1, child, font)
if child.Type == html.ElementNode && isBlockElement(child.Data) { if child.Type == html.ElementNode && isBlockElement(child.Data) {
if child == first { if current != nil {
if n.Data == "blockquote" { blocks = append(blocks, *current)
childMarginTop = 0.
} }
marginTop = go2.Max(marginTop, childMarginTop) current = &blockAttrs{}
if child == first && n.Data == "blockquote" {
current.marginTop = 0.
} else { } else {
marginDiff := childMarginTop - prevMarginBottom current.marginTop = childBlock.marginTop
}
if child == last && n.Data == "blockquote" {
current.marginBottom = 0.
} else {
current.marginBottom = childBlock.marginBottom
}
current.width = childBlock.width
current.height = childBlock.height
blocks = append(blocks, *current)
current = nil
} else if child.Type == html.ElementNode && child.Data == "br" {
if current != nil {
if !isCode && current.height > 0 && current.height < MarkdownLineHeightPx {
current.height = MarkdownLineHeightPx
}
blocks = append(blocks, *current)
current = nil
}
} else if childBlock.isNotEmpty() {
if current == nil {
current = &childBlock
} else {
current.marginTop = go2.Max(current.marginTop, childBlock.marginTop)
current.marginBottom = go2.Max(current.marginBottom, childBlock.marginBottom)
current.width += childBlock.width
current.height = go2.Max(current.height, childBlock.height)
}
}
}
if current != nil {
if !isCode && current.height > 0 && current.height < MarkdownLineHeightPx {
current.height = MarkdownLineHeightPx
}
blocks = append(blocks, *current)
current = nil
}
var prevMarginBottom float64
for i, b := range blocks {
if i == 0 {
block.marginTop = go2.Max(block.marginTop, b.marginTop)
} else {
marginDiff := b.marginTop - prevMarginBottom
if marginDiff > 0 { if marginDiff > 0 {
childHeight += marginDiff block.height += marginDiff
} }
} }
if child == last { if i == len(blocks)-1 {
if n.Data == "blockquote" { block.marginBottom = go2.Max(block.marginBottom, b.marginBottom)
childMarginBottom = 0.
}
marginBottom = go2.Max(marginBottom, childMarginBottom)
} else { } else {
childHeight += childMarginBottom block.height += b.marginBottom
prevMarginBottom = childMarginBottom prevMarginBottom = b.marginBottom
} }
height += childHeight block.height += b.height
width = go2.Max(width, childWidth) block.width = go2.Max(block.width, b.width)
} else {
marginTop = go2.Max(marginTop, childMarginTop)
marginBottom = go2.Max(marginBottom, childMarginBottom)
width += childWidth
height = go2.Max(height, childHeight)
}
} }
} }
switch n.Data { switch n.Data {
case "blockquote": case "blockquote":
width += (2*PaddingLR_blockquote_em + BorderLeft_blockquote_em) * float64(font.Size) block.width += (2*PaddingLR_blockquote_em + BorderLeft_blockquote_em) * float64(font.Size)
marginBottom = go2.Max(marginBottom, MarginBottom_blockquote) block.marginBottom = go2.Max(block.marginBottom, MarginBottom_blockquote)
case "p": case "p":
if parentElementType == "li" { if parentElementType == "li" {
marginTop = go2.Max(marginTop, MarginTop_li_p) block.marginTop = go2.Max(block.marginTop, MarginTop_li_p)
} }
marginBottom = go2.Max(marginBottom, MarginBottom_p) block.marginBottom = go2.Max(block.marginBottom, MarginBottom_p)
case "h1", "h2", "h3", "h4", "h5", "h6": case "h1", "h2", "h3", "h4", "h5", "h6":
marginTop = go2.Max(marginTop, MarginTop_h) block.marginTop = go2.Max(block.marginTop, MarginTop_h)
marginBottom = go2.Max(marginBottom, MarginBottom_h) block.marginBottom = go2.Max(block.marginBottom, MarginBottom_h)
switch n.Data { switch n.Data {
case "h1", "h2": case "h1", "h2":
height += PaddingBottom_h1_h2_em * float64(font.Size) block.height += PaddingBottom_h1_h2_em * float64(font.Size)
} }
case "li": case "li":
width += PaddingLeft_ul_ol block.width += PaddingLeft_ul_ol
if hasPrev(n) { if hasPrev(n) {
marginTop = go2.Max(marginTop, 4) block.marginTop = go2.Max(block.marginTop, 4)
} }
case "ol", "ul": case "ol", "ul":
if hasAncestorElement(n, "ul") || hasAncestorElement(n, "ol") { if hasAncestorElement(n, "ul") || hasAncestorElement(n, "ol") {
marginTop = 0 block.marginTop = 0
marginBottom = 0 block.marginBottom = 0
} else { } else {
marginBottom = go2.Max(marginBottom, MarginBottom_ul) block.marginBottom = go2.Max(block.marginBottom, MarginBottom_ul)
} }
case "pre": case "pre":
width += 2 * Padding_pre block.width += 2 * Padding_pre
height += 2 * Padding_pre block.height += 2 * Padding_pre
marginBottom = go2.Max(marginBottom, MarginBottom_pre) block.marginBottom = go2.Max(block.marginBottom, MarginBottom_pre)
case "code": case "code":
if parentElementType != "pre" { if parentElementType != "pre" {
width += 2 * PaddingLeftRight_code_em * float64(font.Size) block.width += 2 * PaddingLeftRight_code_em * float64(font.Size)
height += 2 * PaddingTopBottom_code_em * float64(font.Size) block.height += 2 * PaddingTopBottom_code_em * float64(font.Size)
} }
case "hr": case "hr":
height += Height_hr block.height += Height_hr
marginTop = go2.Max(marginTop, MarginTopBottom_hr) block.marginTop = go2.Max(block.marginTop, MarginTopBottom_hr)
marginBottom = go2.Max(marginBottom, MarginTopBottom_hr) block.marginBottom = go2.Max(block.marginBottom, MarginTopBottom_hr)
} }
if block.height > 0 && block.height < MarkdownLineHeightPx {
if height > 0 && height < MarkdownLineHeightPx { block.height = MarkdownLineHeightPx
height = MarkdownLineHeightPx
} }
return block
} }
return width, height, marginTop, marginBottom return blockAttrs{}
} }

View file

@ -925,6 +925,28 @@ x -> y: {
stroke-dash: 5 stroke-dash: 5
} }
} }
`,
},
{
name: "md_2space_newline",
script: `
markdown: {
md: |md
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
}
`,
},
{
name: "md_backslash_newline",
script: `
markdown: {
md: |md
Lorem ipsum dolor sit amet, consectetur adipiscing elit,\
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
}
`, `,
}, },
} }

View file

@ -0,0 +1,81 @@
{
"name": "",
"shapes": [
{
"id": "markdown",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 559,
"height": 148,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "markdown",
"fontSize": 28,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 129,
"labelHeight": 41,
"labelPosition": "INSIDE_TOP_CENTER"
},
{
"id": "markdown.md",
"type": "text",
"pos": {
"x": 50,
"y": 50
},
"width": 459,
"height": 48,
"level": 2,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 459,
"labelHeight": 48
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 659 KiB

View file

@ -0,0 +1,81 @@
{
"name": "",
"shapes": [
{
"id": "markdown",
"type": "",
"pos": {
"x": 12,
"y": 12
},
"width": 609,
"height": 198,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "markdown",
"fontSize": 28,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 129,
"labelHeight": 41,
"labelPosition": "INSIDE_TOP_CENTER"
},
{
"id": "markdown.md",
"type": "text",
"pos": {
"x": 87,
"y": 87
},
"width": 459,
"height": 48,
"level": 2,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 459,
"labelHeight": 48
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 659 KiB

View file

@ -0,0 +1,81 @@
{
"name": "",
"shapes": [
{
"id": "markdown",
"type": "",
"pos": {
"x": 0,
"y": 0
},
"width": 559,
"height": 148,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "markdown",
"fontSize": 28,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 129,
"labelHeight": 41,
"labelPosition": "INSIDE_TOP_CENTER"
},
{
"id": "markdown.md",
"type": "text",
"pos": {
"x": 50,
"y": 50
},
"width": 459,
"height": 48,
"level": 2,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\\\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 459,
"labelHeight": 48
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 659 KiB

View file

@ -0,0 +1,81 @@
{
"name": "",
"shapes": [
{
"id": "markdown",
"type": "",
"pos": {
"x": 12,
"y": 12
},
"width": 609,
"height": 198,
"level": 1,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "markdown",
"fontSize": 28,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 129,
"labelHeight": 41,
"labelPosition": "INSIDE_TOP_CENTER"
},
{
"id": "markdown.md",
"type": "text",
"pos": {
"x": 87,
"y": 87
},
"width": 459,
"height": 48,
"level": 2,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#FFFFFF",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\\\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "markdown",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 459,
"labelHeight": 48
}
],
"connections": []
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 659 KiB