package mdprint import ( "strings" ) func parseBlocks(content string) []Node { lines := strings.Split(content, "\n") var blocks []Node i := 0 for i < len(lines) { line := lines[i] trim := strings.TrimSpace(line) if trim == "" { i++ continue } if level := headingLevel(trim); level > 0 { rest := strings.TrimSpace(trim[level:]) blocks = append(blocks, Heading{Level: level, Content: parseInline(rest)}) i++ continue } if trim == "---" || trim == "***" || trim == "___" { blocks = append(blocks, ThematicBreak{}) i++ continue } if isFence(trim) { b, lang, consumed := parseFence(lines, i) blocks = append(blocks, CodeBlock{Lang: lang, Body: b}) i += consumed continue } if strings.HasPrefix(trim, ">") { q, consumed := parseQuote(lines, i) blocks = append(blocks, q) i += consumed continue } if mark, ordered, ok := listMarker(trim); ok { items, consumed := parseListItems(lines, i, mark) blocks = append(blocks, List{Ordered: ordered, Items: items}) i += consumed continue } if strings.HasPrefix(trim, "|") && i+1 < len(lines) && isTableSep(strings.TrimSpace(lines[i+1])) { t, consumed := parseTable(lines, i) blocks = append(blocks, t) i += consumed continue } p, consumed := parseParagraph(lines, i) blocks = append(blocks, p) i += consumed } return blocks } func headingLevel(s string) int { n := 0 for _, c := range s { if c == '#' { n++ } else { break } } if n > 0 && n <= 6 && len(s) > n && s[n] == ' ' { return n } return 0 } func isFence(s string) bool { count := 0 for _, c := range s { if c == '`' || c == '~' { count++ } else { break } } return count >= 3 } func fenceChar(s string) rune { for _, c := range s { return c } return '`' } func parseFence(lines []string, start int) (body, lang string, consumed int) { first := strings.TrimSpace(lines[start]) ch := fenceChar(first) lang = strings.TrimSpace(first[3:]) var buf strings.Builder consumed = 1 for i := start + 1; i < len(lines); i++ { consumed++ if strings.TrimSpace(lines[i]) == strings.Repeat(string(ch), 3) { break } if buf.Len() > 0 { buf.WriteString("\n") } buf.WriteString(lines[i]) } body = buf.String() return } func parseQuote(lines []string, start int) (Blockquote, int) { var contents []Node var buf []string flush := func() { if len(buf) > 0 { text := strings.Join(buf, "\n") contents = append(contents, Paragraph{Content: parseInline(text)}) buf = nil } } i := start for i < len(lines) { trim := strings.TrimSpace(lines[i]) if !strings.HasPrefix(trim, ">") { break } rest := trim[1:] if strings.HasPrefix(rest, " ") { rest = rest[1:] } if rest == "" { flush() } else { buf = append(buf, rest) } i++ } flush() return Blockquote{Children: contents}, i - start } func listMarker(s string) (mark string, ordered bool, ok bool) { trim := strings.TrimSpace(s) if len(trim) >= 2 && (trim[0] == '-' || trim[0] == '*') && trim[1] == ' ' { return string(trim[0]), false, true } if len(trim) >= 3 && trim[0] >= '1' && trim[0] <= '9' && trim[1] == '.' && trim[2] == ' ' { return ".", true, true } return "", false, false } func parseListItems(lines []string, start int, mark string) ([]ListItem, int) { var items []ListItem consumed := 0 for i := start; i < len(lines); i++ { trim := strings.TrimSpace(lines[i]) m, _, ok := listMarker(trim) if !ok || m != mark { break } consumed++ rest := strings.TrimSpace(trim[len(mark)+1:]) var checked *bool if strings.HasPrefix(rest, "[ ] ") { f := false checked = &f rest = rest[4:] } else if strings.HasPrefix(rest, "[x] ") { t := true checked = &t rest = rest[4:] } else if strings.HasPrefix(rest, "[X] ") { t := true checked = &t rest = rest[4:] } items = append(items, ListItem{ Checked: checked, Content: parseInline(rest), }) } return items, consumed } func isTableSep(s string) bool { if !strings.HasPrefix(s, "|") || !strings.HasSuffix(s, "|") { return false } s = s[1 : len(s)-1] cells := strings.Split(s, "|") if len(cells) == 0 { return false } for _, c := range cells { c = strings.TrimSpace(c) if c == "" { return false } for _, ch := range c { if ch != '-' && ch != ':' { return false } } } return true } func parseTable(lines []string, start int) (Table, int) { headerLine := strings.TrimSpace(lines[start]) headers := splitTableRow(headerLine) consumed := 2 var rows [][]string for i := start + 2; i < len(lines); i++ { trim := strings.TrimSpace(lines[i]) if !strings.HasPrefix(trim, "|") { break } rows = append(rows, splitTableRow(trim)) consumed++ } return Table{Headers: headers, Rows: rows}, consumed } func splitTableRow(s string) []string { s = strings.TrimSpace(s) if strings.HasPrefix(s, "|") { s = s[1:] } if strings.HasSuffix(s, "|") { s = s[:len(s)-1] } cells := strings.Split(s, "|") result := make([]string, len(cells)) for i, c := range cells { result[i] = strings.TrimSpace(c) } return result } func parseParagraph(lines []string, start int) (Paragraph, int) { var contentLines []string consumed := 0 for i := start; i < len(lines); i++ { trim := strings.TrimSpace(lines[i]) if trim == "" { break } if headingLevel(trim) > 0 { break } if trim == "---" || trim == "***" || trim == "___" { break } if isFence(trim) { break } if strings.HasPrefix(trim, ">") { break } if _, _, ok := listMarker(trim); ok { break } contentLines = append(contentLines, lines[i]) consumed++ } text := strings.Join(contentLines, "\n") return Paragraph{Content: parseInline(text)}, consumed }