blob: 2542836b5772de7760ea89d2d5ec7eff020544d0 [file] [log] [blame]
// Other than the custom stateFn's, much of this is copied from
// https://golang.org/src/pkg/text/template/parse/lex.go. Cannot use
// stuct embedding to reuse, since all the good parts are private.
package util
import (
"fmt"
"os"
"strings"
"unicode/utf8"
)
type Pos int
type CommandBlock struct {
labels []string
codeText string
}
func (x CommandBlock) GetLabels() []string {
return x.labels
}
func (x CommandBlock) GetCodeText() string {
return x.codeText
}
type item struct {
typ itemType // Type of this item.
val string // The value of this item.
}
func (i item) String() string {
switch {
case i.typ == itemEOF:
return "EOF"
case i.typ == itemError:
return i.val
case i.typ == itemBlockLabel:
return string(labelMarker) + i.val
case i.typ == itemCommandBlock:
return "--------\n" + i.val + "--------\n"
case len(i.val) > 10:
return fmt.Sprintf("%.30s...", i.val)
}
return fmt.Sprintf("%s", i.val)
}
type itemType int
const (
itemError itemType = iota
itemBlockLabel // Label for a command block
itemCommandBlock // All lines between codeFence marks
itemEOF
)
const (
labelMarker = '@'
commentOpen = "<!--"
commentClose = "-->"
codeFence = "```"
)
const eof = -1
type stateFn func(*lexer) stateFn
type lexer struct {
input string // string being scanned
state stateFn // the next lexing function to enter
pos Pos // current position in 'input'
start Pos // start of this item
width Pos // width of last rune read
items chan item // channel of scanned items
}
// next returns the next rune in the input.
func (l *lexer) next() rune {
if int(l.pos) >= len(l.input) {
l.width = 0
return eof
}
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = Pos(w)
l.pos += l.width
return r
}
func (l *lexer) peek() rune {
r := l.next()
l.backup()
return r
}
func (l *lexer) backup() {
l.pos -= l.width
}
func (l *lexer) emit(t itemType) {
l.items <- item{t, l.input[l.start:l.pos]}
l.start = l.pos
}
func (l *lexer) ignore() {
l.start = l.pos
}
// Consumes the next rune if it's from the valid set.
func (l *lexer) accept(valid string) bool {
if strings.IndexRune(valid, l.next()) >= 0 {
return true
}
l.backup()
return false
}
// Consumes a run of runes from the valid set
func (l *lexer) acceptRun(valid string) {
// is the next character of the input an element
// of the (defining) 'valid' set of runes (a string).
for strings.IndexRune(valid, l.next()) >= 0 {
}
l.backup()
}
func (l *lexer) acceptWord() {
l.acceptRun("012345789abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
}
// errorf returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem.
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
l.items <- item{itemError, fmt.Sprintf(format, args...)}
return nil
}
// nextItem returns the next item from the input.
func (l *lexer) nextItem() item {
item := <-l.items
return item
}
// newLex creates a new scanner for the input string.
func newLex(input string) *lexer {
l := &lexer{
input: input,
items: make(chan item),
}
go l.run()
return l
}
func (l *lexer) run() {
for l.state = lexText; l.state != nil; {
l.state = l.state(l)
}
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isEndOfLine(r rune) bool {
return r == '\r' || r == '\n'
}
// lexText scans until an opening comment delimiter.
func lexText(l *lexer) stateFn {
for {
if strings.HasPrefix(l.input[l.pos:], commentOpen) {
return lexPutativeComment
}
if l.next() == eof {
l.ignore()
l.emit(itemEOF)
return nil
}
}
}
// Move to lexing a command block intended for a particular script, or to
// lexing a simple comment. Comment opener known to be present.
func lexPutativeComment(l *lexer) stateFn {
l.pos += Pos(len(commentOpen))
for {
switch r := l.next(); {
case isSpace(r):
l.ignore()
case r == labelMarker:
l.backup()
return lexBlockLabels
default:
l.backup()
return lexCommentRemainder
}
}
}
// lexCommentRemainder assumes a comment opener was read,
// and eats everything up to and including the comment closer.
func lexCommentRemainder(l *lexer) stateFn {
i := strings.Index(l.input[l.pos:], commentClose)
if i < 0 {
return l.errorf("unclosed comment")
}
l.pos += Pos(i + len(commentClose))
l.ignore()
return lexText
}
// lexBlockLabels scans a string like "@1 @hey" emitting the labels
// "1" and "hey". LabelMarker known to be present.
func lexBlockLabels(l *lexer) stateFn {
for {
switch r := l.next(); {
case r == eof || isEndOfLine(r):
return l.errorf("unclosed block label sequence")
case isSpace(r):
l.ignore()
case r == labelMarker:
l.ignore()
l.acceptWord()
if l.width == 0 {
return l.errorf("empty block label")
}
l.emit(itemBlockLabel)
default:
l.backup()
if !strings.HasPrefix(l.input[l.pos:], commentClose) {
return l.errorf("improperly closed block label sequence")
}
l.pos += Pos(len(commentClose))
l.ignore()
l.acceptRun(" \t")
l.ignore()
r := l.next()
if r != '\n' && r != '\r' {
return l.errorf("Expected command block marker at start of line.")
}
l.ignore()
if !strings.HasPrefix(l.input[l.pos:], codeFence) {
return l.errorf("Expected command block mark, got: " + l.input[l.pos:])
}
return lexCommandBlock
}
}
}
// lexCommandBlock scans a command block. Initial marker known to be present.
func lexCommandBlock(l *lexer) stateFn {
l.pos += Pos(len(codeFence))
l.ignore()
// Ignore any language specifier.
if idx := strings.Index(l.input[l.pos:], "\n"); idx > -1 {
l.pos += Pos(idx) + 1
l.ignore()
}
for {
if strings.HasPrefix(l.input[l.pos:], codeFence) {
if l.pos > l.start {
l.emit(itemCommandBlock)
}
l.pos += Pos(len(codeFence))
l.ignore()
return lexText
}
if l.next() == eof {
return l.errorf("unclosed command block")
}
}
}
func shouldSleep(labels []string) bool {
for _, label := range labels {
if label == "sleep" {
return true
}
}
return false
}
// Parse lexes the incoming string into a mapping from block label to
// CommandBlock array. The labels are the strings after a labelMarker in
// a comment preceding a command block. Arrays hold command blocks in the
// order they appeared in the input.
func Parse(s string) (result map[string][]*CommandBlock) {
result = make(map[string][]*CommandBlock)
currentLabels := make([]string, 0, 10)
l := newLex(s)
for {
item := l.nextItem()
switch {
case item.typ == itemEOF || item.typ == itemError:
return
case item.typ == itemBlockLabel:
currentLabels = append(currentLabels, item.val)
case item.typ == itemCommandBlock:
if len(currentLabels) == 0 {
fmt.Println("Have an unlabelled command block:\n " + item.val)
os.Exit(1)
}
// If the command block has a 'sleep' label, add a brief sleep
// at the end. This is hack to give servers placed in the
// background time to start.
if shouldSleep(currentLabels) {
item.val = item.val + "sleep 2s # Added by mdrip\n"
}
newBlock := &CommandBlock{currentLabels, item.val}
for _, label := range currentLabels {
blocks, ok := result[label]
if ok {
blocks = append(blocks, newBlock)
} else {
blocks = []*CommandBlock{newBlock}
}
result[label] = blocks
}
currentLabels = make([]string, 0, 10)
}
}
}