| 📦 Turbo JS 91999d1 k33g yesterday | 1 | package jslang |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "rickub.com/turbo-editors/turbo-core/syntax" |
| 8 | ) |
| 9 | |
| 10 | // TestTheLanguagesReferenceIsTrue holds docs/*/reference/languages.md to the |
| 11 | // scanners. Every row of its JavaScript and JSON tables that no other test |
| 12 | // here covers is checked, so a reference claim and the code cannot drift apart |
| 13 | // quietly. |
| 14 | // |
| 15 | // The MAX_SIZE and 1.toString cases are the ones that document a limitation |
| 16 | // rather than a feature: the leading-capital rule colours a |
| 17 | // SCREAMING_SNAKE_CASE constant as a class, the reference says so, and this is |
| 18 | // what stops somebody "fixing" it without also fixing the sentence. |
| 19 | func TestTheLanguagesReferenceIsTrue(t *testing.T) { |
| 20 | tests := []struct { |
| 21 | highlight func(string) [][]syntax.Span |
| 22 | src string |
| 23 | word string |
| 24 | want syntax.Class |
| 25 | }{ |
| 26 | // JavaScript: the "Recognised" table, row by row. |
| 27 | {Highlight, "let x = 1; enum E {}", "enum", syntax.ClassKeyword}, |
| 28 | {Highlight, "x = Infinity;", "Infinity", syntax.ClassConstant}, |
| 29 | {Highlight, "x = structuredClone(y);", "structuredClone", syntax.ClassBuiltin}, |
| 30 | {Highlight, "x = __dirname;", "__dirname", syntax.ClassBuiltin}, |
| 31 | {Highlight, "x = performance.now();", "performance", syntax.ClassBuiltin}, |
| 32 | {Highlight, "x = window.document;", "window", syntax.ClassBuiltin}, |
| 33 | {Highlight, "async function fetchAll() {}", "fetchAll", syntax.ClassFunction}, |
| 34 | {Highlight, "class widget extends Base {}", "widget", syntax.ClassType}, |
| 35 | {Highlight, "const e = new EventEmitter();", "EventEmitter", syntax.ClassType}, |
| 36 | {Highlight, "obj.method();", "method", syntax.ClassFunction}, |
| 37 | {Highlight, "const c = user?.class;", "class", syntax.ClassIdentifier}, |
| 38 | {Highlight, "const v = set(k, v);", "set", syntax.ClassFunction}, |
| 39 | {Highlight, "this.#reset();", "#reset", syntax.ClassFunction}, |
| 40 | {Highlight, "@observable.ref count = 0;", "@observable.ref", syntax.ClassAttribute}, |
| 41 | {Highlight, "const $el = 1;", "$el", syntax.ClassIdentifier}, |
| 42 | {Highlight, "const 名前 = 1;", "名前", syntax.ClassIdentifier}, |
| 43 | {Highlight, "const n = 0xFFn;", "0xFFn", syntax.ClassNumber}, |
| 44 | {Highlight, "const n = 2E+10;", "2E+10", syntax.ClassNumber}, |
| 45 | {Highlight, "x = y >>>= 2;", ">>>=", syntax.ClassOperator}, |
| 46 | {Highlight, "x = a ** b;", "**", syntax.ClassOperator}, |
| 47 | {Highlight, "x = a === b;", "===", syntax.ClassOperator}, |
| 48 | {Highlight, "f(...args);", "...", syntax.ClassPunctuation}, |
| 49 | {Highlight, "x = a?.b;", "?.", syntax.ClassOperator}, |
| 50 | // JavaScript: the prose rules under the table. |
| 51 | {Highlight, "x = y / z;", "/", syntax.ClassOperator}, |
| 52 | {Highlight, "x = [1] / 2;", "/", syntax.ClassOperator}, |
| 53 | {Highlight, "const n = 0xE+1;", "+", syntax.ClassOperator}, |
| 54 | {Highlight, "const MAX_SIZE = 10;", "MAX_SIZE", syntax.ClassType}, |
| 55 | {Highlight, "const Count = 1;", "Count", syntax.ClassType}, |
| 56 | // JavaScript: the "Not recognised" table, each row as the scanner reads it. |
| 57 | {Highlight, "const t = `a ${b} c`;", "${b}", syntax.ClassString}, |
| 58 | {Highlight, "const el = <div>;", "<", syntax.ClassOperator}, |
| 59 | {Highlight, "const el = <div>;", "div", syntax.ClassIdentifier}, |
| 60 | {Highlight, "/* a /* b */ let x;", "let", syntax.ClassKeyword}, |
| 61 | {Highlight, "x = 1 # not a comment", "#", syntax.ClassIdentifier}, |
| 62 | // JSON: every row of its table. |
| 63 | {HighlightJSON, `{"name": "demo"}`, `"name"`, syntax.ClassAttribute}, |
| 64 | {HighlightJSON, `{"name": "demo"}`, `"demo"`, syntax.ClassString}, |
| 65 | {HighlightJSON, `{"n": -12.5e+3}`, "-12.5e+3", syntax.ClassNumber}, |
| 66 | {HighlightJSON, `{"n": 0.5}`, "0.5", syntax.ClassNumber}, |
| 67 | {HighlightJSON, `{"a": false}`, "false", syntax.ClassConstant}, |
| 68 | {HighlightJSON, `{"a": [1]}`, "[", syntax.ClassPunctuation}, |
| 69 | {HighlightJSON, `{"a": 1} // trailing`, "// trailing", syntax.ClassComment}, |
| 70 | {HighlightJSON, `{"a": NaN}`, "NaN", syntax.ClassIdentifier}, |
| 71 | {HighlightJSON, `{"a" 1}`, `"a"`, syntax.ClassString}, |
| 72 | } |
| 73 | |
| 74 | for _, test := range tests { |
| 75 | t.Run(test.src+"/"+test.word, func(t *testing.T) { |
| 76 | index := strings.Index(test.src, test.word) |
| 77 | if index < 0 { |
| 78 | t.Fatalf("%q not in %q", test.word, test.src) |
| 79 | } |
| 80 | col := len([]rune(test.src[:index])) |
| 81 | got, ok := classAt(test.highlight(test.src), 0, col) |
| 82 | if !ok && test.want == syntax.ClassIdentifier && test.word == "#" { |
| 83 | // A stray # outside a private name is stepped over uncoloured, |
| 84 | // which is what "not a comment" means for the reference's row. |
| 85 | return |
| 86 | } |
| 87 | if !ok || got != test.want { |
| 88 | t.Errorf("%q in %q is %v (covered %v), want %v", test.word, test.src, got, ok, test.want) |
| 89 | } |
| 90 | }) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // TestASingleQuoteInJSONIsSteppedOver pins the JSON5 row of the reference: a |
| 95 | // ' is not a string delimiter in JSON, and the scanner neither colours it nor |
| 96 | // stops at it. |
| 97 | func TestASingleQuoteInJSONIsSteppedOver(t *testing.T) { |
| 98 | const src = `{'a': 1}` |
| 99 | spans := HighlightJSON(src) |
| 100 | |
| 101 | if _, ok := classAt(spans, 0, 1); ok { |
| 102 | t.Errorf("the single quote is coloured; the reference says JSON5 is not recognised") |
| 103 | } |
| 104 | if got, ok := classAt(spans, 0, 6); !ok || got != syntax.ClassNumber { |
| 105 | t.Errorf("the number after the single-quoted word is %v (covered %v), want number", got, ok) |
| 106 | } |
| 107 | } |