// Package cnm implements CNM document parsing and composition. package cnm // import "contnet.org/lib/cnm-go" import ( "bufio" "io" "path" "strings" ) // Document represents a CNM document. type Document struct { // Title is the document title (top-level "title" block). Title string // Links is a list of document-level hyperlinks (top-level "links" block). Links []Link // Site is a sitemap (top-level "site" block). Site Site // Content is the document content (top-level "content" block). Content *ContentBlock } // NewDocument creates a new Document. func NewDocument() *Document { return &Document{} } // ParseDocument parses a CNM document from r. func ParseDocument(r io.Reader) (doc *Document, err error) { p := NewParser(r) doc = NewDocument() err = p.Next() for err == nil { token := p.Block() if err = p.Next(); err != nil { break } if blk, ok := token.(*TokenBlock); ok { switch blk.Name { case "title": err = doc.parseTitle(p, blk) case "links": err = doc.parseLinks(p, blk) case "site": err = doc.Site.parse(p, blk) case "content": if doc.Content == nil { doc.Content = &ContentBlock{name: "content"} } err = doc.Content.parse(p, blk) default: // discard lines inside this block for err == nil { if !p.Empty() && p.Indent() <= blk.Indent() { break } err = p.Next() } } } } if err == io.EOF { err = nil } return } // Name returns an empty string (top-level block context has no name). func (doc *Document) Name() string { return "" } // Args returns a nil slice (top-level block context has no args). func (doc *Document) Args() []string { return nil } // WriteIndent writes the document to w. // // n must be 0 or this function will panic. func (doc *Document) WriteIndent(w io.Writer, n int) error { if n != 0 { panic("cnm-go: Document.WriteIndent: n must be 0") } return doc.Write(w) } // Write writes the document to w. func (doc *Document) Write(w io.Writer) error { bw := bufio.NewWriter(w) if doc.Title != "" { if err := WriteIndent(bw, "title", 0); err != nil { return err } if err := WriteIndent(bw, Escape(doc.Title), 1); err != nil { return err } } if len(doc.Links) > 0 { if err := WriteIndent(bw, "links", 0); err != nil { return err } for _, link := range doc.Links { if err := link.WriteIndent(bw, 1); err != nil { return err } } } if len(doc.Site.Children) > 0 { if err := WriteIndent(bw, "site", 0); err != nil { return err } for _, site := range doc.Site.Children { if err := site.WriteIndent(bw, 1); err != nil { return err } } } if doc.Content != nil { if err := doc.Content.WriteIndent(bw, 0); err != nil { return err } } return bw.Flush() } func (doc *Document) parseTitle(p *Parser, block *TokenBlock) (err error) { s, err := getSimpleText(p, block) if doc.Title == "" { doc.Title = s } else { doc.Title += " " + s } return } func (doc *Document) parseLinks(p *Parser, block *TokenBlock) (err error) { for err == nil { if !p.Empty() && p.Indent() <= block.Indent() { break } token := p.Block() if blk, ok := token.(*TokenBlock); ok { if blk.Name == "" { err = parseUnknown(p, blk) } else { link := Link{ URL: blk.Name, Name: strings.Join(blk.Args, " "), } doc.Links = append(doc.Links, link) if err = p.Next(); err != nil { break } doc.Links[len(doc.Links)-1].Description, err = getSimpleText(p, blk) } } } return } func getSimpleText(p *Parser, block *TokenBlock) (s string, err error) { for err == nil { if !p.Empty() && p.Indent() <= block.Indent() { break } token := p.SimpleText() if text, ok := token.(*TokenSimpleText); ok && text.Text != "" { if s == "" { s = text.Text } else { s += " " + text.Text } } err = p.Next() } return } // Link represents a document-level hyperlink in the "links" top-level block. type Link struct { // URL is the hyperlink URL. URL string // Name is the hyperlink text. Name string // Description is the description of the hyperlink. Description string } // WriteIndent writes the link URL, name and description indented by n tabs. func (link Link) WriteIndent(w io.Writer, n int) error { s := EscapeAll(link.URL) if link.Name != "" { s += " " + Escape(link.Name) } if err := WriteIndent(w, s, n); err != nil { return err } if link.Description != "" { if err := WriteIndent(w, Escape(link.Description), n+1); err != nil { return err } } return nil } // Site represents a node in the sitemap in the "site" top-level block. type Site struct { // Path is the node's path fragment. Path string // Name is the node's name. Name string // Children are the nodes below this node. Children []Site } // WriteIndent writes the sitemap indented by n tabs. func (site Site) WriteIndent(w io.Writer, n int) error { s := EscapeAll(site.Path) if site.Name != "" { s += " " + Escape(site.Name) } if err := WriteIndent(w, s, n); err != nil { return err } for _, ch := range site.Children { if err := ch.WriteIndent(w, n+1); err != nil { return err } } return nil } func (site *Site) parse(p *Parser, block *TokenBlock) (err error) { for err == nil { if !p.Empty() && p.Indent() <= block.Indent() { break } token := p.Block() if blk, ok := token.(*TokenBlock); ok { if blk.Name == "" { err = parseUnknown(p, blk) } else { s := Site{ Path: strings.Trim(path.Clean(blk.Name), "/"), Name: strings.Join(blk.Args, " "), } site.Children = append(site.Children, s) if err = p.Next(); err != nil { break } err = site.Children[len(site.Children)-1].parse(p, blk) } } else { err = p.Next() } } return } func parseUnknown(p *Parser, block *TokenBlock) (err error) { err = p.Next() for err == nil { if !p.Empty() && p.Indent() <= block.Indent() { break } // discard lines inside this block err = p.Next() } return } // WriteIndent writes an indented line into a writer. // // Writes depth tab characters, the string s and a newline. If s is blank, no // indentation is used. s should not contain newlines. func WriteIndent(w io.Writer, s string, depth int) error { const tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" if s == "" { _, err := w.Write([]byte{'\n'}) return err } if depth == 0 { _, err := w.Write([]byte(s + "\n")) return err } var ind string if depth <= len(tabs) { ind = tabs[:depth] } else { ind = strings.Repeat("\t", depth) } _, err := w.Write([]byte(ind + s + "\n")) return err }