| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | package parse |
| |
|
| | import ( |
| | "bytes" |
| | "fmt" |
| | "runtime" |
| | "strconv" |
| | "strings" |
| | ) |
| |
|
| | |
| | type Tree struct { |
| | Name string |
| | ParseName string |
| | Root *ListNode |
| | Mode Mode |
| | text string |
| | |
| | funcs []map[string]any |
| | lex *lexer |
| | token [3]item |
| | peekCount int |
| | vars []string |
| | treeSet map[string]*Tree |
| | actionLine int |
| | rangeDepth int |
| | stackDepth int |
| | } |
| |
|
| | |
| | type Mode uint |
| |
|
| | const ( |
| | ParseComments Mode = 1 << iota |
| | SkipFuncCheck |
| | ) |
| |
|
| | |
| | |
| | var maxStackDepth = 10000 |
| |
|
| | |
| | func init() { |
| | if runtime.GOARCH == "wasm" { |
| | maxStackDepth = 1000 |
| | } |
| | } |
| |
|
| | |
| | func (t *Tree) Copy() *Tree { |
| | if t == nil { |
| | return nil |
| | } |
| | return &Tree{ |
| | Name: t.Name, |
| | ParseName: t.ParseName, |
| | Root: t.Root.CopyList(), |
| | text: t.text, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]any) (map[string]*Tree, error) { |
| | treeSet := make(map[string]*Tree) |
| | t := New(name) |
| | t.text = text |
| | _, err := t.Parse(text, leftDelim, rightDelim, treeSet, funcs...) |
| | return treeSet, err |
| | } |
| |
|
| | |
| | func (t *Tree) next() item { |
| | if t.peekCount > 0 { |
| | t.peekCount-- |
| | } else { |
| | t.token[0] = t.lex.nextItem() |
| | } |
| | return t.token[t.peekCount] |
| | } |
| |
|
| | |
| | func (t *Tree) backup() { |
| | t.peekCount++ |
| | } |
| |
|
| | |
| | |
| | func (t *Tree) backup2(t1 item) { |
| | t.token[1] = t1 |
| | t.peekCount = 2 |
| | } |
| |
|
| | |
| | |
| | func (t *Tree) backup3(t2, t1 item) { |
| | t.token[1] = t1 |
| | t.token[2] = t2 |
| | t.peekCount = 3 |
| | } |
| |
|
| | |
| | func (t *Tree) peek() item { |
| | if t.peekCount > 0 { |
| | return t.token[t.peekCount-1] |
| | } |
| | t.peekCount = 1 |
| | t.token[0] = t.lex.nextItem() |
| | return t.token[0] |
| | } |
| |
|
| | |
| | func (t *Tree) nextNonSpace() (token item) { |
| | for { |
| | token = t.next() |
| | if token.typ != itemSpace { |
| | break |
| | } |
| | } |
| | return token |
| | } |
| |
|
| | |
| | func (t *Tree) peekNonSpace() item { |
| | token := t.nextNonSpace() |
| | t.backup() |
| | return token |
| | } |
| |
|
| | |
| |
|
| | |
| | func New(name string, funcs ...map[string]any) *Tree { |
| | return &Tree{ |
| | Name: name, |
| | funcs: funcs, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func (t *Tree) ErrorContext(n Node) (location, context string) { |
| | pos := int(n.Position()) |
| | tree := n.tree() |
| | if tree == nil { |
| | tree = t |
| | } |
| | text := tree.text[:pos] |
| | byteNum := strings.LastIndex(text, "\n") |
| | if byteNum == -1 { |
| | byteNum = pos |
| | } else { |
| | byteNum++ |
| | byteNum = pos - byteNum |
| | } |
| | lineNum := 1 + strings.Count(text, "\n") |
| | context = n.String() |
| | return fmt.Sprintf("%s:%d:%d", tree.ParseName, lineNum, byteNum), context |
| | } |
| |
|
| | |
| | func (t *Tree) errorf(format string, args ...any) { |
| | t.Root = nil |
| | format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format) |
| | panic(fmt.Errorf(format, args...)) |
| | } |
| |
|
| | |
| | func (t *Tree) error(err error) { |
| | t.errorf("%s", err) |
| | } |
| |
|
| | |
| | func (t *Tree) expect(expected itemType, context string) item { |
| | token := t.nextNonSpace() |
| | if token.typ != expected { |
| | t.unexpected(token, context) |
| | } |
| | return token |
| | } |
| |
|
| | |
| | func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item { |
| | token := t.nextNonSpace() |
| | if token.typ != expected1 && token.typ != expected2 { |
| | t.unexpected(token, context) |
| | } |
| | return token |
| | } |
| |
|
| | |
| | func (t *Tree) unexpected(token item, context string) { |
| | if token.typ == itemError { |
| | extra := "" |
| | if t.actionLine != 0 && t.actionLine != token.line { |
| | extra = fmt.Sprintf(" in action started at %s:%d", t.ParseName, t.actionLine) |
| | if strings.HasSuffix(token.val, " action") { |
| | extra = extra[len(" in action"):] |
| | } |
| | } |
| | t.errorf("%s%s", token, extra) |
| | } |
| | t.errorf("unexpected %s in %s", token, context) |
| | } |
| |
|
| | |
| | func (t *Tree) recover(errp *error) { |
| | e := recover() |
| | if e != nil { |
| | if _, ok := e.(runtime.Error); ok { |
| | panic(e) |
| | } |
| | if t != nil { |
| | t.stopParse() |
| | } |
| | *errp = e.(error) |
| | } |
| | } |
| |
|
| | |
| | func (t *Tree) startParse(funcs []map[string]any, lex *lexer, treeSet map[string]*Tree) { |
| | t.Root = nil |
| | t.lex = lex |
| | t.vars = []string{"$"} |
| | t.funcs = funcs |
| | t.treeSet = treeSet |
| | t.stackDepth = 0 |
| | lex.options = lexOptions{ |
| | emitComment: t.Mode&ParseComments != 0, |
| | breakOK: !t.hasFunction("break"), |
| | continueOK: !t.hasFunction("continue"), |
| | } |
| | } |
| |
|
| | |
| | func (t *Tree) stopParse() { |
| | t.lex = nil |
| | t.vars = nil |
| | t.funcs = nil |
| | t.treeSet = nil |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) { |
| | defer t.recover(&err) |
| | t.ParseName = t.Name |
| | lexer := lex(t.Name, text, leftDelim, rightDelim) |
| | t.startParse(funcs, lexer, treeSet) |
| | t.text = text |
| | t.parse() |
| | t.add() |
| | t.stopParse() |
| | return t, nil |
| | } |
| |
|
| | |
| | func (t *Tree) add() { |
| | tree := t.treeSet[t.Name] |
| | if tree == nil || IsEmptyTree(tree.Root) { |
| | t.treeSet[t.Name] = t |
| | return |
| | } |
| | if !IsEmptyTree(t.Root) { |
| | t.errorf("template: multiple definition of template %q", t.Name) |
| | } |
| | } |
| |
|
| | |
| | func IsEmptyTree(n Node) bool { |
| | switch n := n.(type) { |
| | case nil: |
| | return true |
| | case *ActionNode: |
| | case *CommentNode: |
| | return true |
| | case *IfNode: |
| | case *ListNode: |
| | for _, node := range n.Nodes { |
| | if !IsEmptyTree(node) { |
| | return false |
| | } |
| | } |
| | return true |
| | case *RangeNode: |
| | case *TemplateNode: |
| | case *TextNode: |
| | return len(bytes.TrimSpace(n.Text)) == 0 |
| | case *WithNode: |
| | default: |
| | panic("unknown node: " + n.String()) |
| | } |
| | return false |
| | } |
| |
|
| | |
| | |
| | |
| | func (t *Tree) parse() { |
| | t.Root = t.newList(t.peek().pos) |
| | for t.peek().typ != itemEOF { |
| | if t.peek().typ == itemLeftDelim { |
| | delim := t.next() |
| | if t.nextNonSpace().typ == itemDefine { |
| | newT := New("definition") |
| | newT.text = t.text |
| | newT.Mode = t.Mode |
| | newT.ParseName = t.ParseName |
| | newT.startParse(t.funcs, t.lex, t.treeSet) |
| | newT.parseDefinition() |
| | continue |
| | } |
| | t.backup2(delim) |
| | } |
| | switch n := t.textOrAction(); n.Type() { |
| | case nodeEnd, nodeElse: |
| | t.errorf("unexpected %s", n) |
| | default: |
| | t.Root.append(n) |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | func (t *Tree) parseDefinition() { |
| | const context = "define clause" |
| | name := t.expectOneOf(itemString, itemRawString, context) |
| | var err error |
| | t.Name, err = strconv.Unquote(name.val) |
| | if err != nil { |
| | t.error(err) |
| | } |
| | t.expect(itemRightDelim, context) |
| | var end Node |
| | t.Root, end = t.itemList() |
| | if end.Type() != nodeEnd { |
| | t.errorf("unexpected %s in %s", end, context) |
| | } |
| | t.add() |
| | t.stopParse() |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) itemList() (list *ListNode, next Node) { |
| | list = t.newList(t.peekNonSpace().pos) |
| | for t.peekNonSpace().typ != itemEOF { |
| | n := t.textOrAction() |
| | switch n.Type() { |
| | case nodeEnd, nodeElse: |
| | return list, n |
| | } |
| | list.append(n) |
| | } |
| | t.errorf("unexpected EOF") |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| | func (t *Tree) textOrAction() Node { |
| | switch token := t.nextNonSpace(); token.typ { |
| | case itemText: |
| | return t.newText(token.pos, token.val) |
| | case itemLeftDelim: |
| | t.actionLine = token.line |
| | defer t.clearActionLine() |
| | return t.action() |
| | case itemComment: |
| | return t.newComment(token.pos, token.val) |
| | default: |
| | t.unexpected(token, "input") |
| | } |
| | return nil |
| | } |
| |
|
| | func (t *Tree) clearActionLine() { |
| | t.actionLine = 0 |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) action() (n Node) { |
| | switch token := t.nextNonSpace(); token.typ { |
| | case itemBlock: |
| | return t.blockControl() |
| | case itemBreak: |
| | return t.breakControl(token.pos, token.line) |
| | case itemContinue: |
| | return t.continueControl(token.pos, token.line) |
| | case itemElse: |
| | return t.elseControl() |
| | case itemEnd: |
| | return t.endControl() |
| | case itemIf: |
| | return t.ifControl() |
| | case itemRange: |
| | return t.rangeControl() |
| | case itemTemplate: |
| | return t.templateControl() |
| | case itemWith: |
| | return t.withControl() |
| | } |
| | t.backup() |
| | token := t.peek() |
| | |
| | return t.newAction(token.pos, token.line, t.pipeline("command", itemRightDelim)) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) breakControl(pos Pos, line int) Node { |
| | if token := t.nextNonSpace(); token.typ != itemRightDelim { |
| | t.unexpected(token, "{{break}}") |
| | } |
| | if t.rangeDepth == 0 { |
| | t.errorf("{{break}} outside {{range}}") |
| | } |
| | return t.newBreak(pos, line) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) continueControl(pos Pos, line int) Node { |
| | if token := t.nextNonSpace(); token.typ != itemRightDelim { |
| | t.unexpected(token, "{{continue}}") |
| | } |
| | if t.rangeDepth == 0 { |
| | t.errorf("{{continue}} outside {{range}}") |
| | } |
| | return t.newContinue(pos, line) |
| | } |
| |
|
| | |
| | |
| | |
| | func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) { |
| | token := t.peekNonSpace() |
| | pipe = t.newPipeline(token.pos, token.line, nil) |
| | |
| | decls: |
| | if v := t.peekNonSpace(); v.typ == itemVariable { |
| | t.next() |
| | |
| | |
| | |
| | |
| | tokenAfterVariable := t.peek() |
| | next := t.peekNonSpace() |
| | switch { |
| | case next.typ == itemAssign, next.typ == itemDeclare: |
| | pipe.IsAssign = next.typ == itemAssign |
| | t.nextNonSpace() |
| | pipe.Decl = append(pipe.Decl, t.newVariable(v.pos, v.val)) |
| | t.vars = append(t.vars, v.val) |
| | case next.typ == itemChar && next.val == ",": |
| | t.nextNonSpace() |
| | pipe.Decl = append(pipe.Decl, t.newVariable(v.pos, v.val)) |
| | t.vars = append(t.vars, v.val) |
| | if context == "range" && len(pipe.Decl) < 2 { |
| | switch t.peekNonSpace().typ { |
| | case itemVariable, itemRightDelim, itemRightParen: |
| | |
| | goto decls |
| | default: |
| | t.errorf("range can only initialize variables") |
| | } |
| | } |
| | t.errorf("too many declarations in %s", context) |
| | case tokenAfterVariable.typ == itemSpace: |
| | t.backup3(v, tokenAfterVariable) |
| | default: |
| | t.backup2(v) |
| | } |
| | } |
| | for { |
| | switch token := t.nextNonSpace(); token.typ { |
| | case end: |
| | |
| | t.checkPipeline(pipe, context) |
| | return |
| | case itemBool, itemCharConstant, itemComplex, itemDot, itemField, itemIdentifier, |
| | itemNumber, itemNil, itemRawString, itemString, itemVariable, itemLeftParen: |
| | t.backup() |
| | pipe.append(t.command()) |
| | default: |
| | t.unexpected(token, context) |
| | } |
| | } |
| | } |
| |
|
| | func (t *Tree) checkPipeline(pipe *PipeNode, context string) { |
| | |
| | if len(pipe.Cmds) == 0 { |
| | t.errorf("missing value for %s", context) |
| | } |
| | |
| | for i, c := range pipe.Cmds[1:] { |
| | switch c.Args[0].Type() { |
| | case NodeBool, NodeDot, NodeNil, NodeNumber, NodeString: |
| | |
| | t.errorf("non executable command in pipeline stage %d", i+2) |
| | } |
| | } |
| | } |
| |
|
| | func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { |
| | defer t.popVars(len(t.vars)) |
| | pipe = t.pipeline(context, itemRightDelim) |
| | if context == "range" { |
| | t.rangeDepth++ |
| | } |
| | var next Node |
| | list, next = t.itemList() |
| | if context == "range" { |
| | t.rangeDepth-- |
| | } |
| | switch next.Type() { |
| | case nodeEnd: |
| | case nodeElse: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if context == "if" && t.peek().typ == itemIf { |
| | t.next() |
| | elseList = t.newList(next.Position()) |
| | elseList.append(t.ifControl()) |
| | } else if context == "with" && t.peek().typ == itemWith { |
| | t.next() |
| | elseList = t.newList(next.Position()) |
| | elseList.append(t.withControl()) |
| | } else { |
| | elseList, next = t.itemList() |
| | if next.Type() != nodeEnd { |
| | t.errorf("expected end; found %s", next) |
| | } |
| | } |
| | } |
| | return pipe.Position(), pipe.Line, pipe, list, elseList |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) ifControl() Node { |
| | return t.newIf(t.parseControl("if")) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) rangeControl() Node { |
| | r := t.newRange(t.parseControl("range")) |
| | return r |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) withControl() Node { |
| | return t.newWith(t.parseControl("with")) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) endControl() Node { |
| | return t.newEnd(t.expect(itemRightDelim, "end").pos) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) elseControl() Node { |
| | peek := t.peekNonSpace() |
| | |
| | |
| | |
| | if peek.typ == itemIf || peek.typ == itemWith { |
| | return t.newElse(peek.pos, peek.line) |
| | } |
| | token := t.expect(itemRightDelim, "else") |
| | return t.newElse(token.pos, token.line) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) blockControl() Node { |
| | const context = "block clause" |
| |
|
| | token := t.nextNonSpace() |
| | name := t.parseTemplateName(token, context) |
| | pipe := t.pipeline(context, itemRightDelim) |
| |
|
| | block := New(name) |
| | block.text = t.text |
| | block.Mode = t.Mode |
| | block.ParseName = t.ParseName |
| | block.startParse(t.funcs, t.lex, t.treeSet) |
| | var end Node |
| | block.Root, end = block.itemList() |
| | if end.Type() != nodeEnd { |
| | t.errorf("unexpected %s in %s", end, context) |
| | } |
| | block.add() |
| | block.stopParse() |
| |
|
| | return t.newTemplate(token.pos, token.line, name, pipe) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) templateControl() Node { |
| | const context = "template clause" |
| | token := t.nextNonSpace() |
| | name := t.parseTemplateName(token, context) |
| | var pipe *PipeNode |
| | if t.nextNonSpace().typ != itemRightDelim { |
| | t.backup() |
| | |
| | pipe = t.pipeline(context, itemRightDelim) |
| | } |
| | return t.newTemplate(token.pos, token.line, name, pipe) |
| | } |
| |
|
| | func (t *Tree) parseTemplateName(token item, context string) (name string) { |
| | switch token.typ { |
| | case itemString, itemRawString: |
| | s, err := strconv.Unquote(token.val) |
| | if err != nil { |
| | t.error(err) |
| | } |
| | name = s |
| | default: |
| | t.unexpected(token, context) |
| | } |
| | return |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) command() *CommandNode { |
| | cmd := t.newCommand(t.peekNonSpace().pos) |
| | for { |
| | t.peekNonSpace() |
| | operand := t.operand() |
| | if operand != nil { |
| | cmd.append(operand) |
| | } |
| | switch token := t.next(); token.typ { |
| | case itemSpace: |
| | continue |
| | case itemRightDelim, itemRightParen: |
| | t.backup() |
| | case itemPipe: |
| | |
| | default: |
| | t.unexpected(token, "operand") |
| | } |
| | break |
| | } |
| | if len(cmd.Args) == 0 { |
| | t.errorf("empty command") |
| | } |
| | return cmd |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) operand() Node { |
| | node := t.term() |
| | if node == nil { |
| | return nil |
| | } |
| | if t.peek().typ == itemField { |
| | chain := t.newChain(t.peek().pos, node) |
| | for t.peek().typ == itemField { |
| | chain.Add(t.next().val) |
| | } |
| | |
| | |
| | |
| | |
| | |
| | switch node.Type() { |
| | case NodeField: |
| | node = t.newField(chain.Position(), chain.String()) |
| | case NodeVariable: |
| | node = t.newVariable(chain.Position(), chain.String()) |
| | case NodeBool, NodeString, NodeNumber, NodeNil, NodeDot: |
| | t.errorf("unexpected . after term %q", node.String()) |
| | default: |
| | node = chain |
| | } |
| | } |
| | return node |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (t *Tree) term() Node { |
| | switch token := t.nextNonSpace(); token.typ { |
| | case itemIdentifier: |
| | checkFunc := t.Mode&SkipFuncCheck == 0 |
| | if checkFunc && !t.hasFunction(token.val) { |
| | t.errorf("function %q not defined", token.val) |
| | } |
| | return NewIdentifier(token.val).SetTree(t).SetPos(token.pos) |
| | case itemDot: |
| | return t.newDot(token.pos) |
| | case itemNil: |
| | return t.newNil(token.pos) |
| | case itemVariable: |
| | return t.useVar(token.pos, token.val) |
| | case itemField: |
| | return t.newField(token.pos, token.val) |
| | case itemBool: |
| | return t.newBool(token.pos, token.val == "true") |
| | case itemCharConstant, itemComplex, itemNumber: |
| | number, err := t.newNumber(token.pos, token.val, token.typ) |
| | if err != nil { |
| | t.error(err) |
| | } |
| | return number |
| | case itemLeftParen: |
| | if t.stackDepth >= maxStackDepth { |
| | t.errorf("max expression depth exceeded") |
| | } |
| | t.stackDepth++ |
| | defer func() { t.stackDepth-- }() |
| | return t.pipeline("parenthesized pipeline", itemRightParen) |
| | case itemString, itemRawString: |
| | s, err := strconv.Unquote(token.val) |
| | if err != nil { |
| | t.error(err) |
| | } |
| | return t.newString(token.pos, token.val, s) |
| | } |
| | t.backup() |
| | return nil |
| | } |
| |
|
| | |
| | func (t *Tree) hasFunction(name string) bool { |
| | for _, funcMap := range t.funcs { |
| | if funcMap == nil { |
| | continue |
| | } |
| | if funcMap[name] != nil { |
| | return true |
| | } |
| | } |
| | return false |
| | } |
| |
|
| | |
| | func (t *Tree) popVars(n int) { |
| | t.vars = t.vars[:n] |
| | } |
| |
|
| | |
| | |
| | func (t *Tree) useVar(pos Pos, name string) Node { |
| | v := t.newVariable(pos, name) |
| | for _, varName := range t.vars { |
| | if varName == v.Ident[0] { |
| | return v |
| | } |
| | } |
| | t.errorf("undefined variable %q", v.Ident[0]) |
| | return nil |
| | } |
| |
|