// markdown.go adds Markdown → the same structured []Paragraph // vocabulary create_document and BuildDocx already use, so an // existing Markdown document (a README, a chat reply, something an // upload already contains) can become a real .docx without the model // ever hand-translating headings and bullets into paragraph arguments // itself. It reuses the gomarkdown parser already vendored for // chat-message rendering (pkg/renderkit) rather than hand-rolling a // second Markdown parser — one less thing in this codebase that can // disagree about what counts as a heading or a list item. // // Scope: block-level structure only — headings, paragraphs, and // bullet/numbered lists (numbered lists render as bullets, since // Paragraph has no numbered-list style of its own yet). Fenced or // indented code blocks come through as plain paragraphs, since // Paragraph has no monospace style either. Inline formatting // (bold/italic spans within a paragraph, links, inline code) is // flattened to plain text — Paragraph only supports whole-paragraph // bold/italic/underline (see docx.go), the same limit // create_document's tool description already states. Tables, raw HTML // blocks, footnotes, and anything else without a Paragraph-vocabulary // equivalent are silently dropped rather than guessed at. package officekit import ( "strings" "github.com/gomarkdown/markdown/ast" mdparser "github.com/gomarkdown/markdown/parser" ) // MarkdownToParagraphs parses a Markdown document into the paragraph // vocabulary BuildDocx/AppendDocxParagraph already understand, so a // caller can go straight from Markdown source to a .docx via // BuildDocx(MarkdownToParagraphs(src)). func MarkdownToParagraphs(src string) []Paragraph { p := mdparser.NewWithExtensions(mdparser.CommonExtensions) doc := p.Parse([]byte(src)) var out []Paragraph for _, block := range childNodes(doc) { out = append(out, blockToParagraphs(block)...) } return out } func blockToParagraphs(n ast.Node) []Paragraph { switch v := n.(type) { case *ast.Heading: text := leafText(v) if text == "" { return nil } style := StyleHeading3 switch { case v.Level <= 1: style = StyleHeading1 case v.Level == 2: style = StyleHeading2 } return []Paragraph{{Text: text, Style: style}} case *ast.Paragraph: text := leafText(v) if text == "" { return nil } return []Paragraph{{Text: text, Style: StyleNormal}} case *ast.List: var out []Paragraph for _, item := range childNodes(v) { text := leafText(item) if text == "" { continue } out = append(out, Paragraph{Text: text, Style: StyleBullet}) } return out case *ast.CodeBlock: text := strings.TrimRight(string(v.Literal), "\n") if text == "" { return nil } return []Paragraph{{Text: text, Style: StyleNormal}} case *ast.BlockQuote: var out []Paragraph for _, c := range childNodes(v) { out = append(out, blockToParagraphs(c)...) } return out default: // HorizontalRule, Table, HTMLBlock, and anything else this // package has no Paragraph-vocabulary equivalent for. return nil } } // childNodes returns n's direct block-level children via gomarkdown's // Container/Leaf node split: a Leaf node (Text, Code, ...) has none. func childNodes(n ast.Node) []ast.Node { if c := n.AsContainer(); c != nil { return c.Children } return nil } // leafText concatenates every Text/Code leaf found under n, depth // first — the inline-formatting-flattening step described in this // file's package doc. func leafText(n ast.Node) string { var sb strings.Builder collectText(n, &sb) return strings.TrimSpace(sb.String()) } func collectText(n ast.Node, sb *strings.Builder) { switch v := n.(type) { case *ast.Text: sb.Write(v.Literal) return case *ast.Code: sb.Write(v.Literal) return case *ast.Hardbreak: sb.WriteByte(' ') return } for _, c := range childNodes(n) { collectText(c, sb) } }