bbcode - processor.go

package bbcode

import (
	"bytes"
	"io"
	"strings"

	"vimagination.zapto.org/parser"
)

// Processor contains methods necessary for creating custom Handler's.
type Processor struct {
	w      io.Writer
	err    error
	p      parser.Parser
	bbCode *BBCode
}

// Write writes to the underlying writer.
// The error is stored and does not need to be handled.
func (p *Processor) Write(b []byte) (int, error) {
	if p.err != nil {
		return 0, p.err
	}

	var n int

	n, p.err = p.w.Write(b)

	return n, p.err
}

// Process will continue processing the bbCode until it gets to an end tag
// which matches the tag name given, or until it reaches the end of the input.
// It returns true if the end tag was found, or false otherwise.
func (p *Processor) Process(untilTag string) bool {
	for {
		switch t := p.Get().(type) {
		case Text:
			p.printText(t)
		case OpenTag:
			p.ProcessTag(t)
		case CloseTag:
			if strings.EqualFold(t.Name, untilTag) {
				return true
			}

			p.printCloseTag(t)
		default:
			return false
		}
	}
}

// GetContents grabs the raw contents of a tag and returns it as a string.
func (p *Processor) GetContents(untilTag string) string {
	if p.err != nil {
		return ""
	}

	w := p.w
	b := bytes.NewBuffer(make([]byte, 0, 1024))
	p.w = b
	t := p.bbCode.text
	p.bbCode.text = PlainText

Loop:
	for {
		switch t := p.Get().(type) {
		case Text:
			p.printText(t)
		case OpenTag:
			p.printOpenTag(t)
		case CloseTag:
			if strings.EqualFold(t.Name, untilTag) {
				break Loop
			}

			p.printCloseTag(t)
		default:
			break Loop
		}
	}

	p.bbCode.text = t
	p.w = w

	return b.String()
}

// ProcessTag will process the given tag as normal.
func (p *Processor) ProcessTag(t OpenTag) {
	h := p.getTagHandler(t.Name)

	if h == nil {
		p.printOpenTag(t)
	} else {
		var attr string

		if t.Attr != nil {
			attr = *t.Attr
		}

		h.Handle(p, attr)
	}
}

func (p *Processor) getTagHandler(name string) Handler {
	for _, tag := range p.bbCode.tags {
		if strings.EqualFold(tag.Name(), name) {
			return tag
		}
	}

	return nil
}

// Get returns the next token.
// It will be either a Text, OpenTag or a CloseTag.
func (p *Processor) Get() interface{} {
	phrase, _ := p.p.GetPhrase()

	switch phrase.Type {
	case phraseText:
		text := make(Text, 0, len(phrase.Data))

		for _, t := range phrase.Data {
			text = append(text, t.Data)
		}

		return text
	case phraseOpen:
		tag := OpenTag{
			Name: phrase.Data[0].Data,
		}

		if len(phrase.Data) > 1 {
			tag.Attr = &phrase.Data[1].Data
		}

		return tag
	case phraseClose:
		return CloseTag{Name: phrase.Data[0].Data}
	}

	return nil
}

// Print writes the textual representation of the given token to the output,
// using the text Handler.
func (p *Processor) Print(t interface{}) {
	switch t := t.(type) {
	case string:
		p.bbCode.text.Handle(p, t)
	case Text:
		p.printText(t)
	case OpenTag:
		p.printOpenTag(t)
	case CloseTag:
		p.printCloseTag(t)
	}
}

func (p *Processor) printText(t Text) {
	for _, str := range t {
		p.bbCode.text.Handle(p, str)
	}
}

func (p *Processor) printOpenTag(t OpenTag) {
	p.bbCode.text.Handle(p, p.bbCode.tks.openTag)
	p.bbCode.text.Handle(p, t.Name)

	if t.Attr != nil {
		p.bbCode.text.Handle(p, p.bbCode.tks.attributeSep)
		p.bbCode.text.Handle(p, *t.Attr)
	}

	p.bbCode.text.Handle(p, p.bbCode.tks.closeTag)
}

func (p *Processor) printCloseTag(t CloseTag) {
	p.bbCode.text.Handle(p, p.bbCode.tks.openTag)
	p.bbCode.text.Handle(p, p.bbCode.tks.closingTag)
	p.bbCode.text.Handle(p, t.Name)
	p.bbCode.text.Handle(p, p.bbCode.tks.closeTag)
}

// Text is a token containing simple textual data.
type Text []string

// OpenTag is a token containing the name of the tag and a possible attribute..
type OpenTag struct {
	Name string
	Attr *string
}

// CloseTag is a token containing the name of the tag.
type CloseTag struct {
	Name string
}