// Package markdown allows reading and editing Markdown files package markdown import ( "bytes" "fmt" "io" "strings" ) // Heading represents a markdown Heading type Heading struct { Level int // the level of the heading. 1 means it's the H1 Content string // the text of the heading Header string // the heading itself } // MD represents a markdown file type MD struct { Headings []Heading } // Bytes returns the markdown as []bytes, ignoring errors func (md MD) Bytes() []byte { var b bytes.Buffer _, _ = md.WriteContent(&b) return b.Bytes() } // String returns the markdown as string, ignoring errors func (md MD) String() string { var b strings.Builder _, _ = md.WriteContent(&b) return b.String() } // TocBytes returns the table of contents as []byte, ignoring errors func (md MD) TocBytes() []byte { var b bytes.Buffer _, _ = md.WriteToc(&b) return b.Bytes() } // TocString returns the table of contents as string, ignoring errors func (md MD) TocString() string { var b strings.Builder _, _ = md.WriteToc(&b) return b.String() } // WriteContent writes the MDs content to the given writer func (md MD) WriteContent(w io.Writer) (int64, error) { written := int64(0) write := func(s string) error { n, err := w.Write([]byte(s)) written += int64(n) return err } for _, h := range md.Headings { if err := write(strings.Repeat("#", h.Level) + " " + h.Header + "\n"); err != nil { return written, err } if len(h.Content) > 0 { if err := write(h.Content); err != nil { return written, err } } } return written, nil } // WriteToc writes the table of contents to the given writer func (md MD) WriteToc(w io.Writer) (int64, error) { var written int64 for _, h := range md.Headings { if h.Level == 1 { // main title not in toc continue } link := fmt.Sprintf("#%s", strings.ToLower(strings.Replace(h.Header, " ", "-", -1))) s := fmt.Sprintf("%s* [%s](%s)\n", strings.Repeat(" ", h.Level-2), h.Header, link) n, err := w.Write([]byte(s)) if err != nil { return written, err } written += int64(n) } return written, nil } // NewMD parses a new Markdown func NewMD(b []byte) MD { var ( md MD heading Heading content strings.Builder ) sendHeading := func() { if heading.Header != "" { heading.Content = content.String() md.Headings = append(md.Headings, heading) content = strings.Builder{} } } parts := strings.Split("\n"+string(b), "\n#") numparts := len(parts) - 1 for i, p := range parts { if i == 0 { // omit part before first heading continue } all := strings.SplitN(p, "\n", 2) if len(all) != 2 { continue } head, con := all[0], all[1] // readd lost "#" heading = headingFromString("#" + head) _, _ = content.WriteString(con) // readd lost "\n" - omit for last part if i < numparts { _, _ = content.WriteString("\n") } // add heading sendHeading() } return md } func headingFromString(s string) Heading { i := strings.LastIndex(s, "#") levs, con := s[:i+1], s[i+1:] return Heading{ Level: len(levs), Header: strings.TrimPrefix(con, " "), } }