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
|
package syntax
import "strings"
// highlightYAML colours a YAML document, one slice of spans per line.
//
// A compose file, a Kubernetes manifest and a CI workflow are all just YAML, so
// there is one scanner rather than one per dialect. Colouring `services:`
// differently from any other key would mean carrying Docker's schema here and
// watching it go stale every time a key is added.
func highlightYAML(src string) [][]Span {
return ScanLines(src, scanYAMLLine)
}
// scanYAMLLine returns the spans of one line, and what it leaves open.
func scanYAMLLine(line []rune, carry yamlCarry) ([]Span, yamlCarry) {
s := &LineScanner{line: line}
if carry.inBlock {
if continues, next := continueBlockScalar(s, carry); continues {
return s.spans, next
}
}
s.SkipSpaces()
if s.AtEnd() {
return s.spans, yamlCarry{}
}
return scanYAMLContent(s)
}
// scanYAMLContent colours one ordinary line of YAML.
func scanYAMLContent(s *LineScanner) ([]Span, yamlCarry) {
switch {
case s.Peek(0) == '#':
s.TakeRest(ClassComment)
return s.spans, yamlCarry{}
case s.HasPrefix(0, "---") || s.HasPrefix(0, "..."):
s.TakeRest(ClassPunctuation)
return s.spans, yamlCarry{}
}
keyIndent := s.Pos()
takeYAMLListMarkers(s)
takeYAMLKey(s)
return scanYAMLValue(s, keyIndent)
}
// takeYAMLListMarkers colours the "- " that opens a sequence entry, and any
// that follow it on the same line: `- - nested` is legal YAML.
func takeYAMLListMarkers(s *LineScanner) {
for s.Peek(0) == '-' && (s.Peek(1) == ' ' || s.Peek(1) == 0) {
s.Take(1, ClassPunctuation)
s.SkipSpaces()
}
}
// scanYAMLValue colours whatever follows a key, and reports a block scalar left
// open at the end of the line.
func scanYAMLValue(s *LineScanner, keyIndent int) ([]Span, yamlCarry) {
if opensBlockScalar(s) {
s.TakeRest(ClassOperator)
return s.spans, yamlCarry{inBlock: true, keyIndent: keyIndent}
}
for !s.AtEnd() {
stepYAMLValue(s)
}
return s.spans, yamlCarry{}
}
// stepYAMLValue colours one thing inside a value.
func stepYAMLValue(s *LineScanner) {
switch r := s.Peek(0); {
case r == ' ' || r == '\t':
s.SkipSpaces()
case r == '#' && startsComment(s):
s.TakeRest(ClassComment)
case r == '"' || r == '\'':
TakeQuoted(s, r, ClassString)
case r == '&' || r == '*':
// An anchor and the alias that refers to it: &defaults, *defaults.
s.Take(1, ClassBuiltin)
s.TakeWhile(ClassBuiltin, isYAMLNameRune)
case r == '!':
s.TakeWhile(ClassType, func(c rune) bool { return c != ' ' })
case isYAMLFlowRune(r):
s.Take(1, ClassPunctuation)
case r == ':' && endsWord(s, 1):
// A colon separates a flow mapping's key from its value. One with no
// space after it is part of the scalar — `nginx:1.27` and
// `http://example.com` are one value each, not a key and a value.
s.Take(1, ClassPunctuation)
case IsDigit(r) || (r == '-' && IsDigit(s.Peek(1))):
s.TakeWhile(ClassNumber, isYAMLNumberRune)
case IsLetter(r) || r == '_':
takeYAMLWord(s)
default:
s.Advance(1)
}
}
// startsComment reports whether a `#` begins a comment rather than sitting
// inside a bare value. YAML needs a space before it, so `a#b` is one scalar.
func startsComment(s *LineScanner) bool {
return s.Pos() == 0 || s.Peek(-1) == ' ' || s.Peek(-1) == '\t'
}
// isYAMLNameRune reports whether a rune can appear in a bare word or an anchor.
func isYAMLNameRune(r rune) bool { return IsWordRune(r) || r == '-' || r == '.' }
// yamlFlowRunes are the characters that structure a flow collection —
// `{a: 1, b: [2]}` — as opposed to appearing inside a scalar.
const yamlFlowRunes = "{}[],"
// isYAMLFlowRune reports whether a rune is one of them.
func isYAMLFlowRune(r rune) bool { return strings.ContainsRune(yamlFlowRunes, r) }
// yamlNumberRunes are the characters that may follow the first digit of an
// unquoted number, date or time. YAML writes all three without quotes, and
// `2026-09-01` and `11:30:00` are numbers to a reader whatever the spec calls
// them.
const yamlNumberRunes = ".-+:eEx"
// isYAMLNumberRune reports whether a rune can appear in one.
func isYAMLNumberRune(r rune) bool {
return IsDigit(r) || strings.ContainsRune(yamlNumberRunes, r)
}
|