package jslang // JSON: the language every Node project's manifest is written in, and one // turbo-core does not colour. A scanner for it is thirty lines, because the // language is: strings, numbers, three words, six punctuation marks. import "rickub.com/turbo-editors/turbo-core/syntax" // jsonCarry is what a line of JSON leaves open: a block comment, which strict // JSON does not have and tsconfig.json, .jsonc files and every editor's own // settings file do. Tolerating comments costs nothing in a file without them // and colours the ones that have them as what they are. type jsonCarry struct { inComment bool } // HighlightJSON colours a JSON document. // // A key is coloured as an **attribute** and a value as a **string**, so that // the two sides of a colon read apart — which is the whole of what a reader // wants from a coloured package.json. Which side a string is on is decided by // what follows it: a string followed by a colon is a key. func HighlightJSON(src string) [][]syntax.Span { return syntax.ScanLines(src, scanJSONLine) } // scanJSONLine colours one line and returns what it leaves open. func scanJSONLine(line []rune, open jsonCarry) ([]syntax.Span, jsonCarry) { s := syntax.NewLineScanner(line) if open.inComment { if !syntax.FinishBlockComment(s, "*/", syntax.ClassComment) { return s.Spans(), open } open.inComment = false } for !s.AtEnd() { scanJSONToken(s, &open) } return s.Spans(), open } // scanJSONToken colours whatever starts at the scanner's position. func scanJSONToken(s *syntax.LineScanner, open *jsonCarry) { r := s.Peek(0) switch { case r == ' ' || r == '\t': s.SkipSpaces() case s.HasPrefix(0, "//"): s.TakeRest(syntax.ClassComment) case s.HasPrefix(0, "/*"): if !syntax.OpenBlockComment(s, "/*", "*/", syntax.ClassComment) { open.inComment = true } case r == '"': takeJSONString(s) case syntax.IsDigit(r) || (r == '-' && syntax.IsDigit(s.Peek(1))): takeJSONNumber(s) case syntax.IsLetter(r): takeJSONWord(s) case syntax.IsPunctuationRune(r) || r == ':': // The colon is an operator rune to the library and structure here: // it separates a key from its value the way a comma separates two. s.Take(1, syntax.ClassPunctuation) default: s.Advance(1) } } // takeJSONString colours a quoted string as a key when a colon follows it and // as a value otherwise. An unterminated one is coloured to the end of the // line, as a value: a key with no colon is not a key yet. func takeJSONString(s *syntax.LineScanner) { start := s.Pos() s.Advance(1) for !s.AtEnd() { switch s.Peek(0) { case '\\': s.Advance(2) continue case '"': s.Advance(1) s.Emit(start, s.Pos(), jsonStringClass(s)) return } s.Advance(1) } s.Emit(start, s.Pos(), syntax.ClassString) } // jsonStringClass says what a string that has just closed is, by looking past // the spaces after it for a colon. func jsonStringClass(s *syntax.LineScanner) syntax.Class { for at := 0; s.Pos()+at < s.Len(); at++ { switch s.Peek(at) { case ' ', '\t': case ':': return syntax.ClassAttribute default: return syntax.ClassString } } return syntax.ClassString } // takeJSONNumber colours a number, its sign, fraction and exponent included. func takeJSONNumber(s *syntax.LineScanner) { start := s.Pos() s.Advance(1) for !s.AtEnd() { r := s.Peek(0) switch { case syntax.IsDigit(r) || r == '.' || r == 'e' || r == 'E': s.Advance(1) case (r == '+' || r == '-') && isExponent(s.Peek(-1)): s.Advance(1) default: s.Emit(start, s.Pos(), syntax.ClassNumber) return } } s.Emit(start, s.Pos(), syntax.ClassNumber) } // takeJSONWord colours true, false and null as the constants they are, and any // other bare word as an identifier — which JSON has none of, so the colour is // the scanner saying "this is not JSON" without giving up on the line. func takeJSONWord(s *syntax.LineScanner) { start := s.Pos() for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) { s.Advance(1) } class := syntax.ClassIdentifier if jsonConstants[wordAt(s, start)] { class = syntax.ClassConstant } s.Emit(start, s.Pos(), class) } // jsonConstants are the three words JSON has. var jsonConstants = map[string]bool{"true": true, "false": true, "null": true}