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
|
package syntax
import "strings"
// Keys and bare words: telling `image: nginx` from a scalar that merely
// contains a colon, which is most of what makes YAML awkward to scan.
// takeYAMLKey colours a mapping key and its colon, when the line has one.
//
// The key is recognised only when a colon follows it before any space, which is
// what tells `image: nginx` from a bare scalar such as `nginx:latest` in a list
// entry — the second has no space after its colon and is a value, not a key.
func takeYAMLKey(s *LineScanner) {
end, ok := yamlKeyEnd(s)
if !ok {
return
}
s.Emit(s.Pos(), end, ClassIdentifier)
s.Advance(end - s.Pos())
s.Take(1, ClassPunctuation) // the colon
s.SkipSpaces()
}
// yamlKeyEnd returns where a key ends, and whether the line starts with one.
func yamlKeyEnd(s *LineScanner) (int, bool) {
if quote := s.Peek(0); quote == '"' || quote == '\'' {
return quotedYAMLKeyEnd(s, quote)
}
for at := s.Pos(); at < s.Len(); at++ {
switch s.line[at] {
case ':':
// A colon ends a key only when a space or the end of the line
// follows it. `http://x` is a value, not a key called `http`.
if at+1 >= s.Len() || s.line[at+1] == ' ' {
return at, at > s.Pos()
}
case '#':
return 0, false
}
}
return 0, false
}
// quotedYAMLKeyEnd returns where a quoted key ends, and whether it is one.
func quotedYAMLKeyEnd(s *LineScanner, quote rune) (int, bool) {
for at := s.Pos() + 1; at < s.Len(); at++ {
if s.line[at] != quote {
continue
}
if at+1 < s.Len() && s.line[at+1] == ':' {
return at + 1, true
}
return 0, false
}
return 0, false
}
// takeYAMLWord colours a bare word, which is a constant when YAML names it and
// plain text otherwise.
//
// A colon inside the word is taken with it unless a space follows, which is
// what keeps `nginx:1.27` and `http://example.com/x` one span each rather than
// three.
func takeYAMLWord(s *LineScanner) {
start := s.Pos()
for !s.AtEnd() {
switch {
case isYAMLNameRune(s.Peek(0)):
s.Advance(1)
case s.Peek(0) == ':' && !endsWord(s, 1):
s.Advance(1)
case s.Peek(0) == '/' && !endsWord(s, 1):
s.Advance(1)
default:
s.Emit(start, s.Pos(), yamlWordClass(s, start))
return
}
}
s.Emit(start, s.Pos(), yamlWordClass(s, start))
}
// yamlWordClass says whether the word running from start to the scanner's
// position is one the language names.
func yamlWordClass(s *LineScanner, start int) Class {
if yamlConstants[strings.ToLower(string(s.line[start:s.Pos()]))] {
return ClassConstant
}
return ClassIdentifier
}
// endsWord reports whether the rune at an offset is a space or the end of the
// line — which is what tells a separator from a character inside a scalar.
func endsWord(s *LineScanner, offset int) bool {
r := s.Peek(offset)
return r == 0 || r == ' ' || r == '\t'
}
// yamlConstants are the words YAML 1.1 reads as booleans and nulls. The
// spelling is generous on purpose: `yes` and `on` are still true to most YAML
// parsers in the wild, and a reader wants to see that they are not plain text.
var yamlConstants = map[string]bool{
"true": true, "false": true,
"yes": true, "no": true,
"on": true, "off": true,
"null": true, "~": true,
}
|