v0.1.0 28f9d6e

tokens.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"strings"
7
8	"bnema.dev/ermite-actions/expr"
9)
10
11// Kind is the type of a [Token], numbered as in the reference.
12type Kind int
13
14// The token kinds.
15const (
16	KindString Kind = iota
17	KindSequence
18	KindMapping
19	KindBasicExpression
20	KindInsertExpression
21	KindBoolean
22	KindNumber
23	KindNull
24)
25
26// String returns the reference's name for the kind, such as "StringToken".
27func (k Kind) String() string {
28	switch k {
29	case KindString:
30		return "StringToken"
31	case KindSequence:
32		return "SequenceToken"
33	case KindMapping:
34		return "MappingToken"
35	case KindBasicExpression:
36		return "BasicExpressionToken"
37	case KindInsertExpression:
38		return "InsertExpressionToken"
39	case KindBoolean:
40		return "BooleanToken"
41	case KindNumber:
42		return "NumberToken"
43	case KindNull:
44		return "NullToken"
45	}
46	return "unknown"
47}
48
49// Pos is a position in a source file. Line and Col are 1-based; the zero
50// value means the position is unknown (a token made by the program).
51type Pos struct{ Line, Col int }
52
53// Token is a node of a template: a scalar, a sequence or a mapping. The
54// concrete types are [*String], [*Number], [*Boolean], [*Null],
55// [*Sequence], [*Mapping], [*BasicExpression] and [*InsertExpression].
56type Token interface {
57	// Kind reports the type of the token.
58	Kind() Kind
59	// File is the name of the file the token was read from, or "".
60	File() string
61	// Pos is where the token starts in File.
62	Pos() Pos
63	// Definition is the schema definition the token was read against, or nil.
64	Definition() *DefinitionInfo
65	// String is the text the reference shows in messages: the value of a
66	// literal, "${{ expr }}" for an expression.
67	String() string
68	base() *tokenBase
69}
70
71type tokenBase struct {
72	file string
73	pos  Pos
74	def  *DefinitionInfo
75}
76
77func (m *tokenBase) File() string                { return m.file }
78func (m *tokenBase) Pos() Pos                    { return m.pos }
79func (m *tokenBase) Definition() *DefinitionInfo { return m.def }
80func (m *tokenBase) base() *tokenBase            { return m }
81
82// Origin is where a token comes from; the zero value is a token made by the
83// program.
84type Origin struct {
85	File string
86	Pos  Pos
87}
88
89func mk(o Origin) tokenBase { return tokenBase{file: o.File, pos: o.Pos} }
90
91// String is a string literal.
92type String struct {
93	tokenBase
94	Value string
95}
96
97// Number is a number literal.
98type Number struct {
99	tokenBase
100	Value float64
101}
102
103// Boolean is a true or false literal.
104type Boolean struct {
105	tokenBase
106	Value bool
107}
108
109// Null is a null literal.
110type Null struct{ tokenBase }
111
112// Sequence is a list of tokens.
113type Sequence struct {
114	tokenBase
115	Items []Token
116}
117
118// Pair is a key and its value in a [Mapping]. The key is a scalar: a
119// [*String] or an expression.
120type Pair struct {
121	Key, Value Token
122}
123
124// Mapping is an ordered list of key-value pairs. Duplicate keys are rejected
125// when reading, comparing case-insensitively.
126type Mapping struct {
127	tokenBase
128	Pairs []Pair
129}
130
131// BasicExpression is a ${{ }} expression. Expression is the text between the
132// braces, trimmed, or a format() call when a string mixes text and
133// expressions.
134type BasicExpression struct {
135	tokenBase
136	Expression string
137	// Original holds the individual expressions when Expression is the
138	// format() call built from a string with several parts.
139	Original []*BasicExpression
140	// Source is the unprocessed text of the value the token was read from.
141	Source string
142
143	node     expr.Node
144	parseErr error
145}
146
147// InsertExpression is the ${{ insert }} directive.
148type InsertExpression struct{ tokenBase }
149
150func (*String) Kind() Kind           { return KindString }
151func (*Number) Kind() Kind           { return KindNumber }
152func (*Boolean) Kind() Kind          { return KindBoolean }
153func (*Null) Kind() Kind             { return KindNull }
154func (*Sequence) Kind() Kind         { return KindSequence }
155func (*Mapping) Kind() Kind          { return KindMapping }
156func (*BasicExpression) Kind() Kind  { return KindBasicExpression }
157func (*InsertExpression) Kind() Kind { return KindInsertExpression }
158
159func (t *String) String() string  { return t.Value }
160func (t *Number) String() string  { return JSNumberString(t.Value) }
161func (*Null) String() string      { return "" }
162func (*Sequence) String() string  { return "" }
163func (*Mapping) String() string   { return "" }
164func (t *Boolean) String() string { return boolString(t.Value) }
165
166func (t *BasicExpression) String() string { return "${{ " + t.Expression + " }}" }
167func (*InsertExpression) String() string  { return "${{ insert }}" }
168
169func boolString(b bool) string {
170	if b {
171		return "true"
172	}
173	return "false"
174}
175
176// NewString returns a string token.
177func NewString(o Origin, v string) *String { return &String{tokenBase: mk(o), Value: v} }
178
179// NewNumber returns a number token.
180func NewNumber(o Origin, v float64) *Number { return &Number{tokenBase: mk(o), Value: v} }
181
182// NewBoolean returns a boolean token.
183func NewBoolean(o Origin, v bool) *Boolean { return &Boolean{tokenBase: mk(o), Value: v} }
184
185// NewNull returns a null token.
186func NewNull(o Origin) *Null { return &Null{tokenBase: mk(o)} }
187
188// NewSequence returns a sequence token holding items.
189func NewSequence(o Origin, items ...Token) *Sequence {
190	return &Sequence{tokenBase: mk(o), Items: items}
191}
192
193// NewMapping returns an empty mapping token.
194func NewMapping(o Origin) *Mapping { return &Mapping{tokenBase: mk(o)} }
195
196// NewExpression returns an expression token. The text is parsed at once
197// against the contexts and functions allowed by def; with a nil def no
198// context and no function besides the built-in ones is allowed. A parse error
199// is returned by [Evaluate] and [BasicExpression.Err].
200func NewExpression(o Origin, expression string, def *DefinitionInfo) *BasicExpression {
201	t := &BasicExpression{tokenBase: mk(o), Expression: expression}
202	t.def = def
203	t.node, t.parseErr = parseWith(expression, def)
204	return t
205}
206
207// Err returns the error of parsing the expression, or nil.
208func (t *BasicExpression) Err() error { return t.parseErr }
209
210// Add appends a pair.
211func (m *Mapping) Add(key, value Token) { m.Pairs = append(m.Pairs, Pair{key, value}) }
212
213// Find returns the value of the first pair whose key's text is exactly key,
214// or nil.
215func (m *Mapping) Find(key string) Token {
216	for _, p := range m.Pairs {
217		if p.Key.String() == key {
218			return p.Value
219		}
220	}
221	return nil
222}
223
224// IsScalar reports whether t is neither a sequence nor a mapping.
225func IsScalar(t Token) bool {
226	switch t.Kind() {
227	case KindSequence, KindMapping:
228		return false
229	}
230	return true
231}
232
233// IsLiteral reports whether t is a string, number, boolean or null.
234func IsLiteral(t Token) bool {
235	switch t.Kind() {
236	case KindString, KindNumber, KindBoolean, KindNull:
237		return true
238	}
239	return false
240}
241
242// IsExpression reports whether t is a basic or insert expression.
243func IsExpression(t Token) bool {
244	k := t.Kind()
245	return k == KindBasicExpression || k == KindInsertExpression
246}
247
248// Walk calls fn for t and for every token below it, depth first, mapping keys
249// included. It stops descending into a token when fn returns false.
250func Walk(t Token, fn func(Token) bool) {
251	if t == nil || !fn(t) {
252		return
253	}
254	switch t := t.(type) {
255	case *Sequence:
256		for _, it := range t.Items {
257			Walk(it, fn)
258		}
259	case *Mapping:
260		for _, p := range t.Pairs {
261			Walk(p.Key, fn)
262			Walk(p.Value, fn)
263		}
264	}
265}
266
267// HasExpression reports whether t, or anything below it, is an expression.
268func HasExpression(t Token) bool {
269	found := false
270	Walk(t, func(x Token) bool {
271		if IsExpression(x) {
272			found = true
273		}
274		return !found
275	})
276	return found
277}
278
279func trimDisplay(s string) string {
280	s = strings.TrimLeft(s, " \t\r\n\v\f")
281	if i := strings.IndexAny(s, "\r\n"); i >= 0 {
282		s = s[:i]
283	}
284	return s
285}
286
287// Node returns the parsed expression, or nil when the text did not parse or
288// is empty. Callers that only inspect the tree (to look for function calls,
289// for instance) use it with the types of package expr.
290func (t *BasicExpression) Node() expr.Node { return t.node }
291
292// UsesContext reports whether the expression reads the named context
293// ("matrix", "secrets"...), anywhere in its syntax tree and whatever the
294// case. A function call is not a use of a context named like the function.
295func (t *BasicExpression) UsesContext(name string) bool { return nodeUsesContext(t.node, name) }
296
297func nodeUsesContext(n expr.Node, name string) bool {
298	switch n := n.(type) {
299	case *expr.ContextAccess:
300		return strings.EqualFold(n.Name.Lexeme, name)
301	case *expr.Unary:
302		return nodeUsesContext(n.Expr, name)
303	case *expr.Binary:
304		return nodeUsesContext(n.Left, name) || nodeUsesContext(n.Right, name)
305	case *expr.Grouping:
306		return nodeUsesContext(n.Group, name)
307	case *expr.IndexAccess:
308		return nodeUsesContext(n.Expr, name) || nodeUsesContext(n.Index, name)
309	case *expr.Logical:
310		for _, a := range n.Args {
311			if nodeUsesContext(a, name) {
312				return true
313			}
314		}
315	case *expr.FunctionCall:
316		for _, a := range n.Args {
317			if nodeUsesContext(a, name) {
318				return true
319			}
320		}
321	}
322	return false
323}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close