turbo-editors/turbo-jspublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

json.go · 143 lines · 4.2 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1package jslang
2
3// JSON: the language every Node project's manifest is written in, and one
4// turbo-core does not colour. A scanner for it is thirty lines, because the
5// language is: strings, numbers, three words, six punctuation marks.
6
7import "rickub.com/turbo-editors/turbo-core/syntax"
8
9// jsonCarry is what a line of JSON leaves open: a block comment, which strict
10// JSON does not have and tsconfig.json, .jsonc files and every editor's own
11// settings file do. Tolerating comments costs nothing in a file without them
12// and colours the ones that have them as what they are.
13type jsonCarry struct {
14 inComment bool
15}
16
17// HighlightJSON colours a JSON document.
18//
19// A key is coloured as an **attribute** and a value as a **string**, so that
20// the two sides of a colon read apart — which is the whole of what a reader
21// wants from a coloured package.json. Which side a string is on is decided by
22// what follows it: a string followed by a colon is a key.
23func HighlightJSON(src string) [][]syntax.Span {
24 return syntax.ScanLines(src, scanJSONLine)
25}
26
27// scanJSONLine colours one line and returns what it leaves open.
28func scanJSONLine(line []rune, open jsonCarry) ([]syntax.Span, jsonCarry) {
29 s := syntax.NewLineScanner(line)
30
31 if open.inComment {
32 if !syntax.FinishBlockComment(s, "*/", syntax.ClassComment) {
33 return s.Spans(), open
34 }
35 open.inComment = false
36 }
37
38 for !s.AtEnd() {
39 scanJSONToken(s, &open)
40 }
41 return s.Spans(), open
42}
43
44// scanJSONToken colours whatever starts at the scanner's position.
45func scanJSONToken(s *syntax.LineScanner, open *jsonCarry) {
46 r := s.Peek(0)
47
48 switch {
49 case r == ' ' || r == '\t':
50 s.SkipSpaces()
51 case s.HasPrefix(0, "//"):
52 s.TakeRest(syntax.ClassComment)
53 case s.HasPrefix(0, "/*"):
54 if !syntax.OpenBlockComment(s, "/*", "*/", syntax.ClassComment) {
55 open.inComment = true
56 }
57 case r == '"':
58 takeJSONString(s)
59 case syntax.IsDigit(r) || (r == '-' && syntax.IsDigit(s.Peek(1))):
60 takeJSONNumber(s)
61 case syntax.IsLetter(r):
62 takeJSONWord(s)
63 case syntax.IsPunctuationRune(r) || r == ':':
64 // The colon is an operator rune to the library and structure here:
65 // it separates a key from its value the way a comma separates two.
66 s.Take(1, syntax.ClassPunctuation)
67 default:
68 s.Advance(1)
69 }
70}
71
72// takeJSONString colours a quoted string as a key when a colon follows it and
73// as a value otherwise. An unterminated one is coloured to the end of the
74// line, as a value: a key with no colon is not a key yet.
75func takeJSONString(s *syntax.LineScanner) {
76 start := s.Pos()
77 s.Advance(1)
78 for !s.AtEnd() {
79 switch s.Peek(0) {
80 case '\\':
81 s.Advance(2)
82 continue
83 case '"':
84 s.Advance(1)
85 s.Emit(start, s.Pos(), jsonStringClass(s))
86 return
87 }
88 s.Advance(1)
89 }
90 s.Emit(start, s.Pos(), syntax.ClassString)
91}
92
93// jsonStringClass says what a string that has just closed is, by looking past
94// the spaces after it for a colon.
95func jsonStringClass(s *syntax.LineScanner) syntax.Class {
96 for at := 0; s.Pos()+at < s.Len(); at++ {
97 switch s.Peek(at) {
98 case ' ', '\t':
99 case ':':
100 return syntax.ClassAttribute
101 default:
102 return syntax.ClassString
103 }
104 }
105 return syntax.ClassString
106}
107
108// takeJSONNumber colours a number, its sign, fraction and exponent included.
109func takeJSONNumber(s *syntax.LineScanner) {
110 start := s.Pos()
111 s.Advance(1)
112 for !s.AtEnd() {
113 r := s.Peek(0)
114 switch {
115 case syntax.IsDigit(r) || r == '.' || r == 'e' || r == 'E':
116 s.Advance(1)
117 case (r == '+' || r == '-') && isExponent(s.Peek(-1)):
118 s.Advance(1)
119 default:
120 s.Emit(start, s.Pos(), syntax.ClassNumber)
121 return
122 }
123 }
124 s.Emit(start, s.Pos(), syntax.ClassNumber)
125}
126
127// takeJSONWord colours true, false and null as the constants they are, and any
128// other bare word as an identifier — which JSON has none of, so the colour is
129// the scanner saying "this is not JSON" without giving up on the line.
130func takeJSONWord(s *syntax.LineScanner) {
131 start := s.Pos()
132 for !s.AtEnd() && syntax.IsWordRune(s.Peek(0)) {
133 s.Advance(1)
134 }
135 class := syntax.ClassIdentifier
136 if jsonConstants[wordAt(s, start)] {
137 class = syntax.ClassConstant
138 }
139 s.Emit(start, s.Pos(), class)
140}
141
142// jsonConstants are the three words JSON has.
143var jsonConstants = map[string]bool{"true": true, "false": true, "null": true}