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