turbo-editors/turbo-jspublic Fork 0
v1.0.2
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.

📦 Turbo JS 91999d1 · on v1.0.2 · k33g · 11h ago
json.go · 143 lines · 4.2 KBGo Blame HistoryRaw
  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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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}