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, }