package textmeasure

import (
	
	stdhtml 
	
	
	
	
	

	
	
	goldmarkHTML 
	

	
	
)

// MarkdownPrimitiveKind identifies the kind of native SVG primitive in a
// MarkdownLayout. The layout package intentionally does not choose concrete
// colors so that renderers can map the semantic roles to their active theme.
type MarkdownPrimitiveKind string

const (
	MarkdownTextPrimitive MarkdownPrimitiveKind = "text"
	MarkdownRectPrimitive MarkdownPrimitiveKind = "rect"
	MarkdownLinePrimitive MarkdownPrimitiveKind = "line"
)

// MarkdownColorRole is a semantic Markdown color. It mirrors the roles used by
// github-markdown.css without coupling text measurement to a D2 theme.
type MarkdownColorRole string

const (
	MarkdownColorNone             MarkdownColorRole = ""
	MarkdownColorForeground       MarkdownColorRole = "foreground"
	MarkdownColorForegroundStroke MarkdownColorRole = "foreground-stroke"
	MarkdownColorMuted            MarkdownColorRole = "muted"
	MarkdownColorMutedStroke      MarkdownColorRole = "muted-stroke"
	MarkdownColorAccent           MarkdownColorRole = "accent"
	MarkdownColorBorder           MarkdownColorRole = "border"
	MarkdownColorBorderMuted      MarkdownColorRole = "border-muted"
	MarkdownColorCanvas           MarkdownColorRole = "canvas"
	MarkdownColorCanvasSubtle     MarkdownColorRole = "canvas-subtle"
	MarkdownColorNeutralMuted     MarkdownColorRole = "neutral-muted"
)

// MarkdownFontRole lets an SVG renderer select the correct embedded D2 font.
// Font sizes are kept separately on each primitive.
type MarkdownFontRole string

const (
	MarkdownFontRegular      MarkdownFontRole = "regular"
	MarkdownFontSemibold     MarkdownFontRole = "semibold"
	MarkdownFontBold         MarkdownFontRole = "bold"
	MarkdownFontItalic       MarkdownFontRole = "italic"
	MarkdownFontMono         MarkdownFontRole = "mono"
	MarkdownFontMonoSemibold MarkdownFontRole = "mono-semibold"
	MarkdownFontMonoBold     MarkdownFontRole = "mono-bold"
	MarkdownFontMonoItalic   MarkdownFontRole = "mono-italic"
)

type MarkdownTextDecoration string

const (
	MarkdownTextDecorationNone        MarkdownTextDecoration = ""
	MarkdownTextDecorationLineThrough MarkdownTextDecoration = "line-through"
)

// MarkdownPrimitive is a serializer-neutral native SVG drawing operation.
//
// Text uses X/Y as the baseline origin and Width/Height as its measured box.
// Rect uses X/Y/Width/Height/Radius. Line uses X/Y and X2/Y2. FillRole and
// StrokeRole are semantic so light/dark themes can serialize the same layout.
type MarkdownPrimitive struct {
	Kind MarkdownPrimitiveKind

	X, Y, X2, Y2        float64
	Width, Height       float64
	Radius, StrokeWidth float64

	Text       string
	Font       MarkdownFontRole
	FontSize   float64
	FillRole   MarkdownColorRole
	StrokeRole MarkdownColorRole
	Link       string
	LinkTitle  string
	Decoration MarkdownTextDecoration
	// SyntheticBold/SyntheticItalic preserve inherited CSS axes when the
	// innermost Markdown element selects a different concrete D2 font family.
	// For example, em > strong uses the bold face with a synthetic slant, while
	// strong > em uses the italic face with synthetic weight.
	SyntheticBold   bool
	SyntheticItalic bool
	// TextLength fits the glyphs to Width. CSS paints a discretionary soft
	// hyphen at one third of an em even inside D2's monospace code face.
	TextLength bool
}

// MarkdownLayout contains the exact dimensions used by D2 layout and the
// positioned primitives used to paint those dimensions. MeasureMarkdown calls
// LayoutMarkdown, ensuring measurement and rendering share one code path.
type MarkdownLayout struct {
	Width, Height int
	Primitives    []MarkdownPrimitive
	// Corpus contains the visible source text used by the primitives. Renderers
	// embedding subset fonts should include it.
	Corpus string
}

// MarkdownSVGPaint controls how a semantic role is emitted by SVG. Class is
// optional; Color is an SVG paint value such as "currentColor", "#fff", or a
// CSS variable.
type MarkdownSVGPaint struct {
	Class string
	Color string
}

// MarkdownSVGOptions controls native SVG serialization without affecting
// layout. A renderer can override role paints and font classes to match its
// theme/font embedding scheme.
type MarkdownSVGOptions struct {
	Class       string
	RolePaint   map[MarkdownColorRole]MarkdownSVGPaint
	FontClasses map[MarkdownFontRole]string
	// DisableLinks emits linked text without inner <a> elements. d2svg uses
	// this when a Markdown label is already wrapped by a shape/connection link.
	DisableLinks bool
	// Underline applies D2's style.underline to every Markdown text primitive.
	Underline bool
}

// LayoutMarkdown parses Markdown once, measures it with D2's existing box
// model, and paints that same box model into native SVG primitives.
func ( string,  *Ruler,  *d2fonts.FontFamily,  *d2fonts.FontFamily,  int) (*MarkdownLayout, error) {
	,  := RenderMarkdown()
	if  != nil {
		return nil, 
	}
	,  := goquery.NewDocumentFromReader(strings.NewReader())
	if  != nil {
		return nil, 
	}
	 := .Find("body").First()
	if len(.Nodes) == 0 {
		return &MarkdownLayout{}, nil
	}
	// Keep an untouched parse tree for D2's established graph-layout
	// measurement. Native painting normalizes whitespace to CSS semantics, but
	// measuring that normalized tree would subtly resize old diagrams (notably
	// a newline immediately after <br> in mixed-Unicode content).
	,  := goquery.NewDocumentFromReader(strings.NewReader())
	if  != nil {
		return nil, 
	}
	 := .Find("body").First()
	if len(.Nodes) == 0 {
		return &MarkdownLayout{}, nil
	}
	if  := validateMarkdownSVGNodes(.Nodes[0]);  != nil {
		return nil, 
	}
	normalizeMarkdownWhitespaceTree(.Nodes[0])

	 := .LineHeightFactor
	 := .boundsWithDot
	.LineHeightFactor = MarkdownLineHeight
	.boundsWithDot = true
	defer func() {
		.LineHeightFactor = 
		.boundsWithDot = 
	}()

	if  == nil {
		 := d2fonts.SourceSansPro
		 = &
	}
	if  == nil {
		 := d2fonts.SourceCodePro
		 = &
	}

	 := markdownPaintContext{
		fontFamily:       ,
		monoFontFamily:   ,
		fontSize:         ,
		fontScale:        1,
		fontStyle:        d2fonts.FONT_STYLE_REGULAR,
		lineHeightFactor: MarkdownLineHeight,
		mono:             * == *,
		role:             MarkdownColorForeground,
	}
	 := markdownPainter{ruler: , layout: &MarkdownLayout{}}
	// D2's historical Markdown measurements are graph-layout API: changing them
	// moves shapes and routes. Paint with browser-style CSS line boxes below,
	// but retain those established outer dimensions.
	 := .measureNode(0, .Nodes[0], , , , d2fonts.FONT_STYLE_REGULAR, false, false)
	 := int(math.Ceil(.width))
	 := int(math.Ceil(.height))
	.layout.Width = 
	.layout.Height = 
	.paintBlock(.Nodes[0], 0, 0, , float64())
	var  strings.Builder
	for ,  := range .layout.Primitives {
		if .Kind == MarkdownTextPrimitive {
			.WriteString(.Text)
		}
	}
	.layout.Corpus = .String()
	return .layout, nil
}

// SVG serializes a MarkdownLayout as native SVG elements. It deliberately
// emits a fragment rather than an <svg> document so d2svg can translate and
// theme it inside a diagram.
func ( *MarkdownLayout) ( MarkdownSVGOptions) string {
	if .Class == "" {
		.Class = "md md-native"
	}
	 := defaultMarkdownSVGRolePaint()
	for ,  := range .RolePaint {
		[] = 
	}
	 := defaultMarkdownSVGFontClasses()
	for ,  := range .FontClasses {
		[] = 
	}

	var  strings.Builder
	fmt.Fprintf(&, `<g class="%s">`, svg.EscapeText(.Class))
	for ,  := range .Primitives {
		 := make([]string, 0, 3)
		if .Kind == MarkdownTextPrimitive {
			 = append(, "md-text")
			if  := [.Font];  != "" {
				 = append(, )
			}
		}
		if  := [.FillRole]; .Class != "" {
			 = append(, .Class)
		}
		if  := [.StrokeRole]; .Class != "" {
			 = append(, .Class)
		}
		 := ""
		if len() > 0 {
			 = fmt.Sprintf(` class="%s"`, svg.EscapeText(strings.Join(, " ")))
		}
		 := [.FillRole].Color
		 := [.StrokeRole].Color
		 := SafeMarkdownLink(.Link)
		if  != "" && !.DisableLinks {
			fmt.Fprintf(&, `<a href="%s" xlink:href="%[1]s">`, svg.EscapeText())
			if .LinkTitle != "" {
				fmt.Fprintf(&, `<title>%s</title>`, svg.EscapeText(.LinkTitle))
			}
		}

		switch .Kind {
		case MarkdownRectPrimitive:
			fmt.Fprintf(&, `<rect x="%s" y="%s" width="%s" height="%s" rx="%s"%s`,
				mdFloat(.X), mdFloat(.Y), mdFloat(.Width), mdFloat(.Height), mdFloat(.Radius), )
			if  == "" {
				 = "none"
			}
			fmt.Fprintf(&, ` fill="%s"`, svg.EscapeText())
			if  != "" && .StrokeWidth > 0 {
				fmt.Fprintf(&, ` stroke="%s" stroke-width="%s"`, svg.EscapeText(), mdFloat(.StrokeWidth))
			}
			.WriteString(`/>`)
		case MarkdownLinePrimitive:
			if  == "" {
				 = "currentColor"
			}
			fmt.Fprintf(&, `<line x1="%s" y1="%s" x2="%s" y2="%s"%s stroke="%s" stroke-width="%s"/>`,
				mdFloat(.X), mdFloat(.Y), mdFloat(.X2), mdFloat(.Y2), ,
				svg.EscapeText(), mdFloat(.StrokeWidth))
		case MarkdownTextPrimitive:
			if  == "" {
				 = "currentColor"
			}
			fmt.Fprintf(&, `<text x="%s" y="%s"%s fill="%s" font-size="%s" xml:space="preserve"`,
				mdFloat(.X), mdFloat(.Y), , svg.EscapeText(), mdFloat(.FontSize))
			 := string(.Decoration)
			if .Underline {
				if  == "" {
					 = "underline"
				} else {
					 = "underline " + 
				}
			}
			if  != "" {
				fmt.Fprintf(&, ` text-decoration="%s"`, svg.EscapeText())
			}
			if .SyntheticBold {
				.WriteString(` font-weight="bold"`)
			}
			if .SyntheticItalic {
				.WriteString(` font-style="italic"`)
			}
			if .TextLength {
				fmt.Fprintf(&, ` textLength="%s" lengthAdjust="spacingAndGlyphs"`, mdFloat(.Width))
			}
			fmt.Fprintf(&, `>%s</text>`, svg.EscapeText(.Text))
		}
		if  != "" && !.DisableLinks {
			.WriteString(`</a>`)
		}
	}
	.WriteString(`</g>`)
	return .String()
}

// SafeMarkdownLink returns link when it is safe to expose as interactive
// metadata and an empty string for unsafe Markdown URL schemes. PDF and PPTX
// annotations use the same policy.
func ( string) string {
	 = strings.TrimSpace()
	if  == "" || IsDangerousLink() {
		return ""
	}
	return 
}

// IsDangerousLink reports whether link uses a URL scheme that must not be
// exposed as interactive metadata. It intentionally shares Goldmark's
// dangerous-URL policy with Markdown links while preserving relative URLs,
// board targets, and opaque application schemes.
func ( string) bool {
	,  := normalizeLinkForSafety()
	if ! {
		// Do not fail open on an excessively nested encoding. Normal links settle
		// in one pass, and a URL with an already-valid scheme settles immediately.
		return true
	}
	return goldmarkHTML.IsDangerousURL([]byte())
}

const maxLinkNormalizationPasses = 8

func normalizeLinkForSafety( string) (string, bool) {
	 := strings.TrimSpace()
	for range maxLinkNormalizationPasses {
		 := stdhtml.UnescapeString()
		 = decodeLinkPercentEscapes()
		// URI consumers ignore controls and whitespace in or around schemes.
		// Normalize them for classification only; safe links retain their
		// original spelling when emitted.
		 = strings.Map(func( rune) rune {
			if unicode.IsControl() || unicode.IsSpace() {
				return -1
			}
			return 
		}, )
		if hasURLScheme() ||  ==  {
			return , true
		}
		 = 
	}
	return , false
}

func hasURLScheme( string) bool {
	 := strings.IndexByte(, ':')
	if  <= 0 || !isASCIIAlpha([0]) {
		return false
	}
	for  := 1;  < ; ++ {
		 := []
		if !isASCIIAlpha() && ( < '0' ||  > '9') &&  != '+' &&  != '-' &&  != '.' {
			return false
		}
	}
	return true
}

func isASCIIAlpha( byte) bool {
	return  >= 'a' &&  <= 'z' ||  >= 'A' &&  <= 'Z'
}

func decodeLinkPercentEscapes( string) string {
	 := strings.IndexByte(, '%')
	if  < 0 {
		return 
	}
	var  strings.Builder
	.Grow(len())
	.WriteString([:])
	for  := ;  < len(); ++ {
		if +2 < len() {
			,  := hexValue([+1])
			,  := hexValue([+2])
			if [] == '%' &&  &&  {
				.WriteByte(<<4 | )
				 += 2
				continue
			}
		}
		.WriteByte([])
	}
	return .String()
}

func hexValue( byte) (byte, bool) {
	switch {
	case  >= '0' &&  <= '9':
		return  - '0', true
	case  >= 'a' &&  <= 'f':
		return  - 'a' + 10, true
	case  >= 'A' &&  <= 'F':
		return  - 'A' + 10, true
	default:
		return 0, false
	}
}

func defaultMarkdownSVGRolePaint() map[MarkdownColorRole]MarkdownSVGPaint {
	return map[MarkdownColorRole]MarkdownSVGPaint{
		MarkdownColorForeground:       {Class: "md-color-fg", Color: "currentColor"},
		MarkdownColorForegroundStroke: {Class: "md-stroke-fg", Color: "currentColor"},
		MarkdownColorMuted:            {Class: "md-color-muted", Color: "var(--color-fg-muted, currentColor)"},
		MarkdownColorMutedStroke:      {Class: "md-stroke-muted", Color: "var(--color-fg-muted, currentColor)"},
		MarkdownColorAccent:           {Class: "md-color-accent", Color: "var(--color-accent-fg, currentColor)"},
		MarkdownColorBorder:           {Class: "md-color-border", Color: "var(--color-border-default, currentColor)"},
		MarkdownColorBorderMuted:      {Class: "md-color-border-muted", Color: "var(--color-border-muted, currentColor)"},
		MarkdownColorCanvas:           {Class: "md-color-canvas", Color: "var(--color-canvas-default, none)"},
		MarkdownColorCanvasSubtle:     {Class: "md-color-canvas-subtle", Color: "var(--color-canvas-subtle, none)"},
		MarkdownColorNeutralMuted:     {Class: "md-color-neutral-muted", Color: "var(--color-neutral-muted, none)"},
	}
}

func defaultMarkdownSVGFontClasses() map[MarkdownFontRole]string {
	return map[MarkdownFontRole]string{
		MarkdownFontRegular:      "text",
		MarkdownFontSemibold:     "text-semibold",
		MarkdownFontBold:         "text-bold",
		MarkdownFontItalic:       "text-italic",
		MarkdownFontMono:         "text-mono",
		MarkdownFontMonoSemibold: "text-mono-semibold",
		MarkdownFontMonoBold:     "text-mono-bold",
		MarkdownFontMonoItalic:   "text-mono-italic",
	}
}

func mdFloat( float64) string {
	if math.Abs() < 0.0005 {
		 = 0
	}
	return strconv.FormatFloat(, 'f', 3, 64)
}

func markdownSnap( float64) float64 {
	return math.Floor( + 0.5)
}

type markdownPaintContext struct {
	fontFamily       *d2fonts.FontFamily
	monoFontFamily   *d2fonts.FontFamily
	fontSize         int
	fontScale        float64
	fontStyle        d2fonts.FontStyle
	bold             bool
	italic           bool
	lineHeightFactor float64
	mono             bool
	inlineCode       bool
	headingCode      bool
	heading          string
	role             MarkdownColorRole
	link             string
	linkTitle        string
	decoration       MarkdownTextDecoration
	decorationGroup  int
	decorationRole   MarkdownColorRole
	tableCell        bool
	textAlign        string
}

func ( markdownPaintContext) () float64 {
	return float64(.fontSize) * .fontScale
}

func ( markdownPaintContext) () d2fonts.FontStyle {
	return .fontStyle
}

func ( markdownPaintContext) () bool {
	return .bold && .fontStyle != d2fonts.FONT_STYLE_BOLD && .fontStyle != d2fonts.FONT_STYLE_SEMIBOLD
}

func ( markdownPaintContext) () bool {
	return .italic && .fontStyle != d2fonts.FONT_STYLE_ITALIC
}

func ( markdownPaintContext) () MarkdownFontRole {
	 := .effectiveFontStyle()
	if .mono {
		switch  {
		case d2fonts.FONT_STYLE_SEMIBOLD:
			return MarkdownFontMonoSemibold
		case d2fonts.FONT_STYLE_BOLD:
			return MarkdownFontMonoBold
		case d2fonts.FONT_STYLE_ITALIC:
			return MarkdownFontMonoItalic
		default:
			return MarkdownFontMono
		}
	}
	switch  {
	case d2fonts.FONT_STYLE_SEMIBOLD:
		return MarkdownFontSemibold
	case d2fonts.FONT_STYLE_BOLD:
		return MarkdownFontBold
	case d2fonts.FONT_STYLE_ITALIC:
		return MarkdownFontItalic
	default:
		return MarkdownFontRegular
	}
}

type markdownPainter struct {
	ruler  *Ruler
	layout *MarkdownLayout
}

func ( *markdownPainter) ( *html.Node,  markdownPaintContext) blockAttrs {
	 := .ruler.LineHeightFactor
	.ruler.LineHeightFactor = .lineHeightFactor
	 := .ruler.measureCSSNode(0, , .fontFamily, .monoFontFamily, .fontSize, .fontScale, .fontStyle, .bold, .italic)
	.ruler.LineHeightFactor = 
	return 
}

func ( *markdownPainter) ( *html.Node,  markdownPaintContext) markdownPaintContext {
	if .Type != html.ElementNode {
		return 
	}
	switch .Data {
	case "h1", "h2", "h3", "h4", "h5", "h6":
		.heading = .Data
		.fontScale *= HeaderToFontScale(.Data)
		.fontStyle = d2fonts.FONT_STYLE_SEMIBOLD
		.bold = false
		.lineHeightFactor = LineHeight_h
		if .Data == "h6" {
			.role = MarkdownColorMuted
		}
	case "em":
		.fontStyle = d2fonts.FONT_STYLE_ITALIC
		.italic = true
	case "b", "strong":
		.fontStyle = d2fonts.FONT_STYLE_BOLD
		.bold = true
	case "pre", "code":
		.fontFamily = .monoFontFamily
		.mono = true
		.fontStyle = d2fonts.FONT_STYLE_REGULAR
		.inlineCode = .Data == "code" && (.Parent == nil || .Parent.Data != "pre")
		.headingCode = .inlineCode && hasMarkdownHeadingAncestor()
	case "a":
		.link = nodeAttr(, "href")
		.linkTitle = nodeAttr(, "title")
		.role = MarkdownColorAccent
	case "del", "s", "strike":
		.decoration = MarkdownTextDecorationLineThrough
	case "blockquote":
		.role = MarkdownColorMuted
	}
	return 
}

func ( *markdownPainter) ( *html.Node, ,  float64,  markdownPaintContext,  float64) float64 {
	 := .measure(, )
	 := .childContext(, )
	if .Type != html.ElementNode {
		return .paintInlineNodes([]*html.Node{}, , , , )
	}

	switch .Data {
	case "hr":
		 := 
		if  <= 0 {
			 = .width
		}
		,  := markdownSnap(), markdownSnap(+)
		,  := markdownSnap(), markdownSnap(+Height_hr_em*.cssFontSize())
		.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
			Kind: MarkdownRectPrimitive, X: , Y: , Width:  - ,
			Height:  - , FillRole: MarkdownColorBorder,
		})
		return .height
	case "pre":
		return .paintPre(, , , , , )
	case "table":
		return .paintTable(, , , , , )
	}

	,  := , 
	 := 0.0
	switch .Data {
	case "blockquote":
		 = goMax(1, math.Floor(BorderLeft_blockquote_em*.cssFontSize()))
		 :=  + PaddingLR_blockquote_em*.cssFontSize()
		 += 
		 = goMax(0, --PaddingLR_blockquote_em*.cssFontSize())
	case "li":
		 := .listIndent(, )
		 += 
		 = goMax(0, -)
		.paintListMarker(, , , , )
	}

	 := .paintFlow(, , , , )
	 := goMax(.height, )
	if .Data == "h1" || .Data == "h2" {
		 := .cssFontSize() * .lineHeightFactor
		if  >  {
			 = .height +  - 
		}
	}

	if .Data == "blockquote" {
		,  := markdownSnap(), markdownSnap(+)
		.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
			Kind: MarkdownRectPrimitive, X: markdownSnap(), Y: , Width: , Height:  - ,
			FillRole: MarkdownColorBorder,
		})
	}

	if .Data == "h1" || .Data == "h2" {
		 := 
		if  <= 0 {
			 = .width
		}
		,  := markdownSnap(), markdownSnap(+)
		 :=  +  - BorderBottom_h1_h2
		,  := markdownSnap(), markdownSnap(+BorderBottom_h1_h2)
		.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
			Kind: MarkdownRectPrimitive, X: , Y: ,
			Width:  - , Height:  - , FillRole: MarkdownColorBorderMuted,
		})
	}
	return 
}

type markdownFlowUnit struct {
	block  *html.Node
	inline []*html.Node
	attrs  blockAttrs
}

func ( *markdownPainter) ( *html.Node, ,  float64,  markdownPaintContext,  float64) float64 {
	 := .flowUnits(, )
	 := 
	 := 0.0
	for ,  := range  {
		if  == 0 && .Data == "body" && .block != nil && (.block.Data == "ul" || .block.Data == "ol") {
			// The root has no parent flow to consume a collapsed first-child
			// margin. Chromium still paints that margin inside the foreignObject
			// viewport when a top-level loose list's first li starts with p,
			// while D2's legacy outer height intentionally excludes it.
			 += .attrs.marginTop
		} else if  > 0 {
			 += goMax(, .attrs.marginTop)
		}
		 := .attrs.height
		if .block != nil {
			 = .paintBlock(.block, , , , )
		} else {
			 = goMax(, .paintInlineNodes(.inline, , , , ))
		}
		 += 
		 = .attrs.marginBottom
	}
	return  - 
}

func ( *markdownPainter) ( *html.Node,  markdownPaintContext) []markdownFlowUnit {
	var  []markdownFlowUnit
	var  []*html.Node
	 := blockAttrs{}
	 := .cssFontSize() * .lineHeightFactor

	 := func() {
		if len() == 0 || !.isNotEmpty() {
			 = nil
			 = blockAttrs{}
			return
		}
		.height = 
		 = append(, markdownFlowUnit{inline: , attrs: })
		 = nil
		 = blockAttrs{}
	}

	 := getNext(.FirstChild)
	 := getPrev(.LastChild)
	for  := .FirstChild;  != nil;  = .NextSibling {
		if .Type == html.ElementNode && isBlockElement(.Data) {
			()
			 := .measure(, )
			if  ==  && .Data == "blockquote" {
				.marginTop = 0
			}
			if  ==  && .Data == "blockquote" {
				.marginBottom = 0
			}
			 = append(, markdownFlowUnit{block: , attrs: })
			continue
		}
		if .Type == html.ElementNode && .Data == "br" {
			if .isNotEmpty() {
				()
			} else {
				 = append(, markdownFlowUnit{attrs: blockAttrs{height: }})
			}
			continue
		}

		 := .measure(, )
		 = append(, )
		if .isNotEmpty() {
			.width += .width
			.height = goMax(.height, .height)
			.marginTop = goMax(.marginTop, .marginTop)
			.marginBottom = goMax(.marginBottom, .marginBottom)
		}
	}
	()
	return 
}

type markdownInlineToken struct {
	breakLine            bool
	breakBefore          bool
	breakAfterSoftHyphen bool
	softHyphenEnd        bool
	discretionaryHyphen  bool
	padding              bool
	leadingPadding       bool
	text                 string
	width                float64
	height               float64
	ctx                  markdownPaintContext
	codeGroup            int
	codeHeight           float64
	decorationGroup      int
}

func ( *markdownPainter) ( []*html.Node, ,  float64,  markdownPaintContext,  float64) float64 {
	var  []markdownInlineToken
	 := 0
	 := 0
	for ,  := range  {
		.collectInlineTokens(, , &, &, &, 0, 0)
	}
	if len() == 0 {
		return 0
	}

	 := .wrapInlineTokens(, )
	 := 0.0
	for ,  := range  {
		 := .markdownInlineLineHeight(, )
		 :=  + 
		 := 
		if .textAlign != "" &&  > 0 {
			 := 0.0
			for ,  := range  {
				 += .width
			}
			 := goMax(0, -)
			if .textAlign == "center" {
				 +=  / 2
			} else if .textAlign == "right" {
				 += 
			}
		}
		 := 
		 := make(map[int][3]float64)
		 := make(map[int]float64)
		 := make(map[int][2]string)
		 := make(map[int]MarkdownPrimitive)
		for ,  := range  {
			if .codeGroup > 0 {
				,  := [.codeGroup]
				if ! {
					 = [3]float64{, , .codeHeight}
					[.codeGroup] = .inlineCodeVerticalShift(, .ctx, , .codeHeight)
					[.codeGroup] = [2]string{.ctx.link, .ctx.linkTitle}
				}
				[1] =  + .width
				[2] = goMax([2], .codeHeight)
				[.codeGroup] = 
			}
			if .decorationGroup > 0 {
				,  := [.decorationGroup]
				if ! {
					 = MarkdownPrimitive{
						Kind: MarkdownRectPrimitive, X: ,
						Y: markdownSnap( + /2), Height: 1,
						FillRole: .ctx.decorationRole,
					}
				}
				.Width =  + .width - .X
				[.decorationGroup] = 
			}
			 += .width
		}
		 := make([]int, 0, len())
		for  := range  {
			 = append(, )
		}
		sort.Ints()
		for ,  := range  {
			 := []
			 := [2]
			.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
				Kind: MarkdownRectPrimitive, X: [0], Y:  + (-)/2 + [],
				Width: [1] - [0], Height: , Radius: 6, FillRole: MarkdownColorNeutralMuted,
				Link: [][0], LinkTitle: [][1],
			})
		}

		 = 
		for ,  := range  {
			if .padding {
				 += .width
				continue
			}
			 := .ctx.cssFontSize()
			 := .ctx.fontScale
			if .ctx.inlineCode && !.ctx.headingCode {
				 *= FontSize_pre_code_em
				 *= FontSize_pre_code_em
			}
			 := .textBaseline(.ctx, , , )
			if .ctx.inlineCode && .codeHeight > 0 {
				 :=  + (-.codeHeight)/2
				 := 0.0
				if !.ctx.headingCode {
					 = PaddingTopBottom_code_em * 
				}
				 :=  + 
				 := goMax(0, .codeHeight-2*)
				 = .textBaseline(.ctx, , , )
				 += [.codeGroup]
			}
			 -= markdownHeadingInkCorrection(.ctx)
			.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
				Kind: MarkdownTextPrimitive, X: , Y: , Width: .width, Height: .height,
				Text: .text, Font: .ctx.fontRole(), FontSize: ,
				FillRole: .ctx.role, Link: .ctx.link, LinkTitle: .ctx.linkTitle,
				SyntheticBold: .ctx.syntheticBold(), SyntheticItalic: .ctx.syntheticItalic(),
				TextLength: .discretionaryHyphen,
			})
			 += .width
		}
		 := make([]int, 0, len())
		for  := range  {
			 = append(, )
		}
		sort.Ints()
		for ,  := range  {
			.layout.Primitives = append(.layout.Primitives, [])
		}
		 += 
	}
	return 
}

func ( *markdownPainter) ( []markdownInlineToken,  markdownPaintContext) float64 {
	 := .cssFontSize() * .lineHeightFactor
	if !.tableCell {
		return 
	}
	 := 
	 := false
	 := true
	 := false
	 := false
	 := false
	for ,  := range  {
		if .padding || .text == "" {
			continue
		}
		 = true
		if !.ctx.inlineCode {
			 := .ctx.fontFamily
			if .ctx.mono {
				 = .ctx.monoFontFamily
			}
			 := .Font(.ctx.fontSize, .ctx.effectiveFontStyle())
			 := strings.ReplaceAll(.text, "\u00ad", "")
			if ,  := .ruler.measureFontAdvance(, ); ! {
				 := uniseg.NewGraphemes()
				for .Next() {
					 := .Str()
					if ,  := .ruler.measureFontAdvance(, );  {
						continue
					}
					 := isColorEmojiGrapheme()
					if ! {
						 := stripDefaultIgnorableRunes()
						if  !=  {
							if  == "" {
								continue
							}
							if ,  := .ruler.measureFontAdvance(, );  {
								continue
							}
							 = 
						}
					}
					 = true
					if  {
						 = true
					} else if hasUnicodeSymbol() {
						 = true
					}
				}
			}
			 = false
			continue
		}
		if .inlineCode == false {
			 = .ctx
		}
	}
	 =  && 
	if ! {
		 = 
	}
	 := .fontFamily
	if .mono {
		 = .monoFontFamily
	}
	 := .cssFontSize()
	if  {
		 *= FontSize_pre_code_em
	}
	 := .Font(.fontSize, .effectiveFontStyle())
	if ,  := .ruler.cssNormalLineHeight(, );  {
		 = 
	}
	if  {
		// Chromium's normal inline-formatting strut leaves two pixels of line
		// gap around Source Code Pro at the 85% code size.
		 = goMax(, math.Round(*1.4))
	}
	if  && ! {
		// Chromium's color-emoji fallback uses a 26px normal line box at 16px.
		 = goMax(, math.Round(.cssFontSize()*1.625))
	} else if  && ! {
		// Text-presentation emoji-capable symbols use a 24px fallback line box.
		 = goMax(, math.Round(.cssFontSize()*1.5))
	} else if  && ! {
		// Host fallback fonts use a slightly taller normal line box for missing
		// proportional glyphs (22px versus Source Sans Pro's 20px at 16px).
		 = goMax(, math.Round(.cssFontSize()*1.375))
	}
	return 
}

// HTML line boxes at h2's 24px and h5's 14px font sizes expose ink one pixel
// above the baseline produced by rounded OpenType font-box metrics. This is a
// paint-only correction: list markers still align to the shared CSS baseline.
func markdownHeadingInkCorrection( markdownPaintContext) float64 {
	if .heading == "h5" || (.heading == "h2" && *.fontFamily == d2fonts.SourceSansPro) {
		return 1
	}
	return 0
}

// wrapInlineTokens implements CSS white-space wrapping at Unicode line-break
// opportunities, with overflow-wrap fallbacks at inline element boundaries.
// The outer viewport remains D2's legacy measured box: extra lines are painted
// at the browser line height and intentionally clip when historical measurement
// underestimated styled content.
func ( *markdownPainter) ( []markdownInlineToken,  float64) [][]markdownInlineToken {
	 := [][]markdownInlineToken{{}}
	 := 0.0
	 := false
	for ,  := range  {
		if .breakLine {
			 = append(, nil)
			 = 0
			continue
		}
		[len()-1] = append([len()-1], )
		 += .width
		 =  ||  > 0 &&  > +0.001
	}
	if ! {
		return 
	}

	 := .atomizeInlineTokens()

	 = [][]markdownInlineToken{{}}
	 = 0
	var  *markdownInlineToken
	var  []markdownInlineToken
	 := 0.0
	 := false
	for  := 0;  < len(); ++ {
		 := []
		if .breakLine {
			 = nil
			 = nil
			 = 0
			 = append(, nil)
			 = 0
			 = false
			continue
		}
		if .text == " " && !.padding {
			 := 
			 = &
			continue
		}
		if .padding && .leadingPadding {
			 = append(, )
			 += .width
			continue
		}

	:
		 := inlineTrailingPaddingWidth(, )
		 := inlineUnbrokenWidth(, ) + 
		if  != nil {
			 += .width
		}
		 := .breakBefore ||  != nil
		if  > 0 &&  && !.padding &&  && + > +0.001 {
			if .breakAfterSoftHyphen {
				 := [len()-1]
				for  := len() - 1;  >= 0; -- {
					if ![].softHyphenEnd {
						continue
					}
					[].softHyphenEnd = false
					 := []
					.text = "-"
					.width = .ctx.cssFontSize() / 3
					.softHyphenEnd = false
					.discretionaryHyphen = true
					.breakBefore = false
					.breakAfterSoftHyphen = false
					 = append(, markdownInlineToken{})
					copy([+2:], [+1:])
					[+1] = 
					 += .width
					[len()-1] = 
					break
				}
			}
			 = append(, nil)
			 = 0
			 = false
			 = nil
		}

		 :=  + 
		if  != nil &&  {
			 += .width
		}
		if  > 0 && !.padding && +.width+ > +0.001 {
			 := goMax(0, -)
			, ,  := .splitInlineTokenAtWidth(, ,  > 0)
			if ! &&  {
				// overflow-wrap: break-word falls back to the preceding grapheme
				// boundary when no normal Unicode opportunity is available.
				 = append(, nil)
				 = 0
				 = false
				 = nil
				goto 
			}
			if  {
				 = 
				 = append(, markdownInlineToken{})
				copy([+2:], [+1:])
				[+1] = 
			}
		}

		if  != nil &&  {
			[len()-1] = append([len()-1], *)
			 += .width
			 = nil
		} else if  != nil {
			// CSS discards leading collapsible whitespace, but inline-box
			// padding still belongs to the first painted fragment.
			 = nil
		}
		[len()-1] = append([len()-1], ...)
		 += 
		 = nil
		 = 0

		[len()-1] = append([len()-1], )
		 += .width
		if !.padding {
			 = true
		}
	}
	return 
}

func inlineTrailingPaddingWidth( []markdownInlineToken,  int) float64 {
	 := 0.0
	for  :=  + 1;  < len() && [].padding && ![].leadingPadding; ++ {
		 += [].width
	}
	return 
}

// inlineUnbrokenWidth looks through style/link token boundaries until the next
// real Unicode break. CSS decides whether to move the whole word before laying
// out its individual styled runs; considering only the first run can leave half
// a word on the preceding line.
func inlineUnbrokenWidth( []markdownInlineToken,  int) float64 {
	 := 0.0
	for  := ;  < len(); ++ {
		 := []
		if .breakLine || ( >  && !.padding && (.breakBefore || .text == " ")) {
			break
		}
		if  ==  && .text == " " && !.padding {
			break
		}
		 += .width
	}
	return 
}

func ( *markdownPainter) ( markdownInlineToken,  float64,  bool) (,  markdownInlineToken,  bool) {
	 := uniseg.NewGraphemes(.text)
	var  []int
	for .Next() {
		,  := .Positions()
		 = append(, )
	}
	if len() < 2 {
		return markdownInlineToken{}, markdownInlineToken{}, false
	}

	 := -1
	for ,  := range  {
		 := .inlineTextAdvance(.text[:], .ctx, .width)
		if  > +0.001 {
			break
		}
		 = 
	}
	if  < 0 {
		return markdownInlineToken{}, markdownInlineToken{}, false
	}
	if  == len()-1 {
		if ! {
			return markdownInlineToken{}, markdownInlineToken{}, false
		}
		--
		if  < 0 {
			return markdownInlineToken{}, markdownInlineToken{}, false
		}
	}

	 := []
	 = 
	.text = .text[:]
	.width = .inlineTextAdvance(.text, .ctx, .width)
	.softHyphenEnd = false

	 = 
	.text = .text[:]
	.width = .inlineTextAdvance(.text, .ctx, .width)
	.breakBefore = false
	.breakAfterSoftHyphen = false
	return , , true
}

// atomizeInlineTokens splits text only at Unicode line-break opportunities.
// Computing boundaries across adjacent styled runs is important: an element
// boundary alone is not a valid place to wrap a Latin word. The split tokens
// retain their original paint context, code group, and decoration group.
func ( *markdownPainter) ( []markdownInlineToken) []markdownInlineToken {
	var  []markdownInlineToken
	for  := 0;  < len(); {
		if [].breakLine {
			 = append(, [])
			++
			continue
		}

		 := 
		var  strings.Builder
		for  < len() && ![].breakLine {
			if ![].padding {
				.WriteString([].text)
			}
			++
		}

		 := make(map[int]bool)
		 := .String()
		// Unicode line-breaking and grapheme segmentation are related but
		// distinct algorithms. Never accept a line-break offset that falls
		// inside an extended grapheme cluster (notably emoji ZWJ sequences),
		// because SVG text would otherwise paint the pieces as separate glyphs.
		 := map[int]bool{0: true}
		 := uniseg.NewGraphemes()
		 := 0
		for .Next() {
			 += len(.Str())
			[] = true
		}
		 := 
		 := -1
		 := 0
		for  != "" {
			, , ,  := uniseg.FirstLineSegmentInString(, )
			if  == "" {
				break
			}
			 += len()
			if  != "" && [] {
				[] = true
			}
			 = 
			 = 
		}

		 := 0
		for ,  := range [:] {
			if .padding || .text == "" {
				 = append(, )
				continue
			}

			 := 
			 :=  + len(.text)
			 := 
			for  :=  + 1;  <= ; ++ {
				if  !=  && ![] {
					continue
				}
				 := .text[- : -]
				 := 
				.breakBefore = []
				.breakAfterSoftHyphen = .breakBefore && strings.HasSuffix([:], "\u00ad")
				if strings.HasSuffix(, "\u00ad") {
					 = strings.TrimSuffix(, "\u00ad")
					.softHyphenEnd = true
				}
				.appendInlineTextAtoms(&, , )
				 = 
			}
			 = 
		}
		 = 
	}
	return 
}

func ( *markdownPainter) ( *[]markdownInlineToken,  markdownInlineToken,  string) {
	if  == "" && .softHyphenEnd {
		.width = 0
		* = append(*, )
		return
	}
	 := strings.TrimRight(, " ")
	if  != "" {
		 := 
		.text = 
		.width = .inlineTextAdvance(, .ctx, .width)
		* = append(*, )
		.breakBefore = false
	}
	for range len() - len() {
		 := 
		.text = " "
		.width = .inlineTextAdvance(" ", .ctx, .width)
		* = append(*, )
		.breakBefore = false
	}
}

func ( *markdownPainter) (,  markdownPaintContext, ,  float64) float64 {
	if .tableCell {
		return 0
	}
	 := .fontScale
	 := .cssFontSize()
	 := 0.0
	if !.headingCode {
		 *= FontSize_pre_code_em
		 *= FontSize_pre_code_em
		 = PaddingTopBottom_code_em * 
	}
	 := (-)/2 + 
	 := goMax(0, -2*)
	 := .textBaseline(, , , )
	 := .textBaseline(, 0, , .fontScale)
	return  - 
}

func ( *markdownPainter) ( *html.Node,  markdownPaintContext,  *[]markdownInlineToken, ,  *int,  int,  float64) {
	if .Type == html.TextNode {
		 := renderableMarkdownText()
		 := .measure(, )
		if  == "" && .width == 0 {
			return
		}
		 := .inlineTextAdvance(, , .width)
		* = append(*, markdownInlineToken{
			text: , width: , height: .height, ctx: ,
			codeGroup: , codeHeight: , decorationGroup: .decorationGroup,
		})
		return
	}
	if .Type != html.ElementNode {
		return
	}
	if .Data == "br" {
		if !.inlineCode {
			* = append(*, markdownInlineToken{breakLine: true})
		}
		return
	}
	if .Data == "img" {
		return
	}

	 := .childContext(, )
	if .decoration != MarkdownTextDecorationNone && .decoration == MarkdownTextDecorationNone {
		*++
		.decorationGroup = *
		.decorationRole = .role
	}
	if .Data == "code" && (.Parent == nil || .Parent.Data != "pre") {
		*++
		 = *
		 := .measure(, )
		 = .height
		 := inlineCodeHorizontalPadding()
		* = append(*, markdownInlineToken{
			padding: true, leadingPadding: true, width: , ctx: , codeGroup: , codeHeight: , decorationGroup: .decorationGroup,
		})
	}
	for  := .FirstChild;  != nil;  = .NextSibling {
		.(, , , , , , )
	}
	if .Data == "code" && (.Parent == nil || .Parent.Data != "pre") {
		 := inlineCodeHorizontalPadding()
		* = append(*, markdownInlineToken{
			padding: true, width: , ctx: , codeGroup: , codeHeight: , decorationGroup: .decorationGroup,
		})
	}
}

func inlineCodeHorizontalPadding( markdownPaintContext) float64 {
	if .headingCode {
		return PaddingLeftRight_heading_code_em * .cssFontSize()
	}
	return PaddingLeftRight_code_em * FontSize_pre_code_em * .cssFontSize()
}

func ( *markdownPainter) ( string,  markdownPaintContext,  float64) float64 {
	 := .fontFamily
	if .mono {
		 = .monoFontFamily
	}
	 := .Font(.fontSize, .effectiveFontStyle())
	 := strings.ReplaceAll(, "\u00ad", "")
	,  := .ruler.measureFontAdvance(, )
	if ! {
		, _ = .ruler.MeasurePrecise(, )
		 = .ruler.scaleUnicodeCSS(, , )
		if  == 0 &&  != "" && !isZeroWidthGrapheme() {
			return 
		}
	}
	 *= .fontScale
	if .inlineCode && !.headingCode {
		 *= FontSize_pre_code_em
	}
	return 
}

func renderableMarkdownText( *html.Node) string {
	return .Data
}

func ( *markdownPainter) ( *html.Node, ,  float64,  blockAttrs,  markdownPaintContext,  float64) float64 {
	 := goMax(.width, )
	.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
		Kind: MarkdownRectPrimitive, X: , Y: , Width: , Height: .height,
		Radius: 6, FillRole: MarkdownColorCanvasSubtle,
	})
	 := markdownNodeText()
	var  []string
	if  != "" {
		 = strings.Split(, "\n")
	}
	if len() > 0 && [len()-1] == "" && strings.HasSuffix(, "\n") {
		 = [:len()-1]
	}
	 := FontSize_pre_code_em * .cssFontSize()
	 := LineHeight_pre * 
	 := float64(len())* + 2*Padding_pre
	if len() == 0 {
		 = 2 * Padding_pre
	}
	.layout.Primitives[len(.layout.Primitives)-1].Height = 
	for ,  := range  {
		if  == "" {
			continue
		}
		 = expandMarkdownTabs()
		.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
			Kind: MarkdownTextPrimitive, X:  + Padding_pre,
			Y:    .textBaseline(, +Padding_pre+float64()*, , FontSize_pre_code_em*.fontScale),
			Text: , Font: .fontRole(), FontSize: , FillRole: .role,
			SyntheticBold: .syntheticBold(), SyntheticItalic: .syntheticItalic(),
		})
	}
	return 
}

func expandMarkdownTabs( string) string {
	if !strings.ContainsRune(, '\t') {
		return 
	}
	var  strings.Builder
	 := 0
	 := uniseg.NewGraphemes()
	for .Next() {
		 := .Str()
		if  == "\t" {
			 := TAB_SIZE - %TAB_SIZE
			.WriteString(strings.Repeat(" ", ))
			 += 
			continue
		}
		.WriteString()
		 := .Width()
		if  < 1 {
			 = 1
		}
		 += 
	}
	return .String()
}

type markdownTableRow struct {
	cells   []*html.Node
	header  bool
	height  float64
	widths  []float64
	heights []float64
	stripe  bool
}

func ( *markdownPainter) ( *html.Node, ,  float64,  blockAttrs,  markdownPaintContext,  float64) float64 {
	 := collectMarkdownTableRows()
	 := make([]float64, 0)
	 := make([]float64, 0)
	for  := range  {
		 := make([]float64, 0, len([].cells))
		for ,  := range [].cells {
			 := markdownTableCellContext(, [], )
			 := .measure(, )
			[].widths = append([].widths, .width+26)
			 = append(, .markdownMinContentWidth(, )+26)
		}
		 = mergeColumnWidths(, [][]float64{[].widths})
		 = mergeColumnWidths(, [][]float64{})
	}
	 := .width
	if  > 0 {
		 = math.Min(, )
	}
	 = goMax(1, )
	 = fitMarkdownTableColumns(, , )
	 := float64(len() + 1)
	for ,  := range  {
		 += 
	}
	 = goMax(, )

	for  := range  {
		 := 0.0
		for ,  := range [].cells {
			 := markdownTableCellContext(, [], )
			 := goMax(0, []-26)
			 := markdownPainter{ruler: .ruler, layout: &MarkdownLayout{}}
			 := .paintFlow(, 0, 0, , )
			[].heights = append([].heights, )
			 = goMax(, +12)
		}
		// Empty cells have only their 6px vertical padding and the collapsed
		// one-pixel row border. Chromium therefore paints an empty row at 13px.
		[].height =  + 1
	}
	 := 
	for ,  := range  {
		 += .height
	}
	 := markdownSnap()
	 := markdownSnap()
	 := markdownSnap( +  - 1)
	 := markdownSnap()
	 :=  -  + 1
	 :=  -  + 1

	.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
		Kind: MarkdownRectPrimitive, X: , Y: , Width: , Height: ,
		FillRole: MarkdownColorCanvas,
	})
	 := 
	for ,  := range  {
		if .stripe {
			 := markdownSnap()
			 := markdownSnap( + .height)
			.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
				Kind: MarkdownRectPrimitive, X:  + 1, Y: , Width: goMax(0, --1), Height:  - ,
				FillRole: MarkdownColorCanvasSubtle,
			})
		}
		 += .height
	}
	 := []MarkdownPrimitive{
		{
			Kind: MarkdownRectPrimitive, X: , Y: , Width: , Height: 1,
			FillRole: MarkdownColorBorder,
		},
		{
			Kind: MarkdownRectPrimitive, X: , Y: , Width: 1, Height: ,
			FillRole: MarkdownColorBorder,
		},
		{
			Kind: MarkdownRectPrimitive, X: , Y: , Width: 1, Height: ,
			FillRole: MarkdownColorBorder,
		},
	}
	 := 
	for ,  := range  {
		 :=  + 1
		for ,  := range .cells {
			 := markdownTableCellContext(, , )
			 :=  + 13
			 := [] - 26
			 := 0.0
			if  < len(.heights) {
				 = .heights[]
			}
			 :=  + 7 + goMax(0, .height-13-)/2
			// The collapsed top border occupies the first CSS pixel of each row;
			// cell padding starts after it. Table cells vertically center content
			// when another cell makes the shared row taller.
			.paintFlow(, , , , )
			 += []
			if  < len(.cells)-1 {
				 := markdownSnap()
				 := markdownSnap( + .height)
				 = append(, MarkdownPrimitive{
					Kind: MarkdownRectPrimitive, X: markdownSnap(), Y: , Width: 1, Height:  - ,
					FillRole: MarkdownColorBorder,
				})
			}
			++
		}
		 += .height
		 = append(, MarkdownPrimitive{
			Kind: MarkdownRectPrimitive, X: , Y: markdownSnap(), Width: , Height: 1,
			FillRole: MarkdownColorBorder,
		})
	}
	.layout.Primitives = append(.layout.Primitives, ...)
	return 
}

// markdownMinContentWidth returns the narrowest width allowed by ordinary CSS
// line-break opportunities. In particular, overflow-wrap: break-word does not
// reduce an element's min-content contribution, while spaces and CJK boundaries
// do. Inline padding stays attached to the first or last fragment it surrounds.
func ( *markdownPainter) ( *html.Node,  markdownPaintContext) float64 {
	 := 0.0
	for ,  := range .flowUnits(, ) {
		if .block != nil {
			 = goMax(, .measure(.block, ).width)
			continue
		}
		var  []markdownInlineToken
		 := 0
		 := 0
		for ,  := range .inline {
			.collectInlineTokens(, , &, &, &, 0, 0)
		}
		 := .atomizeInlineTokens()
		 := 0.0
		 := 0.0
		var  *markdownPaintContext
		 := func( float64) {
			 = goMax(, +)
			 = 0
			 = nil
		}
		for ,  := range  {
			if .breakLine {
				 += 
				 = 0
				(0)
				continue
			}
			if .text == " " && !.padding {
				 += 
				 = 0
				(0)
				continue
			}
			if .padding && .leadingPadding {
				 += .width
				continue
			}
			if !.padding && .breakBefore {
				 := 0.0
				if .breakAfterSoftHyphen &&  != nil {
					 = .cssFontSize() / 3
				}
				()
			}
			 += 
			 = 0
			 += .width
			if !.padding {
				 := .ctx
				 = &
			}
		}
		 += 
		(0)
	}
	return 
}

func markdownTableCellContext( markdownPaintContext,  markdownTableRow,  *html.Node) markdownPaintContext {
	.tableCell = true
	// The browser UA stylesheet resets tables to line-height: normal. Source
	// Sans Pro's normal 16px line box is 20px rather than Markdown's 24px.
	.lineHeightFactor = 1.25
	if .header || .Data == "th" {
		.fontStyle = d2fonts.FONT_STYLE_SEMIBOLD
		.bold = true
	}
	.textAlign = nodeAttr(, "align")
	if .textAlign == "" && (.header || .Data == "th") {
		.textAlign = "center"
	}
	return 
}

func fitMarkdownTableColumns(,  []float64,  float64) []float64 {
	if len() == 0 {
		return 
	}
	 := append([]float64(nil), ...)
	 := goMax(0, -float64(len()+1))
	 := 0.0
	for ,  := range  {
		 += 
	}
	if  <= +0.001 {
		return 
	}
	 := 0.0
	 := 0.0
	for ,  := range  {
		 := 0.0
		if  < len() {
			 = math.Min(, [])
		}
		 += 
		 +=  - 
	}
	if  <= +0.001 &&  > 0 {
		 :=  - 
		for ,  := range  {
			 := 0.0
			if  < len() {
				 = math.Min(, [])
			}
			 :=  - 
			[] =  - */
		}
		return 
	}
	if  > +0.001 {
		// CSS automatic table layout does not compress columns below their
		// min-content widths. The row can overflow a max-width-constrained table.
		for ,  := range  {
			if  < len() {
				[] = math.Min(, [])
			}
		}
		return 
	}
	 := 26.0 * float64(len())
	if  <=  {
		for  := range  {
			[] =  / float64(len())
		}
		return 
	}
	 :=  - 
	 := 0.0
	for ,  := range  {
		 += goMax(0, -26)
	}
	if  == 0 {
		for  := range  {
			[] = 26 + /float64(len())
		}
		return 
	}
	for ,  := range  {
		 := goMax(0, -26) / 
		[] = 26 + *
	}
	return 
}

func collectMarkdownTableRows( *html.Node) []markdownTableRow {
	var  []markdownTableRow
	 := func( *html.Node, ,  bool) {
		 := markdownTableRow{header: , stripe: }
		for  := .FirstChild;  != nil;  = .NextSibling {
			if .Type == html.ElementNode && (.Data == "td" || .Data == "th") {
				.cells = append(.cells, )
				if .Data == "th" {
					.header = true
				}
			}
		}
		 = append(, )
	}
	var  func(*html.Node, bool)
	 = func( *html.Node,  bool) {
		if .Type != html.ElementNode {
			return
		}
		if .Data == "thead" || .Data == "tbody" || .Data == "tfoot" || .Data == "table" {
			if .Data == "thead" {
				 = true
			}
			 := 0
			for  := .FirstChild;  != nil;  = .NextSibling {
				if .Type == html.ElementNode && .Data == "tr" {
					(, , %2 == 1)
					++
				} else {
					(, )
				}
			}
			return
		}
		for  := .FirstChild;  != nil;  = .NextSibling {
			(, )
		}
	}
	(, false)
	return 
}

func markdownListMarker( *html.Node) string {
	if .Parent == nil || .Parent.Type != html.ElementNode {
		return ""
	}
	if .Parent.Data == "ul" {
		// Browser list markers are geometric shapes, not font glyphs. They are
		// painted by paintListMarker and deliberately excluded from font corpus.
		return ""
	}
	 := ""
	if .Parent.Data == "ol" {
		 := 1
		if ,  := strconv.Atoi(nodeAttr(.Parent, "start"));  == nil {
			 = 
		}
		for  := .PrevSibling;  != nil;  = .PrevSibling {
			if .Type == html.ElementNode && .Data == "li" {
				++
			}
		}
		 := nodeAttr(.Parent, "type")
		if  == "" {
			 := 0
			for  := .Parent.Parent;  != nil;  = .Parent {
				if .Type == html.ElementNode && (.Data == "ol" || .Data == "ul") {
					++
				}
			}
			switch  {
			case 0:
				 = "1"
			case 1:
				 = "i"
			default:
				 = "a"
			}
		}
		 = orderedListMarker(, )
	}
	return 
}

func ( *markdownPainter) ( *html.Node,  markdownPaintContext) float64 {
	return PaddingLeft_ul_ol_em * .cssFontSize()
}

func ( *markdownPainter) ( *html.Node, ,  float64,  markdownPaintContext,  float64) {
	 := .listMarkerBaseline(, , )
	if .Parent != nil && .Parent.Type == html.ElementNode && .Parent.Data == "ul" {
		 := .cssFontSize()
		// Blink paints list markers as pixel-snapped geometry. Its disc/square
		// box is about .3em, with a three-pixel minimum at small font sizes.
		 := goMax(3, markdownSnap(0.3*))
		// CSS places outside markers in a marker box whose center sits about
		// one em before the list content. The 1.5px correction comes from the
		// browser marker box; another 5px aligns the geometric marker with that
		// box instead of the text-glyph origin used by the first native version.
		 :=  +  - /2 - 6.5
		 := MarkdownPrimitive{
			Kind: MarkdownRectPrimitive,
			X:    markdownSnap( - /2), Y:  - math.Floor(1.5*),
			Width: , Height: ,
		}
		 := markdownListDepth()
		switch  {
		case 0:
			.Radius =  / 2
			.FillRole = .role
		case 1:
			.Radius =  / 2
			.StrokeWidth = 1
			if .role == MarkdownColorMuted {
				.StrokeRole = MarkdownColorMutedStroke
			} else {
				.StrokeRole = MarkdownColorForegroundStroke
			}
		default:
			.FillRole = .role
		}
		.layout.Primitives = append(.layout.Primitives, )
		return
	}
	 := markdownListMarker()
	if  == "" {
		return
	}
	 := .fontFamily
	if .mono {
		 = .monoFontFamily
	}
	 := .Font(.fontSize, .effectiveFontStyle())
	,  := .ruler.MeasurePrecise(, )
	 *= .fontScale
	 *= .fontScale
	.layout.Primitives = append(.layout.Primitives, MarkdownPrimitive{
		Kind: MarkdownTextPrimitive,
		X:     +  -  - .ruler.spaceWidth()*.fontScale,
		Y:    , Width: , Height: ,
		Text: , Font: .fontRole(), FontSize: .cssFontSize(),
		FillRole: .role, SyntheticBold: .syntheticBold(), SyntheticItalic: .syntheticItalic(),
	})
}

// A list marker participates in the list item's first line box. When that line
// is introduced by a heading, its larger baseline pulls the marker down, but
// the marker itself still inherits the list item's (body) font size.
func ( *markdownPainter) ( *html.Node,  float64,  markdownPaintContext) float64 {
	 := .textBaseline(, , .cssFontSize()*.lineHeightFactor, .fontScale)
	 := getNext(.FirstChild)
	if  == nil || .Type != html.ElementNode || !isMarkdownHeading(.Data) {
		return 
	}
	 := .childContext(, )
	 := .textBaseline(
		,
		,
		.cssFontSize()*.lineHeightFactor,
		.fontScale,
	)
	return goMax(, )
}

func markdownListDepth( *html.Node) int {
	 := 0
	if  == nil || .Parent == nil {
		return 
	}
	for  := .Parent.Parent;  != nil;  = .Parent {
		if .Type == html.ElementNode && (.Data == "ol" || .Data == "ul") {
			++
		}
	}
	return 
}

func orderedListMarker( int,  string) string {
	switch strings.ToLower() {
	case "a":
		return lowerAlpha() + "."
	case "i":
		return lowerRoman() + "."
	default:
		return strconv.Itoa() + "."
	}
}

func lowerAlpha( int) string {
	if  <= 0 {
		return strconv.Itoa()
	}
	var  []byte
	for  > 0 {
		--
		 = append(, byte('a'+%26))
		 /= 26
	}
	for ,  := 0, len()-1;  < ; ,  = +1, -1 {
		[], [] = [], []
	}
	return string()
}

func lowerRoman( int) string {
	if  <= 0 ||  > 3999 {
		return strconv.Itoa()
	}
	 := []struct {
		 int
		  string
	}{
		{1000, "m"}, {900, "cm"}, {500, "d"}, {400, "cd"},
		{100, "c"}, {90, "xc"}, {50, "l"}, {40, "xl"},
		{10, "x"}, {9, "ix"}, {5, "v"}, {4, "iv"}, {1, "i"},
	}
	var  strings.Builder
	for ,  := range  {
		for  >= . {
			.WriteString(.)
			 -= .
		}
	}
	return .String()
}

func markdownNodeText( *html.Node) string {
	var  strings.Builder
	var  func(*html.Node)
	 = func( *html.Node) {
		if .Type == html.TextNode {
			.WriteString(.Data)
			return
		}
		if .Type == html.ElementNode && .Data == "br" {
			.WriteByte('\n')
			return
		}
		for  := .FirstChild;  != nil;  = .NextSibling {
			()
		}
	}
	()
	return .String()
}

func nodeAttr( *html.Node,  string) string {
	for ,  := range .Attr {
		if .Key ==  {
			return .Val
		}
	}
	return ""
}

func validateMarkdownSVGNodes( *html.Node) error {
	if .Type == html.ElementNode {
		switch .Data {
		case "html", "head", "body",
			"p", "h1", "h2", "h3", "h4", "h5", "h6",
			"blockquote", "ul", "ol", "li", "pre", "code",
			"em", "b", "strong", "del", "s", "strike", "a", "br", "hr", "img",
			"table", "thead", "tbody", "tfoot", "tr", "td", "th":
		default:
			return fmt.Errorf("native Markdown SVG does not support HTML element <%s>", .Data)
		}
	}
	for  := .FirstChild;  != nil;  = .NextSibling {
		if  := ();  != nil {
			return 
		}
	}
	return nil
}

func goMax(,  float64) float64 {
	if  >  {
		return 
	}
	return 
}

// textBaseline matches CSS line-box placement: half of the extra line-height
// sits above the rounded OpenType font box, followed by its ascent. This is the
// same box exposed by Canvas TextMetrics for D2's embedded fonts.
func ( *markdownPainter) ( markdownPaintContext, , ,  float64) float64 {
	 := .fontFamily
	if .mono {
		 = .monoFontFamily
	}
	 := .Font(.fontSize, .effectiveFontStyle())
	if , ,  := .ruler.cssFontBoxMetrics(, float64(.fontSize)*);  {
		 := goMax(0, --)
		return  + /2 + 
	}
	if ,  := .ruler.atlases[]; ! {
		.ruler.addFontSize()
	}
	 := .ruler.atlases[]
	 := .lineHeight * 
	 := goMax(0, -)
	return  + /2 + .Ascent()*
}