-
Notifications
You must be signed in to change notification settings - Fork 0
/
elems.go
130 lines (116 loc) · 3.06 KB
/
elems.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package rx
import (
"errors"
"io"
"sync"
)
var cachedParseResults = make(map[string]qVM)
var cachedParseResultsMx sync.Mutex
// Get parses the Get text in tpl, and returns a node matching it.
// This is a short hand for the [GetNode], [Node.AddAttr], … constructors, optimized for easy definition.
// The node returned can be further modified.
//
// When used in the short form (defining a single element), the element need not be closed:
//
// Get(`<div class="flex">`)
//
// Get can also create a full tree:
//
// Get(`<div class="flex"><button>Click me</button></div>`)
//
// Get panics if the template is not valid, and should not be used for untrusted inputs.
// Get uses a caching mechanism to prevent unecessary parsing, so it works best with static strings.
func Get(tpl string) *Node {
cachedParseResultsMx.Lock()
defer cachedParseResultsMx.Unlock()
if ctor := cachedParseResults[tpl]; ctor != nil {
return ctor.run(tpl)
}
var vm qVM
z := newTokenizer(tpl)
for {
tt := z.Next()
switch tt {
case errorToken:
if errors.Is(z.Err(), io.EOF) {
cachedParseResults[tpl] = vm
return vm.run(tpl)
}
panic(z.Err())
case textToken:
val := z.Text()
vm = append(vm, qVMOp{Op: qText, R1: val.Start, R2: val.End})
case startTagToken:
tn, hasattr := z.TagName()
vm = append(vm, qVMOp{Op: qNode, R1: tn.Start, R2: tn.End})
for hasattr {
var key, val span
key, val, hasattr = z.TagAttr()
if tpl[key.Start:key.End] == "class" {
vm = append(vm, qVMOp{Op: qClasses, R1: val.Start, R2: val.End})
} else {
vm = append(vm, qVMOp{Op: qAttrs, R1: key.Start, R2: key.End, R3: val.Start, R4: val.End})
}
}
case selfClosingTagToken:
tn, hasattr := z.TagName()
vm = append(vm, qVMOp{Op: qNode, R1: tn.Start, R2: tn.End})
for hasattr {
var key, val span
key, val, hasattr = z.TagAttr()
if tpl[key.Start:key.End] == "class" {
vm = append(vm, qVMOp{Op: qClasses, R1: val.Start, R2: val.End})
} else {
vm = append(vm, qVMOp{Op: qAttrs, R1: key.Start, R2: key.End, R3: val.Start, R4: val.End})
}
}
vm = append(vm, qVMOp{Op: qTerm})
case endTagToken:
vm = append(vm, qVMOp{Op: qTerm})
}
}
}
type qVMOp struct {
Op byte
R1, R2 int // op-dependent
R3, R4 int // op-dependent
}
const (
qTerm byte = iota
qNode
qAttrs
qClasses
qText
)
type qVM []qVMOp
func (vm qVM) run(tpl string) *Node {
p, stack := new(Node), make([]*Node, 0, 16)
for _, op := range vm {
switch op.Op {
case qTerm:
p, stack = pop(stack)
case qNode:
if p.TagName == "" {
p.TagName = tpl[op.R1:op.R2]
stack = append(stack, p) // store root twice so pop in qTerm always a good op
continue
}
c := GetNode(tpl[op.R1:op.R2])
p.AddChildren(c)
p, stack = c, append(stack, p)
case qAttrs:
p.AddAttr(tpl[op.R1:op.R2], tpl[op.R3:op.R4])
case qClasses:
p.Classes = tpl[op.R1:op.R2]
case qText:
p.Text = tpl[op.R1:op.R2]
}
}
if p.TagName == "" {
panic("invalid tag")
}
return p
}
func pop(stack []*Node) (*Node, []*Node) {
return stack[len(stack)-1], stack[:len(stack)-1]
}