From 118f466290e1678d5f405f7ef1a2bb0745102e9a Mon Sep 17 00:00:00 2001 From: Amir Alavi Date: Wed, 18 Feb 2026 22:36:40 -0500 Subject: [PATCH] chore: upgrade github.com/gomarkdown/markdown to latest version Upgrade github.com/gomarkdown/markdown from v0.0.0-20240328165702-4d01890c35c0 to v0.0.0-20260217112301-37c66b85d6ab (latest as of 2026-02-17) --- go.mod | 2 +- go.sum | 4 +- .../gomarkdown/markdown/html/renderer.go | 47 ++++--- .../gomarkdown/markdown/parser/block.go | 124 ++++++++++++------ .../gomarkdown/markdown/parser/citation.go | 3 + .../gomarkdown/markdown/parser/inline.go | 103 +++++++++++---- .../gomarkdown/markdown/parser/parser.go | 39 +++++- vendor/modules.txt | 2 +- 8 files changed, 227 insertions(+), 97 deletions(-) diff --git a/go.mod b/go.mod index 83a38fccd..8d1e15efe 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect + github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect diff --git a/go.sum b/go.sum index b6d3646ee..fe0e3b9bc 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= -github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= diff --git a/vendor/github.com/gomarkdown/markdown/html/renderer.go b/vendor/github.com/gomarkdown/markdown/html/renderer.go index 494e7540b..8e2c1d1dc 100644 --- a/vendor/github.com/gomarkdown/markdown/html/renderer.go +++ b/vendor/github.com/gomarkdown/markdown/html/renderer.go @@ -641,14 +641,29 @@ func (r *Renderer) EnsureUniqueHeadingID(id string) string { return id } -func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) { +func (r *Renderer) MakeUniqueHeadingID(hdr *ast.Heading) string { + if hdr.HeadingID == "" { + return "" + } + id := r.EnsureUniqueHeadingID(hdr.HeadingID) + if r.Opts.HeadingIDPrefix != "" { + id = r.Opts.HeadingIDPrefix + id + } + if r.Opts.HeadingIDSuffix != "" { + id = id + r.Opts.HeadingIDSuffix + } + hdr.HeadingID = id + return id +} + +func (r *Renderer) HeadingEnter(w io.Writer, hdr *ast.Heading) { var attrs []string var class string // TODO(miek): add helper functions for coalescing these classes. - if nodeData.IsTitleblock { + if hdr.IsTitleblock { class = "title" } - if nodeData.IsSpecial { + if hdr.IsSpecial { if class != "" { class += " special" } else { @@ -659,35 +674,29 @@ func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) { attrs = []string{`class="` + class + `"`} } - if nodeData.HeadingID != "" { - id := r.EnsureUniqueHeadingID(nodeData.HeadingID) - if r.Opts.HeadingIDPrefix != "" { - id = r.Opts.HeadingIDPrefix + id - } - if r.Opts.HeadingIDSuffix != "" { - id = id + r.Opts.HeadingIDSuffix - } + if hdr.HeadingID != "" { + id := r.MakeUniqueHeadingID(hdr) attrID := `id="` + id + `"` attrs = append(attrs, attrID) } - attrs = append(attrs, BlockAttrs(nodeData)...) + attrs = append(attrs, BlockAttrs(hdr)...) r.CR(w) - r.OutTag(w, HeadingOpenTagFromLevel(nodeData.Level), attrs) + r.OutTag(w, HeadingOpenTagFromLevel(hdr.Level), attrs) } -func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) { - r.Outs(w, HeadingCloseTagFromLevel(heading.Level)) - if !(IsListItem(heading.Parent) && ast.GetNextNode(heading) == nil) { +func (r *Renderer) HeadingExit(w io.Writer, hdr *ast.Heading) { + r.Outs(w, HeadingCloseTagFromLevel(hdr.Level)) + if !(IsListItem(hdr.Parent) && ast.GetNextNode(hdr) == nil) { r.CR(w) } } // Heading writes ast.Heading node -func (r *Renderer) Heading(w io.Writer, node *ast.Heading, entering bool) { +func (r *Renderer) Heading(w io.Writer, hdr *ast.Heading, entering bool) { if entering { - r.headingEnter(w, node) + r.HeadingEnter(w, hdr) } else { - r.headingExit(w, node) + r.HeadingExit(w, hdr) } } diff --git a/vendor/github.com/gomarkdown/markdown/parser/block.go b/vendor/github.com/gomarkdown/markdown/parser/block.go index 4b7326e7d..e0b99604e 100644 --- a/vendor/github.com/gomarkdown/markdown/parser/block.go +++ b/vendor/github.com/gomarkdown/markdown/parser/block.go @@ -76,6 +76,7 @@ var ( "output": {}, "progress": {}, "section": {}, + "svg": {}, "video": {}, } ) @@ -925,7 +926,7 @@ func syntaxRange(data []byte, iout *int) (int, int) { i++ } else { - for i < n && !IsSpace(data[i]) { + for i < n && data[i] != '\n' { syn++ i++ } @@ -950,8 +951,6 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int { work.WriteByte('\n') for { - // safe to assume beg < len(data) - // check for the end of the code block fenceEnd, _ := isFenceLine(data[beg:], nil, marker) if fenceEnd != 0 { @@ -968,48 +967,47 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int { } // verbatim copy to the working buffer - if doRender { - work.Write(data[beg:end]) - } + work.Write(data[beg:end]) beg = end } - if doRender { - codeBlock := &ast.CodeBlock{ - IsFenced: true, - } - codeBlock.Content = work.Bytes() // TODO: get rid of temp buffer + if !doRender { + return beg + } + codeBlock := &ast.CodeBlock{ + IsFenced: true, + } + codeBlock.Content = work.Bytes() // TODO: get rid of temp buffer - if p.extensions&Mmark == 0 { - p.AddBlock(codeBlock) - finalizeCodeBlock(codeBlock) - return beg - } - - // Check for caption and if found make it a figure. - if captionContent, id, consumed := p.caption(data[beg:], []byte(captionFigure)); consumed > 0 { - figure := &ast.CaptionFigure{} - caption := &ast.Caption{} - figure.HeadingID = id - p.Inline(caption, captionContent) - - p.AddBlock(figure) - codeBlock.AsLeaf().Attribute = figure.AsContainer().Attribute - p.addChild(codeBlock) - finalizeCodeBlock(codeBlock) - p.addChild(caption) - p.Finalize(figure) - - beg += consumed - - return beg - } - - // Still here, normal block + if p.extensions&Mmark == 0 { p.AddBlock(codeBlock) finalizeCodeBlock(codeBlock) + return beg } + // Check for caption and if found make it a figure. + if captionContent, id, consumed := p.caption(data[beg:], []byte(captionFigure)); consumed > 0 { + figure := &ast.CaptionFigure{} + caption := &ast.Caption{} + figure.HeadingID = id + p.Inline(caption, captionContent) + + p.AddBlock(figure) + codeBlock.AsLeaf().Attribute = figure.AsContainer().Attribute + p.addChild(codeBlock) + finalizeCodeBlock(codeBlock) + p.addChild(caption) + p.Finalize(figure) + + beg += consumed + + return beg + } + + // Still here, normal block + p.AddBlock(codeBlock) + finalizeCodeBlock(codeBlock) + return beg } @@ -1352,6 +1350,7 @@ func finalizeList(list *ast.List) { // Parse a single list item. // Assumes initial prefix is already removed if this is a sublist. func (p *Parser) listItem(data []byte, flags *ast.ListType) int { + isDefinitionList := *flags&ast.ListTypeDefinition != 0 // keep track of the indentation of the first line itemIndent := 0 if data[0] == '\t' { @@ -1384,7 +1383,7 @@ func (p *Parser) listItem(data []byte, flags *ast.ListType) int { } if i == 0 { // if in definition list, set term flag and continue - if *flags&ast.ListTypeDefinition != 0 { + if isDefinitionList { *flags |= ast.ListTypeTerm } else { return 0 @@ -1410,6 +1409,9 @@ func (p *Parser) listItem(data []byte, flags *ast.ListType) int { // process the following lines containsBlankLine := false sublist := 0 + // track fenced code blocks inside list items so that lines within + // the fence are gathered verbatim (not misinterpreted as list items) + fenceMarker := "" gatherlines: for line < len(data) { @@ -1443,9 +1445,49 @@ gatherlines: chunk := data[line+indentIndex : i] + // track fenced code blocks inside list items; + // only track fences that are indented (part of the list item content), + // a fence at indent 0 ends the list (handled below) + if !isDefinitionList && p.extensions&FencedCode != 0 { + if fenceMarker != "" { + if indent == 0 { + // non-indented line while inside a fence means we + // left the list item content -- abandon the fence + fenceMarker = "" + } else { + // inside a fence: check for closing fence + _, marker := isFenceLine(chunk, nil, fenceMarker) + if marker != "" { + fenceMarker = "" + } + // gather the line verbatim, skip structure detection + if containsBlankLine { + containsBlankLine = false + raw.WriteByte('\n') + } + raw.Write(chunk) + line = i + continue + } + } else if indent > 0 { + // not inside a fence: check for opening fence (indented only) + _, marker := isFenceLine(chunk, nil, "") + if marker != "" { + fenceMarker = marker + } + } + } + // If there is a fence line (marking starting of a code block) // without indent do not process it as part of the list. - if p.extensions&FencedCode != 0 { + // + // does not apply for definition lists because it causes infinite + // loop if text before defintion term is fenced code block start + // marker but not part of actual fenced code block + // for defnition lists we're called after parsing fence code blocks + // so we kno this cannot be a fenced block + // https://github.com/gomarkdown/markdown/issues/326 + if !isDefinitionList && p.extensions&FencedCode != 0 { fenceLineEnd, _ := isFenceLine(chunk, nil, "") if fenceLineEnd > 0 && indent == 0 { *flags |= ast.ListItemEndOfList @@ -1652,7 +1694,9 @@ func (p *Parser) paragraph(data []byte) int { if p.extensions&DefinitionLists != 0 { if i < len(data)-1 && data[i+1] == ':' { listLen := p.list(data[prev:], ast.ListTypeDefinition, 0, '.') - return prev + listLen + if listLen > 0 { + return prev + listLen + } } } diff --git a/vendor/github.com/gomarkdown/markdown/parser/citation.go b/vendor/github.com/gomarkdown/markdown/parser/citation.go index 217d4def0..5790e22a8 100644 --- a/vendor/github.com/gomarkdown/markdown/parser/citation.go +++ b/vendor/github.com/gomarkdown/markdown/parser/citation.go @@ -49,6 +49,9 @@ func citation(p *Parser, data []byte, offset int) (int, ast.Node) { for _, citation := range citations { var suffix []byte citation = bytes.TrimSpace(citation) + if len(citation) == 0 { + continue + } j := 0 if citation[j] != '@' { // not a citation, drop out entirely. diff --git a/vendor/github.com/gomarkdown/markdown/parser/inline.go b/vendor/github.com/gomarkdown/markdown/parser/inline.go index d6c8a7204..23a53e384 100644 --- a/vendor/github.com/gomarkdown/markdown/parser/inline.go +++ b/vendor/github.com/gomarkdown/markdown/parser/inline.go @@ -271,7 +271,7 @@ func maybeInlineFootnoteOrSuper(p *Parser, data []byte, offset int) (int, ast.No // '[': parse a link or an image or a footnote or a citation func link(p *Parser, data []byte, offset int) (int, ast.Node) { // no links allowed inside regular links, footnote, and deferred footnotes - if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { + if p.InsideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { return 0, nil } @@ -362,25 +362,27 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) { linkB := i brace := 0 + var c byte // look for link end: ' " ) findlinkend: for i < len(data) { + c = data[i] switch { - case data[i] == '\\': + case c == '\\': i += 2 - case data[i] == '(': + case c == '(': brace++ i++ - case data[i] == ')': + case c == ')': if brace <= 0 { break findlinkend } brace-- i++ - case data[i] == '\'' || data[i] == '"': + case c == '\'' || c == '"': break findlinkend default: @@ -402,14 +404,15 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) { findtitleend: for i < len(data) { + c = data[i] switch { - case data[i] == '\\': + case c == '\\': i++ - case data[i] == data[titleB-1]: // matching title delimiter + case c == data[titleB-1]: // matching title delimiter titleEndCharFound = true - case titleEndCharFound && data[i] == ')': + case titleEndCharFound && c == ')': break findtitleend } i++ @@ -619,10 +622,10 @@ func link(p *Parser, data []byte, offset int) (int, ast.Node) { } else { // links cannot contain other links, so turn off link parsing // temporarily and recurse - insideLink := p.insideLink - p.insideLink = true + InsideLink := p.InsideLink + p.InsideLink = true p.Inline(link, data[1:txtE]) - p.insideLink = insideLink + p.InsideLink = InsideLink } return i, link @@ -736,7 +739,7 @@ func leftAngle(p *Parser, data []byte, offset int) (int, ast.Node) { } // '\\' backslash escape -var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~^$") +var EscapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~^$") func escape(p *Parser, data []byte, offset int) (int, ast.Node) { data = data[offset:] @@ -753,7 +756,7 @@ func escape(p *Parser, data []byte, offset int) (int, ast.Node) { return 2, &ast.Hardbreak{} } - if bytes.IndexByte(escapeChars, data[1]) < 0 { + if bytes.IndexByte(EscapeChars, data[1]) < 0 { return 0, nil } @@ -814,7 +817,12 @@ func entity(p *Parser, data []byte, offset int) (int, ast.Node) { codepoint, err = strconv.ParseUint(string(ent[2:len(ent)-1]), 10, 64) } if err == nil { // only if conversion was valid return here. - return end, newTextNode([]byte(string(rune(codepoint)))) + r := rune(codepoint) + // Replace invalid codepoints with U+FFFD per CommonMark spec section 6.2 + if r == 0 || (r >= 0xD800 && r <= 0xDFFF) || r > 0x10FFFF { + r = '\uFFFD' + } + return end, newTextNode([]byte(string(r))) } return end, newTextNode(ent) @@ -857,7 +865,7 @@ const shortestPrefix = 6 // len("ftp://"), the shortest of the above func maybeAutoLink(p *Parser, data []byte, offset int) (int, ast.Node) { // quick check to rule out most false hits - if p.insideLink || len(data) < offset+shortestPrefix { + if p.InsideLink || len(data) < offset+shortestPrefix { return 0, nil } for _, prefix := range protocolPrefixes { @@ -1070,10 +1078,11 @@ func tagLength(data []byte) (autolink autolinkType, end int) { // one of the forbidden chars has been found autolink = notAutolink } - i += bytes.IndexByte(data[i:], '>') - if i < 0 { + j = bytes.IndexByte(data[i:], '>') + if j < 0 { return autolink, 0 } + i += j return autolink, i + 1 } @@ -1185,30 +1194,30 @@ func helperFindEmphChar(data []byte, c byte) int { func helperEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) { i := 0 - // skip one symbol if coming from emph3 + // skip two symbol if coming from emph3, as it detected a double emphasis case if len(data) > 1 && data[0] == c && data[1] == c { - i = 1 + i = 2 } for i < len(data) { length := helperFindEmphChar(data[i:], c) - if length == 0 { - return 0, nil - } i += length if i >= len(data) { return 0, nil } if i+1 < len(data) && data[i+1] == c { - i++ + i += 2 continue } if data[i] == c && !IsSpace(data[i-1]) { - if p.extensions&NoIntraEmphasis != 0 { - if !(i+1 == len(data) || IsSpace(data[i+1]) || IsPunctuation(data[i+1])) { + rest := data[i+1:] + if !(len(rest) == 0 || IsSpace(rest[0]) || IsPunctuation2(rest)) { + if length == 0 { + return 0, nil + } continue } } @@ -1217,6 +1226,11 @@ func helperEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) { p.Inline(emph, data[:i]) return i + 1, emph } + + // We have to check this at the end, otherwise the scenario where we find repeated c's will get skipped + if length == 0 { + return 0, nil + } } return 0, nil @@ -1233,18 +1247,53 @@ func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) { i += length if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !IsSpace(data[i-1]) { + // When the closing delimiter is *** (3+ chars) and there is an + // unclosed single emphasis opener inside the content, include + // one extra char in the content so that the inner emphasis can + // pair with it. For example: **bold *ital*** should produce + // bold ital, not bold *ital*. + // See https://github.com/gomarkdown/markdown/issues/279 + contentEnd := i + if i+2 < len(data) && data[i+2] == c && c != '~' { + if hasTrailingEmphOpener(data[:i], c) { + contentEnd = i + 1 + } + } + var node ast.Node = &ast.Strong{} if c == '~' { node = &ast.Del{} } - p.Inline(node, data[:i]) - return i + 2, node + p.Inline(node, data[:contentEnd]) + return contentEnd + 2, node } i++ } return 0, nil } +// hasTrailingEmphOpener checks if the last occurrence of c in data is an +// unclosed opener. An opener is c preceded by whitespace or start of data, +// followed by non-whitespace. If the last c is a closer (preceded by +// non-whitespace), the emphasis pair is balanced and we should not shift +// the content boundary. +func hasTrailingEmphOpener(data []byte, c byte) bool { + // find the last c in data + last := -1 + for j := len(data) - 1; j >= 0; j-- { + if data[j] == c { + last = j + break + } + } + if last < 0 { + return false + } + // opener: preceded by space/start, followed by non-space + return (last == 0 || IsSpace(data[last-1])) && + last+1 < len(data) && !IsSpace(data[last+1]) +} + func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, ast.Node) { i := 0 origData := data diff --git a/vendor/github.com/gomarkdown/markdown/parser/parser.go b/vendor/github.com/gomarkdown/markdown/parser/parser.go index f0a0d9cd9..43f213692 100644 --- a/vendor/github.com/gomarkdown/markdown/parser/parser.go +++ b/vendor/github.com/gomarkdown/markdown/parser/parser.go @@ -8,6 +8,8 @@ import ( "fmt" "strconv" "strings" + "unicode" + "unicode/utf8" "github.com/gomarkdown/markdown/ast" ) @@ -56,7 +58,7 @@ const ( ) // for each character that triggers a response when parsing inline data. -type inlineParser func(p *Parser, data []byte, offset int) (int, ast.Node) +type InlineParser func(p *Parser, data []byte, offset int) (int, ast.Node) // ReferenceOverrideFunc is expected to be called with a reference string and // return either a valid Reference type that the reference string maps to or @@ -98,10 +100,10 @@ type Parser struct { refs map[string]*reference refsRecord map[string]struct{} - inlineCallback [256]inlineParser + inlineCallback [256]InlineParser nesting int maxNesting int - insideLink bool + InsideLink bool indexCnt int // incremented after every index // Footnotes need to be ordered as well as available to quickly check for @@ -122,6 +124,8 @@ type Parser struct { // collect headings where we auto-generated id so that we can // ensure they are unique at the end allHeadingsWithAutoID []*ast.Heading + + didParse bool } // New creates a markdown parser with CommonExtensions. @@ -138,8 +142,8 @@ func NewWithExtensions(extension Extensions) *Parser { p := Parser{ refs: make(map[string]*reference), refsRecord: make(map[string]struct{}), - maxNesting: 16, - insideLink: false, + maxNesting: 64, + InsideLink: false, Doc: &ast.Document{}, extensions: extension, allClosed: true, @@ -181,7 +185,7 @@ func NewWithExtensions(extension Extensions) *Parser { return &p } -func (p *Parser) RegisterInline(n byte, fn inlineParser) inlineParser { +func (p *Parser) RegisterInline(n byte, fn InlineParser) InlineParser { prev := p.inlineCallback[n] p.inlineCallback[n] = fn return prev @@ -290,7 +294,14 @@ type Reference struct { // // You can then convert AST to html using html.Renderer, to some other format // using a custom renderer or transform the tree. +// +// Parser is not reusable. Create a new Parser for each Parse() call. func (p *Parser) Parse(input []byte) ast.Node { + if p.didParse { + panic("Parser is not reusable. Must create new Parser for each Parse() call.") + } + p.didParse = true + // the code only works with Unix CR newlines so to make life easy for // callers normalize newlines input = NormalizeNewlines(input) @@ -727,7 +738,21 @@ func IsPunctuation(c byte) bool { return false } -// IsSpace returns true if c is a white-space charactr +func IsPunctuation2(d []byte) bool { + if len(d) == 0 { + return false + } + if IsPunctuation(d[0]) { + return true + } + r, _ := utf8.DecodeRune(d) + if r == utf8.RuneError { + return false + } + return unicode.IsPunct(r) +} + +// IsSpace returns true if c is a white-space character func IsSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' } diff --git a/vendor/modules.txt b/vendor/modules.txt index ead692d5f..5db07718c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -103,7 +103,7 @@ github.com/golang/glog/internal/stackdump # github.com/golang/protobuf v1.5.4 ## explicit; go 1.17 github.com/golang/protobuf/proto -# github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 +# github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab ## explicit; go 1.12 github.com/gomarkdown/markdown/ast github.com/gomarkdown/markdown/html