summaryrefslogtreecommitdiffstats
path: root/content.go
diff options
context:
space:
mode:
Diffstat (limited to 'content.go')
-rw-r--r--content.go610
1 files changed, 610 insertions, 0 deletions
diff --git a/content.go b/content.go
new file mode 100644
index 0000000..971e9e3
--- /dev/null
+++ b/content.go
@@ -0,0 +1,610 @@
+package cnm
+
+import (
+ "io"
+ "strings"
+)
+
+func init() {
+ RegisterTextContentParser("", parseTextPlain)
+ RegisterTextContentParser("plain", parseTextPlain)
+ RegisterTextContentParser("pre", parseTextPre)
+}
+
+// Block represents an arbitrary CNM within the "content" top-level block.
+type Block interface {
+ // Name returns the name of the block.
+ Name() string
+
+ // Args returns the block arguments.
+ Args() []string
+ WriteIndent(w io.Writer, n int) error
+}
+
+// ContentBlock represents a block that holds other content blocks.
+type ContentBlock struct {
+ name string
+ args []string
+ children []Block
+}
+
+// WriteIndent writes the block header and its children indented by n tabs.
+func (cb *ContentBlock) WriteIndent(w io.Writer, n int) error {
+ ss := []string{Escape(cb.name)}
+ ss = append(ss, cb.args...)
+ if err := writeIndent(w, JoinEscape(ss), n); err != nil {
+ return err
+ }
+ for _, ch := range cb.children {
+ if err := ch.WriteIndent(w, n+1); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// NewContentBlock creates a new ContentBlock with a name and argument.
+func NewContentBlock(name string, args ...string) *ContentBlock {
+ var a []string
+ for _, arg := range args {
+ if arg != "" {
+ a = append(a, arg)
+ }
+ }
+ return &ContentBlock{name: name, args: a}
+}
+
+// Name returns the block's name.
+func (cb *ContentBlock) Name() string {
+ return cb.name
+}
+
+// Args returns the block arguments.
+func (cb *ContentBlock) Args() []string {
+ return cb.args
+}
+
+// Children returns the block's child blocks.
+func (cb *ContentBlock) Children() []Block {
+ return cb.children
+}
+
+// AppendChild adds a new child block to the end of the list of children.
+func (cb *ContentBlock) AppendChild(block Block) {
+ cb.children = append(cb.children, block)
+}
+
+func (cb *ContentBlock) 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 {
+ var b Block
+ switch blk.Name {
+ case "section":
+ b, err = parseContentSection(p, blk)
+ case "text":
+ b, err = parseContentText(p, blk)
+ case "raw":
+ b, err = parseContentRaw(p, blk)
+ case "list":
+ b, err = parseContentList(p, blk)
+ case "table":
+ b, err = parseContentTable(p, blk)
+ case "embed":
+ b, err = parseContentEmbed(p, blk)
+ default:
+ err = parseUnknown(p, blk)
+ }
+ if b != nil {
+ cb.AppendChild(b)
+ }
+ } else if err = p.Next(); err != nil {
+ break
+ }
+ }
+ return
+}
+
+// SectionBlock represents a "section" content block.
+type SectionBlock struct {
+ ContentBlock
+}
+
+// NewSectionBlock creates a new SectionBlock with a title.
+func NewSectionBlock(title string) *SectionBlock {
+ return &SectionBlock{*NewContentBlock("section", title)}
+}
+
+// Title returns the section block's title.
+func (b *SectionBlock) Title() string { return strings.Join(b.args, " ") }
+
+func parseContentSection(p *Parser, block *TokenBlock) (*SectionBlock, error) {
+ sec := NewSectionBlock(strings.Join(block.Args, " "))
+ if err := p.Next(); err != nil {
+ return sec, err
+ }
+ return sec, sec.parse(p, block)
+}
+
+// TextBlock represents a "text" content block.
+type TextBlock struct {
+ // Format is the text format (first word of the block argument).
+ Format string
+ // Contents are the text contents.
+ Contents TextContents
+}
+
+// NewTextBlock creates a new TextBlock containing arbitrary text contents.
+func NewTextBlock(format string, contents TextContents) *TextBlock {
+ return &TextBlock{format, contents}
+}
+
+// Name returns the block name "text".
+func (t *TextBlock) Name() string { return "text" }
+
+// Args returns the block's arguments (format).
+func (t *TextBlock) Args() []string {
+ if t.Format == "" {
+ return nil
+ }
+ return []string{t.Format}
+}
+
+// WriteIndent writes the block header and its content indented by n tabs.
+func (t *TextBlock) WriteIndent(w io.Writer, n int) error {
+ s := t.Name()
+ if t.Format != "" {
+ s += " " + Escape(t.Format)
+ }
+ if err := writeIndent(w, s, n); err != nil {
+ return err
+ }
+ if err := t.Contents.WriteIndent(w, n+1); err != nil {
+ return err
+ }
+ return nil
+}
+
+func parseContentText(p *Parser, block *TokenBlock) (*TextBlock, error) {
+ format := ""
+ if len(block.Args) >= 1 {
+ format = block.Args[0]
+ }
+ tb := NewTextBlock(format, nil)
+
+ if err := p.Next(); err != nil {
+ return tb, err
+ }
+
+ var err error
+ tb.Contents, err = parseTextFormat(p, block, tb.Format)
+
+ return tb, err
+}
+
+func parseTextFormat(p *Parser, block *TokenBlock, format string) (TextContents, error) {
+ if parser := GetTextContentParser(format); parser != nil {
+ return parser(p, block)
+ }
+ r, err := parseContentRaw(p, block)
+ return TextPreContents{r.Contents}, err
+}
+
+// TextContents represents the textual contents of a text block.
+type TextContents interface {
+ WriteIndent(w io.Writer, n int) error
+}
+
+// TextContentParser parses text content in a text block.
+type TextContentParser func(p *Parser, block *TokenBlock) (TextContents, error)
+
+var textContentParsers = map[string]TextContentParser{}
+
+// GetTextContentParser retrieves a text content parser or nil if it doesn't
+// exist.
+func GetTextContentParser(name string) TextContentParser {
+ return textContentParsers[name]
+}
+
+// RegisterTextContentParser registers a new text content parser for a format.
+func RegisterTextContentParser(name string, parser TextContentParser) {
+ if parser == nil {
+ delete(textContentParsers, name)
+ } else {
+ textContentParsers[name] = parser
+ }
+}
+
+// TextPlainContents represents a list of simple text paragraphs.
+type TextPlainContents struct {
+ // Paragraphs is a list of simple text paragraphs.
+ Paragraphs []string
+}
+
+// WriteIndent writes the plain text content indented by n tabs.
+func (t TextPlainContents) WriteIndent(w io.Writer, n int) error {
+ for i, p := range t.Paragraphs {
+ if i != 0 {
+ if err := writeIndent(w, "", 0); err != nil {
+ return err
+ }
+ }
+ if err := writeIndent(w, Escape(p), n); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// NewTextPlainBlock creates a new TextBlock containing TextPlainContents.
+func NewTextPlainBlock(paragraphs []string) *TextBlock {
+ par := make([]string, len(paragraphs))
+ copy(par, paragraphs)
+ return NewTextBlock("", TextPlainContents{par})
+}
+
+func parseTextPlain(p *Parser, block *TokenBlock) (TextContents, error) {
+ txt := TextPlainContents{}
+ paragraph := ""
+ var err error
+ for err == nil {
+ if !p.Empty() && p.Indent() <= block.Indent() {
+ break
+ }
+
+ token := p.SimpleText()
+ end := false
+ if text, ok := token.(*TokenSimpleText); ok {
+ if text.Text == "" {
+ end = true
+ } else if paragraph == "" {
+ paragraph = text.Text
+ } else {
+ paragraph += " " + text.Text
+ }
+ } else if _, ok := token.(*TokenEmptyLine); ok && paragraph != "" {
+ end = true
+ }
+ if end {
+ txt.Paragraphs = append(txt.Paragraphs, paragraph)
+ paragraph = ""
+ }
+ err = p.Next()
+ }
+ if paragraph != "" {
+ txt.Paragraphs = append(txt.Paragraphs, paragraph)
+ }
+ return txt, err
+}
+
+// TextPreContents represents preformatted contents of a text block.
+type TextPreContents struct {
+ // Text is the preformatted content.
+ Text string
+}
+
+// WriteIndent writes the preformatted text content indented by n tabs.
+func (t TextPreContents) WriteIndent(w io.Writer, n int) error {
+ ss := strings.Split(t.Text, "\n")
+ for _, s := range ss {
+ if err := writeIndent(w, EscapeNonspace(s), n); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// NewTextPreBlock creates a new TextBlock containing TextPreContents.
+func NewTextPreBlock(text string) *TextBlock {
+ return NewTextBlock("", TextPreContents{text})
+}
+
+func parseTextPre(p *Parser, block *TokenBlock) (TextContents, error) {
+ var lines []string
+ var ls []string
+ var err error
+ for err == nil {
+ if !p.Empty() && p.Indent() <= block.Indent() {
+ break
+ }
+
+ token := p.RawText()
+ if text, ok := token.(*TokenRawText); ok {
+ if len(ls) > 0 {
+ lines = append(lines, ls...)
+ ls = ls[:0]
+ }
+ lines = append(lines, Unescape(text.Text))
+ } else if _, ok := token.(*TokenEmptyLine); ok && len(lines) > 0 {
+ ls = append(ls, "")
+ }
+ err = p.Next()
+ }
+ return TextPreContents{strings.Join(lines, "\n")}, err
+}
+
+// RawBlock represents a "raw" content block.
+type RawBlock struct {
+ // Syntax is the syntax of the block contents (first word of block argument)
+ Syntax string
+
+ // Contents is the raw content.
+ Contents string
+}
+
+// Name returns the block name "raw".
+func (r *RawBlock) Name() string { return "raw" }
+
+// Args returns the block's arguments (syntax).
+func (r *RawBlock) Args() []string {
+ if r.Syntax == "" {
+ return nil
+ }
+ return []string{r.Syntax}
+}
+
+// WriteIndent writes the raw content indented by n tabs.
+func (r *RawBlock) WriteIndent(w io.Writer, n int) error {
+ s := r.Name()
+ if r.Syntax != "" {
+ s += " " + Escape(r.Syntax)
+ }
+ if err := writeIndent(w, s, n); err != nil {
+ return err
+ }
+ if r.Contents != "" {
+ ss := strings.Split(r.Contents, "\n")
+ for _, s := range ss {
+ if err := writeIndent(w, s, n+1); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func parseContentRaw(p *Parser, block *TokenBlock) (*RawBlock, error) {
+ arg := ""
+ if len(block.Args) > 0 {
+ arg = block.Args[0]
+ }
+ rb := &RawBlock{arg, ""}
+
+ if err := p.Next(); err != nil {
+ return rb, err
+ }
+
+ var lines []string
+ var ls []string
+ var err error
+ for err == nil {
+ if !p.Empty() && p.Indent() <= block.Indent() {
+ break
+ }
+
+ token := p.RawText()
+ if text, ok := token.(*TokenRawText); ok {
+ if len(ls) > 0 {
+ lines = append(lines, ls...)
+ ls = ls[:0]
+ }
+ lines = append(lines, text.Text)
+ } else if _, ok := token.(*TokenEmptyLine); ok && len(lines) > 0 {
+ ls = append(ls, "")
+ }
+ err = p.Next()
+ }
+ rb.Contents = strings.Join(lines, "\n")
+
+ return rb, err
+}
+
+// ListBlock represents a "list" content block.
+type ListBlock struct {
+ ContentBlock
+}
+
+// NewListBlock creates a new ListBlock.
+//
+// If the ordered parameter is true, the list is created in "ordered" mode.
+func NewListBlock(ordered bool) *ListBlock {
+ arg := ""
+ if ordered {
+ arg = "ordered"
+ }
+ return &ListBlock{*NewContentBlock("list", arg)}
+}
+
+// Ordered returns true if the list is in ordered mode (first word of the
+// block argument is "ordered").
+func (b *ListBlock) Ordered() bool {
+ return len(b.args) >= 1 && b.args[0] == "ordered"
+}
+
+func parseContentList(p *Parser, block *TokenBlock) (*ListBlock, error) {
+ list := NewListBlock(false)
+ list.args = block.Args
+ if err := p.Next(); err != nil {
+ return list, err
+ }
+ return list, list.parse(p, block)
+}
+
+// TableBlock represents a "table" content block.
+type TableBlock struct {
+ rows []Block
+}
+
+// NewTableBlock creates a new TableBlock.
+func NewTableBlock() *TableBlock {
+ return &TableBlock{}
+}
+
+// Name returns the block name "table".
+func (t *TableBlock) Name() string {
+ return "table"
+}
+
+// Args returns the block's nil arguments.
+func (t *TableBlock) Args() []string {
+ return nil
+}
+
+// WriteIndent writes the table header and contents indented by n tabs.
+func (t *TableBlock) WriteIndent(w io.Writer, n int) error {
+ if err := writeIndent(w, t.Name(), n); err != nil {
+ return err
+ }
+ for _, row := range t.rows {
+ if err := row.WriteIndent(w, n+1); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Rows returns the table's rows.
+func (t *TableBlock) Rows() []Block {
+ return t.rows
+}
+
+// AppendRow adds a new row to the end of the table.
+func (t *TableBlock) AppendRow(row Block) {
+ t.rows = append(t.rows, row)
+}
+
+func (t *TableBlock) 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 {
+ var b Block
+ switch blk.Name {
+ case "row":
+ b, err = parseTableRow(p, blk)
+ case "header":
+ b, err = parseTableHeader(p, blk)
+ default:
+ err = parseUnknown(p, blk)
+ }
+ if b != nil {
+ t.AppendRow(b)
+ }
+ } else if err = p.Next(); err != nil {
+ break
+ }
+ }
+ return
+}
+
+func parseContentTable(p *Parser, block *TokenBlock) (*TableBlock, error) {
+ table := NewTableBlock()
+ if err := p.Next(); err != nil {
+ return table, err
+ }
+ return table, table.parse(p, block)
+}
+
+// RowBlock represents a "row" table block.
+type RowBlock struct {
+ ContentBlock
+}
+
+// NewRowBlock creates a new RowBlock.
+func NewRowBlock() *RowBlock {
+ return &RowBlock{*NewContentBlock("row", "")}
+}
+
+func parseTableRow(p *Parser, block *TokenBlock) (*RowBlock, error) {
+ row := NewRowBlock()
+ if err := p.Next(); err != nil {
+ return row, err
+ }
+ return row, row.parse(p, block)
+}
+
+// HeaderBlock represents a "header" table block.
+type HeaderBlock struct {
+ ContentBlock
+}
+
+// NewHeaderBlock creates a new HeaderBlock.
+func NewHeaderBlock() *HeaderBlock {
+ return &HeaderBlock{*NewContentBlock("header", "")}
+}
+
+func parseTableHeader(p *Parser, block *TokenBlock) (*HeaderBlock, error) {
+ hdr := NewHeaderBlock()
+ if err := p.Next(); err != nil {
+ return hdr, err
+ }
+ return hdr, hdr.parse(p, block)
+}
+
+// EmbedBlock represents an "embed" content block.
+type EmbedBlock struct {
+ // Type is the content type (first word of block argument).
+ Type string
+
+ // URL is the content URL (second word of the block argument).
+ URL string
+
+ // Description is the content description (block body as simple text).
+ Description string
+}
+
+// Name returns the block name "embed".
+func (e *EmbedBlock) Name() string { return "embed" }
+
+// Args returns the block argument (type and URL).
+func (e *EmbedBlock) Args() []string {
+ if e.Type != "" && e.URL != "" {
+ return []string{e.Type, e.URL}
+ }
+ return []string{e.Type}
+}
+
+// WriteIndent writes the embed block header and contents indented by n tabs.
+func (e *EmbedBlock) WriteIndent(w io.Writer, n int) error {
+ if e.URL == "" {
+ return nil
+ }
+
+ s := e.Name() + " "
+ if e.Type == "" {
+ s += "*/*"
+ } else {
+ s += Escape(e.Type)
+ }
+ s += " " + Escape(e.URL)
+ if err := writeIndent(w, s, n); err != nil {
+ return err
+ }
+ if err := writeIndent(w, Escape(e.Description), n+1); err != nil {
+ return err
+ }
+ return nil
+}
+
+func parseContentEmbed(p *Parser, block *TokenBlock) (*EmbedBlock, error) {
+ embed := &EmbedBlock{}
+ if len(block.Args) >= 1 {
+ embed.Type = block.Args[0]
+ if len(block.Args) >= 2 {
+ embed.URL = block.Args[1]
+ }
+ }
+ if err := p.Next(); err != nil {
+ return embed, err
+ }
+ s, err := getSimpleText(p, block)
+ embed.Description = s
+ return embed, err
+}