diff options
Diffstat (limited to 'content.go')
-rw-r--r-- | content.go | 610 |
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 +} |