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