| 📦 Turbo JS 91999d1 k33g 11h ago | 1 | package jslang |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "rickub.com/turbo-editors/turbo-core/syntax" |
| 8 | ) |
| 9 | |
| 10 | // jsonClassOf is classOfFirst for JSON. |
| 11 | func jsonClassOf(t *testing.T, src, word string) syntax.Class { |
| 12 | t.Helper() |
| 13 | return classOfFirst(t, HighlightJSON, src, word) |
| 14 | } |
| 15 | |
| 16 | const packageJSON = `{ |
| 17 | "name": "demo", |
| 18 | "version": "1.0.0", |
| 19 | "private": true, |
| 20 | "engines": { "node": ">=24" }, |
| 21 | "scripts": { |
| 22 | "start": "node main.js", |
| 23 | "test": "node --test" |
| 24 | }, |
| 25 | "dependencies": {}, |
| 26 | "count": -12.5e+3, |
| 27 | "nothing": null |
| 28 | } |
| 29 | ` |
| 30 | |
| 31 | func TestJSONReturnsOneEntryPerLine(t *testing.T) { |
| 32 | if got, want := len(HighlightJSON(packageJSON)), strings.Count(packageJSON, "\n")+1; got != want { |
| 33 | t.Errorf("HighlightJSON() returned %d lines, want %d", got, want) |
| 34 | } |
| 35 | if got := len(HighlightJSON("")); got != 1 { |
| 36 | t.Errorf("HighlightJSON(\"\") returned %d lines, want 1", got) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestAJSONKeyIsAnAttributeAndAValueIsAString(t *testing.T) { |
| 41 | // The two sides of a colon read apart, which is the whole of what a |
| 42 | // coloured package.json is for. |
| 43 | tests := []struct { |
| 44 | word string |
| 45 | want syntax.Class |
| 46 | }{ |
| 47 | {`"name"`, syntax.ClassAttribute}, |
| 48 | {`"demo"`, syntax.ClassString}, |
| 49 | {`"scripts"`, syntax.ClassAttribute}, |
| 50 | {`"node main.js"`, syntax.ClassString}, |
| 51 | {`"node"`, syntax.ClassAttribute}, // a key inside an inline object |
| 52 | {`">=24"`, syntax.ClassString}, |
| 53 | } |
| 54 | |
| 55 | for _, test := range tests { |
| 56 | t.Run(test.word, func(t *testing.T) { |
| 57 | if got := jsonClassOf(t, packageJSON, test.word); got != test.want { |
| 58 | t.Errorf("%s is %v, want %v", test.word, got, test.want) |
| 59 | } |
| 60 | }) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestJSONConstantsNumbersAndPunctuation(t *testing.T) { |
| 65 | tests := []struct { |
| 66 | word string |
| 67 | want syntax.Class |
| 68 | }{ |
| 69 | {"true", syntax.ClassConstant}, |
| 70 | {"null", syntax.ClassConstant}, |
| 71 | {"-12.5e+3", syntax.ClassNumber}, |
| 72 | {"{", syntax.ClassPunctuation}, |
| 73 | {":", syntax.ClassPunctuation}, |
| 74 | {",", syntax.ClassPunctuation}, |
| 75 | {"[", syntax.ClassPunctuation}, |
| 76 | } |
| 77 | |
| 78 | src := packageJSON + `[1, 2]` |
| 79 | for _, test := range tests { |
| 80 | t.Run(test.word, func(t *testing.T) { |
| 81 | if got := jsonClassOf(t, src, test.word); got != test.want { |
| 82 | t.Errorf("%s is %v, want %v", test.word, got, test.want) |
| 83 | } |
| 84 | }) |
| 85 | } |
| 86 | wholeWordIs(t, HighlightJSON, `{"count": -12.5e+3}`, "-12.5e+3", syntax.ClassNumber) |
| 87 | } |
| 88 | |
| 89 | func TestJSONTakesCommentsInItsStride(t *testing.T) { |
| 90 | // Strict JSON has none; tsconfig.json, .jsonc and every editor's own |
| 91 | // settings file do, and a scanner that painted them as broken would be |
| 92 | // wrong in exactly the files most likely to hold one. |
| 93 | const src = "{\n // the entry point\n \"main\": \"index.js\", /* one\n two */ \"x\": 1\n}" |
| 94 | spans := HighlightJSON(src) |
| 95 | |
| 96 | if got := jsonClassOf(t, src, "// the entry point"); got != syntax.ClassComment { |
| 97 | t.Errorf("a line comment is %v, want comment", got) |
| 98 | } |
| 99 | if got := jsonClassOf(t, src, "/* one"); got != syntax.ClassComment { |
| 100 | t.Errorf("a block comment is %v, want comment", got) |
| 101 | } |
| 102 | class, ok := classAt(spans, 3, 0) // the " two */" line |
| 103 | if !ok || class != syntax.ClassComment { |
| 104 | t.Errorf("the second line of the block comment is %v (covered: %v), want comment", class, ok) |
| 105 | } |
| 106 | if got := jsonClassOf(t, src, `"x"`); got != syntax.ClassAttribute { |
| 107 | t.Errorf("the key after the block comment is %v, want attribute", got) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestAnEscapedQuoteDoesNotEndAJSONString(t *testing.T) { |
| 112 | const src = `{"say": "a \" b", "n": 1}` |
| 113 | |
| 114 | if got := jsonClassOf(t, src, `"n"`); got != syntax.ClassAttribute { |
| 115 | t.Errorf("the key after an escaped quote is %v, want attribute", got) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | func TestBrokenJSONIsStillColoured(t *testing.T) { |
| 120 | // A bare word is not JSON, and it is coloured as an identifier rather |
| 121 | // than stopping the line; an unterminated string runs to the end of its |
| 122 | // line, as a value, and the next line is JSON again. |
| 123 | tests := []struct{ src, word string }{ |
| 124 | {`{"a": undefined}`, "undefined"}, |
| 125 | {`{"a": "unterminated`, `"unterminated`}, |
| 126 | {`{"a" 1}`, `"a"`}, |
| 127 | } |
| 128 | |
| 129 | for _, test := range tests { |
| 130 | t.Run(test.src, func(t *testing.T) { |
| 131 | spans := HighlightJSON(test.src) |
| 132 | if len(spans) != 1 { |
| 133 | t.Fatalf("HighlightJSON(%q) returned %d lines, want 1", test.src, len(spans)) |
| 134 | } |
| 135 | if _, ok := classAt(spans, 0, strings.Index(test.src, test.word)); !ok { |
| 136 | t.Errorf("nothing covers %q in %q", test.word, test.src) |
| 137 | } |
| 138 | }) |
| 139 | } |
| 140 | if got := jsonClassOf(t, `{"a": undefined}`, "undefined"); got != syntax.ClassIdentifier { |
| 141 | t.Errorf("a bare word is %v, want identifier", got) |
| 142 | } |
| 143 | if got := jsonClassOf(t, `{"a" 1}`, `"a"`); got != syntax.ClassString { |
| 144 | t.Errorf("a string with no colon after it is %v, want string — it is not a key yet", got) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | func TestJSONSpansAreOrderedAndDoNotOverlap(t *testing.T) { |
| 149 | for line, onLine := range HighlightJSON(packageJSON) { |
| 150 | previousEnd := 0 |
| 151 | for _, span := range onLine { |
| 152 | if span.Start < previousEnd { |
| 153 | t.Errorf("line %d: span %+v starts before the previous one ended at %d", line, span, previousEnd) |
| 154 | } |
| 155 | previousEnd = span.End |
| 156 | } |
| 157 | } |
| 158 | } |