summaryrefslogtreecommitdiffstats
path: root/document.go
diff options
context:
space:
mode:
Diffstat (limited to 'document.go')
-rw-r--r--document.go278
1 files changed, 278 insertions, 0 deletions
diff --git a/document.go b/document.go
new file mode 100644
index 0000000..b5cdbe5
--- /dev/null
+++ b/document.go
@@ -0,0 +1,278 @@
+// 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
+}
+
+// ParseDocument parses a CNM document from r.
+func ParseDocument(r io.Reader) (doc *Document, err error) {
+ p := NewParser(r)
+ doc = &Document{}
+ 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
+}
+
+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 := Escape(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 := Escape(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
+}
+
+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
+}