Skip to content

Instantly share code, notes, and snippets.

@EgorBron
Created August 2, 2026 17:51
Show Gist options
  • Select an option

  • Save EgorBron/6888a5a1a311b76612ee7cf33647f6c5 to your computer and use it in GitHub Desktop.

Select an option

Save EgorBron/6888a5a1a311b76612ee7cf33647f6c5 to your computer and use it in GitHub Desktop.
package markdown
import (
"encoding/json"
"strings"
"unicode/utf8"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
const (
formatVersion = "1"
formatBold = "bold"
formatItalic = "italic"
formatURL = "url"
bulletPoint = " • "
)
type FormatItem struct {
Type string `json:"type"`
Offset int `json:"offset"`
Length int `json:"length"`
URL string `json:"url,omitempty"`
}
type FormatData struct {
Version string `json:"version"`
Items []FormatItem `json:"items"`
}
type ParseResult struct {
FormatData string `json:"format_data"`
Message string `json:"message"`
}
func ParseMarkdown(markdown string) (ParseResult, error) {
parser := newMarkdownParser(markdown)
doc := goldmark.New().Parser().Parse(text.NewReader([]byte(markdown)))
parser.walk(doc)
formatDataJSON, err := json.Marshal(parser.getFormatData())
if err != nil {
return ParseResult{}, err
}
return ParseResult{
FormatData: string(formatDataJSON),
Message: parser.output.String(),
}, nil
}
type markdownParser struct {
source []byte
output strings.Builder
offset int
items []FormatItem
}
func newMarkdownParser(markdown string) *markdownParser {
return &markdownParser{
source: []byte(markdown),
}
}
func (p *markdownParser) getFormatData() FormatData {
return FormatData{
Version: formatVersion,
Items: p.items,
}
}
func (p *markdownParser) walk(node ast.Node) {
switch n := node.(type) {
case *ast.Document:
p.walkChildren(n)
case *ast.Heading:
p.handleHeading(n)
case *ast.Paragraph:
p.handleBlockElement(n)
case *ast.Text:
p.writeText(n.Segment.Value(p.source))
case *ast.String:
p.writeText(n.Value)
case *ast.Emphasis:
p.handleEmphasis(n)
case *ast.Link:
p.handleLink(n)
case *ast.ListItem:
p.handleListItem(n)
case *ast.List:
p.handleBlockElement(n)
case *ast.Blockquote:
p.handleBlockElement(n)
case *ast.CodeBlock:
p.handleCodeBlock(n)
default:
p.walkChildren(n)
}
}
func (p *markdownParser) handleHeading(heading *ast.Heading) {
p.addBlankLineIfNeeded(heading)
p.processFormattedNode(heading, formatBold, "")
p.addNewline()
}
func (p *markdownParser) handleEmphasis(emphasis *ast.Emphasis) {
formatType := formatItalic
if emphasis.Level == 2 {
formatType = formatBold
}
p.processFormattedNode(emphasis, formatType, "")
}
func (p *markdownParser) handleLink(link *ast.Link) {
p.processFormattedNode(link, formatURL, string(link.Destination))
}
func (p *markdownParser) handleListItem(item *ast.ListItem) {
p.writeText([]byte(bulletPoint))
p.walkChildren(item)
p.addNewline()
}
func (p *markdownParser) handleBlockElement(node ast.Node) {
p.addBlankLineIfNeeded(node)
p.walkChildren(node)
p.addNewline()
}
func (p *markdownParser) handleCodeBlock(block *ast.CodeBlock) {
p.addBlankLineIfNeeded(block)
lines := block.Lines()
for i := 0; i < lines.Len(); i++ {
segment := lines.At(i)
p.writeText(segment.Value(p.source))
if i < lines.Len()-1 {
p.addNewline()
}
}
p.addNewline()
}
func (p *markdownParser) processFormattedNode(node ast.Node, formatType, url string) {
startOffset := p.offset
p.walkChildren(node)
p.addFormatItem(formatType, startOffset, p.offset-startOffset, url)
}
func (p *markdownParser) walkChildren(node ast.Node) {
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
p.walk(child)
}
}
func (p *markdownParser) writeText(data []byte) {
p.output.Write(data)
p.offset += utf8.RuneCount(data)
}
func (p *markdownParser) addFormatItem(formatType string, startOffset, length int, url string) {
if length <= 0 {
return
}
p.items = append(p.items, FormatItem{
Type: formatType,
Offset: startOffset,
Length: length,
URL: url,
})
}
func (p *markdownParser) addBlankLineIfNeeded(node ast.Node) {
if p.offset == 0 {
return
}
blockNode, ok := node.(interface{ HasBlankPreviousLines() bool })
if ok && blockNode.HasBlankPreviousLines() {
p.addNewline()
}
}
func (p *markdownParser) addNewline() {
p.output.WriteRune('\n')
p.offset++
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment