package main import ( "bytes" "regexp" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" ) var sectionHeadingRe = regexp.MustCompile(`(?m)^#{1,6} `) // splitSections splits raw markdown into sections. // Section 0 is any content before the first heading. // Each subsequent section begins at a heading line and runs to the next. func splitSections(raw []byte) [][]byte { locs := sectionHeadingRe.FindAllIndex(raw, -1) if len(locs) == 0 { return [][]byte{raw} } sections := make([][]byte, 0, len(locs)+1) prev := 0 for _, loc := range locs { sections = append(sections, raw[prev:loc[0]]) prev = loc[0] } sections = append(sections, raw[prev:]) return sections } // headingIDs returns the auto-generated id of every heading in raw markdown, // in document order. The kth heading (1-indexed) corresponds to section k from // splitSections. Uses the package-level goldmark parser so duplicate-id // numbering matches what the renderer emits. func headingIDs(raw []byte) []string { doc := md.Parser().Parse(text.NewReader(raw)) var ids []string ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } if _, ok := n.(*ast.Heading); ok { if v, ok := n.AttributeString("id"); ok { if b, ok := v.([]byte); ok { ids = append(ids, string(b)) } } } return ast.WalkContinue, nil }) return ids } // joinSections reassembles sections produced by splitSections. // Inserts a newline between sections when a non-empty section lacks a // trailing newline, so an edited section cannot inline the next heading. func joinSections(sections [][]byte) []byte { var buf bytes.Buffer for i, s := range sections { buf.Write(s) if i < len(sections)-1 && len(s) > 0 && s[len(s)-1] != '\n' { buf.WriteByte('\n') } } return buf.Bytes() }