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_test.go · 158 lines · 4.8 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package jslang

import (
	"strings"
	"testing"

	"rickub.com/turbo-editors/turbo-core/syntax"
)

// jsonClassOf is classOfFirst for JSON.
func jsonClassOf(t *testing.T, src, word string) syntax.Class {
	t.Helper()
	return classOfFirst(t, HighlightJSON, src, word)
}

const packageJSON = `{
  "name": "demo",
  "version": "1.0.0",
  "private": true,
  "engines": { "node": ">=24" },
  "scripts": {
    "start": "node main.js",
    "test": "node --test"
  },
  "dependencies": {},
  "count": -12.5e+3,
  "nothing": null
}
`

func TestJSONReturnsOneEntryPerLine(t *testing.T) {
	if got, want := len(HighlightJSON(packageJSON)), strings.Count(packageJSON, "\n")+1; got != want {
		t.Errorf("HighlightJSON() returned %d lines, want %d", got, want)
	}
	if got := len(HighlightJSON("")); got != 1 {
		t.Errorf("HighlightJSON(\"\") returned %d lines, want 1", got)
	}
}

func TestAJSONKeyIsAnAttributeAndAValueIsAString(t *testing.T) {
	// The two sides of a colon read apart, which is the whole of what a
	// coloured package.json is for.
	tests := []struct {
		word string
		want syntax.Class
	}{
		{`"name"`, syntax.ClassAttribute},
		{`"demo"`, syntax.ClassString},
		{`"scripts"`, syntax.ClassAttribute},
		{`"node main.js"`, syntax.ClassString},
		{`"node"`, syntax.ClassAttribute}, // a key inside an inline object
		{`">=24"`, syntax.ClassString},
	}

	for _, test := range tests {
		t.Run(test.word, func(t *testing.T) {
			if got := jsonClassOf(t, packageJSON, test.word); got != test.want {
				t.Errorf("%s is %v, want %v", test.word, got, test.want)
			}
		})
	}
}

func TestJSONConstantsNumbersAndPunctuation(t *testing.T) {
	tests := []struct {
		word string
		want syntax.Class
	}{
		{"true", syntax.ClassConstant},
		{"null", syntax.ClassConstant},
		{"-12.5e+3", syntax.ClassNumber},
		{"{", syntax.ClassPunctuation},
		{":", syntax.ClassPunctuation},
		{",", syntax.ClassPunctuation},
		{"[", syntax.ClassPunctuation},
	}

	src := packageJSON + `[1, 2]`
	for _, test := range tests {
		t.Run(test.word, func(t *testing.T) {
			if got := jsonClassOf(t, src, test.word); got != test.want {
				t.Errorf("%s is %v, want %v", test.word, got, test.want)
			}
		})
	}
	wholeWordIs(t, HighlightJSON, `{"count": -12.5e+3}`, "-12.5e+3", syntax.ClassNumber)
}

func TestJSONTakesCommentsInItsStride(t *testing.T) {
	// Strict JSON has none; tsconfig.json, .jsonc and every editor's own
	// settings file do, and a scanner that painted them as broken would be
	// wrong in exactly the files most likely to hold one.
	const src = "{\n  // the entry point\n  \"main\": \"index.js\", /* one\n  two */ \"x\": 1\n}"
	spans := HighlightJSON(src)

	if got := jsonClassOf(t, src, "// the entry point"); got != syntax.ClassComment {
		t.Errorf("a line comment is %v, want comment", got)
	}
	if got := jsonClassOf(t, src, "/* one"); got != syntax.ClassComment {
		t.Errorf("a block comment is %v, want comment", got)
	}
	class, ok := classAt(spans, 3, 0) // the "  two */" line
	if !ok || class != syntax.ClassComment {
		t.Errorf("the second line of the block comment is %v (covered: %v), want comment", class, ok)
	}
	if got := jsonClassOf(t, src, `"x"`); got != syntax.ClassAttribute {
		t.Errorf("the key after the block comment is %v, want attribute", got)
	}
}

func TestAnEscapedQuoteDoesNotEndAJSONString(t *testing.T) {
	const src = `{"say": "a \" b", "n": 1}`

	if got := jsonClassOf(t, src, `"n"`); got != syntax.ClassAttribute {
		t.Errorf("the key after an escaped quote is %v, want attribute", got)
	}
}

func TestBrokenJSONIsStillColoured(t *testing.T) {
	// A bare word is not JSON, and it is coloured as an identifier rather
	// than stopping the line; an unterminated string runs to the end of its
	// line, as a value, and the next line is JSON again.
	tests := []struct{ src, word string }{
		{`{"a": undefined}`, "undefined"},
		{`{"a": "unterminated`, `"unterminated`},
		{`{"a" 1}`, `"a"`},
	}

	for _, test := range tests {
		t.Run(test.src, func(t *testing.T) {
			spans := HighlightJSON(test.src)
			if len(spans) != 1 {
				t.Fatalf("HighlightJSON(%q) returned %d lines, want 1", test.src, len(spans))
			}
			if _, ok := classAt(spans, 0, strings.Index(test.src, test.word)); !ok {
				t.Errorf("nothing covers %q in %q", test.word, test.src)
			}
		})
	}
	if got := jsonClassOf(t, `{"a": undefined}`, "undefined"); got != syntax.ClassIdentifier {
		t.Errorf("a bare word is %v, want identifier", got)
	}
	if got := jsonClassOf(t, `{"a" 1}`, `"a"`); got != syntax.ClassString {
		t.Errorf("a string with no colon after it is %v, want string — it is not a key yet", got)
	}
}

func TestJSONSpansAreOrderedAndDoNotOverlap(t *testing.T) {
	for line, onLine := range HighlightJSON(packageJSON) {
		previousEnd := 0
		for _, span := range onLine {
			if span.Start < previousEnd {
				t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd)
			}
			previousEnd = span.End
		}
	}
}