~samwhited/xmpp

1a576c21e827479132579ba4f61c24c7ae224559 — Sam Whited 2 years ago 7e026ef
styling: new package
3 files changed, 302 insertions(+), 0 deletions(-)

A styling/preblock.go
A styling/style.go
A styling/style_test.go
A styling/preblock.go => styling/preblock.go +99 -0
@@ 0,0 1,99 @@
// Copyright 2018 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package styling

import (
	"bufio"
	"bytes"
	"io"
)

// TODO: refactor to get rid of the state stuff and just use bools. I originally
// did the state byte because I needed to be able to rewind on occasion and
// didn't want to store all these values and all the previous values (it was
// easier to store just one byte and one previous state byte) but now that
// optimization is no longer necessary.
const (
	inPre = 1 << iota
	notLineStart
	nextInPre
	exitingPre
)

type preBlockParser struct {
	r     *bufio.Reader
	state uint8
}

func (p *preBlockParser) Style() Style {
	if p.state&inPre != 0 {
		return PreBlock
	}
	return 0
}

func (p *preBlockParser) Read(b []byte) (n int, err error) {
	if p.state&nextInPre != 0 {
		p.state = (p.state &^ nextInPre) | inPre
	}

	for i := 0; i < len(b); i++ {
		// If we're at the start of a line
		if p.state&notLineStart == 0 {
			// If we're not already in a pre block, look for "```" (start pre)
			if p.state&inPre == 0 {
				peek, err := p.r.Peek(3)
				switch err {
				case bufio.ErrBufferFull:
					return n, nil
				case nil, io.EOF:
				default:
					return n, err
				}

				// We found the start of a pre block.
				if bytes.Equal(peek, []byte("```")) {
					if i > 0 {
						p.state |= nextInPre
						return n, err
					}
					p.state = inPre
				}
			} else {
				// If we are in a pre block, look for "```\n" (end pre)
				peek, err := p.r.Peek(4)
				switch err {
				case bufio.ErrBufferFull:
					return n, nil
				case nil, io.EOF:
				default:
					return n, err
				}

				// We found the end of the pre block.
				if bytes.Equal(peek, []byte("```\n")) {
					p.state |= exitingPre
				}
			}
		}

		bb, err := p.r.ReadByte()
		if err != nil {
			return n, err
		}
		b[i] = bb
		n++
		if bb == '\n' {
			if p.state&exitingPre != 0 {
				p.state = p.state &^ (exitingPre | inPre)
			}
			p.state = p.state &^ notLineStart
		} else {
			p.state |= notLineStart
		}
	}

	return n, err
}

A styling/style.go => styling/style.go +78 -0
@@ 0,0 1,78 @@
// Copyright 2018 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

// Package styling implements XEP-0393: Message Styling.
//
// For more information see:
// https://xmpp.org/extensions/xep-0393.html
//
// BE ADVISED: This package is experimental and the API is subject to change.
package styling

import (
	"bufio"
	"io"
)

// Style represents the currently active styles and blocks.
// For example, bytes between the styling directives in the span "_*Strong and
// emph*_" would have the style "Strong|Emph".
// Styling directives will have a marker indicating whether they are the start
// or end directive as well as the format itself.
// For example, the first "_" in the previous example would have the style:
// "StartEmph|Emph".
type Style uint32

// A list of possible styles and masks for accessing them.
const (
	// Spans
	Strong Style = 1 << iota
	Emph
	Pre
	Strike

	// Blocks
	PreBlock
	QuoteBlock

	// Masks
	Span  = Strong | Emph | Pre | Strike
	Block = PreBlock | QuoteBlock
)

// Parser reads message styling data from an underlying reader and returns the
// style of each byte.
type Parser struct {
	r *preBlockParser
}

// NewParser returns a Parser that reads data from the provided io.Reader.
// If r is not a bufio.Reader, Parser does its own buffering.
func NewParser(r io.Reader) Parser {
	p := Parser{}
	var buf *bufio.Reader
	if rr, ok := r.(*bufio.Reader); ok {
		buf = rr
	} else {
		buf = bufio.NewReader(r)
	}
	p.r = &preBlockParser{r: buf}
	return p
}

// Read reads data from the underlying reader and stops when the style would
// change.
func (p Parser) Read(b []byte) (n int, err error) {
	return p.r.Read(b)
}

// Style returns the style of the last byte read from the underlying reader.
func (p Parser) Style() Style {
	return p.r.Style()
}

type parser interface {
	io.Reader
	Style() Style
}

A styling/style_test.go => styling/style_test.go +125 -0
@@ 0,0 1,125 @@
// Copyright 2018 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package styling_test

import (
	"io"
	"strconv"
	"strings"
	"testing"

	"mellium.im/xmpp/styling"
)

var decoderTests = [...]struct {
	text   string
	bufs   []int
	reads  []string
	styles []styling.Style
	err    error
}{
	0: {err: io.EOF},
	1: {
		text:   "```ignored\next",
		bufs:   []int{len("```ignored\next")},
		reads:  []string{"```ignored\next"},
		styles: []styling.Style{styling.PreBlock},
		err:    io.EOF,
	},
	2: {
		text:   "```ignored\next",
		bufs:   []int{2, len("`ignored\next")},
		reads:  []string{"``", "`ignored\next"},
		styles: []styling.Style{styling.PreBlock, styling.PreBlock},
		err:    io.EOF,
	},
	3: {
		text:   "```",
		bufs:   []int{3},
		reads:  []string{"```"},
		styles: []styling.Style{styling.PreBlock},
		err:    io.EOF,
	},
	4: {
		text:   "line\n```",
		bufs:   []int{5, 3},
		reads:  []string{"line\n", "```"},
		styles: []styling.Style{0, styling.PreBlock},
		err:    io.EOF,
	},
	5: {
		text:   "line\n````",
		bufs:   []int{6, 4},
		reads:  []string{"line\n", "````"},
		styles: []styling.Style{0, styling.PreBlock},
		err:    io.EOF,
	},
	6: {
		text:   "line\n````\ntest\n```",
		bufs:   []int{6, len("````\ntest\n```")},
		reads:  []string{"line\n", "````\ntest\n```"},
		styles: []styling.Style{0, styling.PreBlock},
		err:    io.EOF,
	},
	7: {
		text:   "line\n````\ntest\n```\ntest",
		bufs:   []int{6, len("````\ntest\n```"), 5},
		reads:  []string{"line\n", "````\ntest\n```", "\ntest"},
		styles: []styling.Style{0, styling.PreBlock, 0},
		err:    io.EOF,
	},
	8: {
		text:   "line\n````\nte```st\n```\ntest",
		bufs:   []int{6, len("````\nte```st\n```"), 5},
		reads:  []string{"line\n", "````\nte```st\n```", "\ntest"},
		styles: []styling.Style{0, styling.PreBlock, 0},
		err:    io.EOF,
	},
}

func TestDecoder(t *testing.T) {
	for i, tc := range decoderTests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			parser := styling.NewParser(strings.NewReader(tc.text))

			var err error
			var n int
			var i int
			var out []byte
			for ; i < len(tc.bufs); i++ {
				curStyle := tc.styles[i]
				b := make([]byte, tc.bufs[i])
				n, err = parser.Read(b)
				if tc.reads[i] != string(b[:n]) {
					t.Errorf("Bad read: want=%q, got=%q", tc.reads[i], b[:n])
				}
				if s := parser.Style(); s != curStyle {
					t.Errorf("Wrong style: want=%q, got=%q", curStyle, s)
				}
				if err != nil {
					break
				}
				out = append(out, b[:n]...)
			}

			if err == nil {
				n, err = parser.Read(make([]byte, 10))
				if n != 0 {
					t.Errorf("Read after final returned unexpected bytes")
				}
			}

			if string(out) != tc.text {
				t.Errorf("Unexpected text: want=%q, got=%q", tc.text, out)
			}
			if i != len(tc.bufs) {
				t.Errorf("Wrong number of reads: want=%d, got=%d", len(tc.bufs), i)
			}
			if err != tc.err {
				t.Errorf("Unexpected error: want=%q, got=%q", tc.err, err)
			}
		})
	}
}