129 lines
3.8 KiB
Go
129 lines
3.8 KiB
Go
// 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)
|
|
}
|
|
}
|