@px-lsp/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. package/src/wordAt.ts +49 -0
@@ -0,0 +1,257 @@
1
+ // Concrete Syntax Tree node types and utilities for Paradox script.
2
+ //
3
+ // All offsets are UTF-16 code-unit offsets into the source string (i.e. the
4
+ // same units JavaScript string indexing / `String.prototype.slice` use, and
5
+ // the same units the VS Code LSP uses for `character` positions on a line).
6
+
7
+ export interface Range {
8
+ start: number;
9
+ end: number;
10
+ }
11
+
12
+ export type Operator = "=" | "?=" | "==" | "!=" | "<" | "<=" | ">" | ">=";
13
+
14
+ export interface RootNode {
15
+ kind: "root";
16
+ statements: Statement[];
17
+ range: Range;
18
+ }
19
+
20
+ export interface AssignmentNode {
21
+ kind: "assignment";
22
+ key: ScalarNode;
23
+ op: Operator | null; // null for GUI-style `key { ... }` with no operator
24
+ value: ValueNode | null; // null when the value is missing (error recorded)
25
+ range: Range;
26
+ }
27
+
28
+ // A bare list element, e.g. the `brave` / `ambitious` in `traits = { brave ambitious }`
29
+ // or an anonymous block in `{ 1 2 } { 3 4 }`.
30
+ export interface ValueStatementNode {
31
+ kind: "value";
32
+ value: ValueNode;
33
+ range: Range;
34
+ }
35
+
36
+ export type Statement = AssignmentNode | ValueStatementNode;
37
+
38
+ export interface ScalarNode {
39
+ kind: "scalar";
40
+ text: string; // the raw text of the scalar; for quoted scalars, WITHOUT surrounding quotes
41
+ quoted: boolean;
42
+ range: Range; // for quoted scalars, INCLUDES the surrounding quotes
43
+ }
44
+
45
+ export interface BlockNode {
46
+ kind: "block";
47
+ statements: Statement[];
48
+ range: Range;
49
+ openBrace: number; // offset of `{`
50
+ closeBrace: number | null; // offset of `}`, or null if missing (error recorded)
51
+ }
52
+
53
+ // e.g. `color = rgb { 255 0 0 }` — a scalar tag immediately followed by a block.
54
+ export interface TaggedBlockNode {
55
+ kind: "tagged-block";
56
+ tag: ScalarNode;
57
+ block: BlockNode;
58
+ range: Range;
59
+ }
60
+
61
+ export type ValueNode = ScalarNode | BlockNode | TaggedBlockNode;
62
+
63
+ export interface CommentNode {
64
+ text: string; // includes the leading `#`
65
+ range: Range;
66
+ line: number; // 0-based line number
67
+ }
68
+
69
+ export type ParseErrorCode =
70
+ | "unclosed-brace" // report at the OPENING brace of the unclosed block
71
+ | "stray-close" // `}` with no open block
72
+ | "unterminated-string" // recover at end of line
73
+ | "missing-value"; // `key =` with nothing parseable after
74
+
75
+ export interface ParseError {
76
+ code: ParseErrorCode;
77
+ message: string;
78
+ range: Range;
79
+ }
80
+
81
+ export interface ParseResult {
82
+ root: RootNode;
83
+ errors: ParseError[];
84
+ comments: CommentNode[];
85
+ }
86
+
87
+ // -------------------------------------------------------------------------
88
+ // LineIndex — maps between offsets and (line, character) positions.
89
+ // -------------------------------------------------------------------------
90
+
91
+ export class LineIndex {
92
+ // lineStarts[i] is the offset of the first character of line i (0-based).
93
+ private readonly lineStarts: number[];
94
+ private readonly length: number;
95
+
96
+ constructor(text: string) {
97
+ this.length = text.length;
98
+ const starts: number[] = [0];
99
+ for (let i = 0; i < text.length; i++) {
100
+ const c = text.charCodeAt(i);
101
+ if (c === 10 /* \n */) {
102
+ starts.push(i + 1);
103
+ } else if (c === 13 /* \r */) {
104
+ // Treat \r\n as a single break; a lone \r also breaks a line.
105
+ if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) {
106
+ starts.push(i + 2);
107
+ i++;
108
+ } else {
109
+ starts.push(i + 1);
110
+ }
111
+ }
112
+ }
113
+ this.lineStarts = starts;
114
+ }
115
+
116
+ get lineCount(): number {
117
+ return this.lineStarts.length;
118
+ }
119
+
120
+ lineStart(line: number): number {
121
+ if (line < 0) return 0;
122
+ if (line >= this.lineStarts.length) return this.length;
123
+ return this.lineStarts[line];
124
+ }
125
+
126
+ positionAt(offset: number): { line: number; character: number } {
127
+ let o = offset;
128
+ if (o < 0) o = 0;
129
+ if (o > this.length) o = this.length;
130
+ // Binary search for the greatest lineStart <= o.
131
+ let lo = 0;
132
+ let hi = this.lineStarts.length - 1;
133
+ while (lo < hi) {
134
+ const mid = (lo + hi + 1) >> 1;
135
+ if (this.lineStarts[mid] <= o) {
136
+ lo = mid;
137
+ } else {
138
+ hi = mid - 1;
139
+ }
140
+ }
141
+ return { line: lo, character: o - this.lineStarts[lo] };
142
+ }
143
+
144
+ offsetAt(pos: { line: number; character: number }): number {
145
+ let line = pos.line;
146
+ if (line < 0) line = 0;
147
+ if (line >= this.lineStarts.length) {
148
+ return this.length;
149
+ }
150
+ const start = this.lineStarts[line];
151
+ // Clamp character to the end of this line (start of next line, or EOF).
152
+ const nextStart = line + 1 < this.lineStarts.length ? this.lineStarts[line + 1] : this.length;
153
+ let ch = pos.character;
154
+ if (ch < 0) ch = 0;
155
+ let offset = start + ch;
156
+ if (offset > nextStart) offset = nextStart;
157
+ if (offset > this.length) offset = this.length;
158
+ return offset;
159
+ }
160
+ }
161
+
162
+ // -------------------------------------------------------------------------
163
+ // Walk helpers
164
+ // -------------------------------------------------------------------------
165
+
166
+ function statementChildBlock(stmt: Statement): BlockNode | null {
167
+ if (stmt.kind === "assignment") {
168
+ const v = stmt.value;
169
+ if (v && v.kind === "block") return v;
170
+ if (v && v.kind === "tagged-block") return v.block;
171
+ return null;
172
+ }
173
+ // value statement
174
+ const v = stmt.value;
175
+ if (v.kind === "block") return v;
176
+ if (v.kind === "tagged-block") return v.block;
177
+ return null;
178
+ }
179
+
180
+ /**
181
+ * Depth-first walk over every statement in the tree. The callback receives the
182
+ * statement plus the chain of ancestor assignment/block nodes (outermost first).
183
+ */
184
+ export function walkStatements(
185
+ root: RootNode | BlockNode,
186
+ cb: (stmt: Statement, ancestors: readonly (AssignmentNode | BlockNode)[]) => void
187
+ ): void {
188
+ const ancestors: (AssignmentNode | BlockNode)[] = [];
189
+
190
+ const visitBlock = (block: BlockNode): void => {
191
+ for (const stmt of block.statements) {
192
+ cb(stmt, ancestors);
193
+ const child = statementChildBlock(stmt);
194
+ if (child) {
195
+ if (stmt.kind === "assignment") {
196
+ ancestors.push(stmt);
197
+ }
198
+ ancestors.push(child);
199
+ visitBlock(child);
200
+ ancestors.pop();
201
+ if (stmt.kind === "assignment") {
202
+ ancestors.pop();
203
+ }
204
+ }
205
+ }
206
+ };
207
+
208
+ if (root.kind === "root") {
209
+ for (const stmt of root.statements) {
210
+ cb(stmt, ancestors);
211
+ const child = statementChildBlock(stmt);
212
+ if (child) {
213
+ if (stmt.kind === "assignment") {
214
+ ancestors.push(stmt);
215
+ }
216
+ ancestors.push(child);
217
+ visitBlock(child);
218
+ ancestors.pop();
219
+ if (stmt.kind === "assignment") {
220
+ ancestors.pop();
221
+ }
222
+ }
223
+ }
224
+ } else {
225
+ visitBlock(root);
226
+ }
227
+ }
228
+
229
+ function offsetInRange(offset: number, range: Range): boolean {
230
+ return offset >= range.start && offset <= range.end;
231
+ }
232
+
233
+ /**
234
+ * Returns the innermost-last chain of statements whose ranges contain `offset`.
235
+ * For a cursor sitting between statements inside a block, the path ends at the
236
+ * enclosing statement chain (i.e. the assignment/value-statement that owns the
237
+ * block the cursor is inside). Returns null if offset is outside all statements.
238
+ */
239
+ export function nodeAtOffset(root: RootNode, offset: number): { path: Statement[] } | null {
240
+ const path: Statement[] = [];
241
+
242
+ const searchStatements = (statements: Statement[]): boolean => {
243
+ for (const stmt of statements) {
244
+ if (!offsetInRange(offset, stmt.range)) continue;
245
+ path.push(stmt);
246
+ const child = statementChildBlock(stmt);
247
+ if (child && offsetInRange(offset, child.range)) {
248
+ searchStatements(child.statements);
249
+ }
250
+ return true;
251
+ }
252
+ return false;
253
+ };
254
+
255
+ searchStatements(root.statements);
256
+ return path.length > 0 ? { path } : null;
257
+ }
@@ -0,0 +1,106 @@
1
+ // Encoding helpers for reading Paradox script / localization files.
2
+ //
3
+ // Paradox files are UTF-8 (often with a BOM). Some older or hand-edited files
4
+ // contain invalid UTF-8 byte sequences (typically Latin-1 / Windows-1252 text);
5
+ // for those we fall back to a latin1 decode so we still get usable text rather
6
+ // than U+FFFD replacement soup.
7
+
8
+ const UTF8_BOM_0 = 0xef;
9
+ const UTF8_BOM_1 = 0xbb;
10
+ const UTF8_BOM_2 = 0xbf;
11
+
12
+ export function hasUtf8Bom(buf: Uint8Array): boolean {
13
+ return buf.length >= 3 && buf[0] === UTF8_BOM_0 && buf[1] === UTF8_BOM_1 && buf[2] === UTF8_BOM_2;
14
+ }
15
+
16
+ /**
17
+ * Returns true if `buf` is entirely valid UTF-8 (ignoring a leading BOM).
18
+ * Single pass, no allocations.
19
+ */
20
+ export function isValidUtf8(buf: Uint8Array): boolean {
21
+ let i = hasUtf8Bom(buf) ? 3 : 0;
22
+ const len = buf.length;
23
+ while (i < len) {
24
+ const b0 = buf[i];
25
+ if (b0 < 0x80) {
26
+ i++;
27
+ continue;
28
+ }
29
+ let extra: number;
30
+ let min: number;
31
+ let codepointHigh: number;
32
+ if (b0 >= 0xc2 && b0 <= 0xdf) {
33
+ extra = 1;
34
+ min = 0x80;
35
+ codepointHigh = b0 & 0x1f;
36
+ } else if (b0 >= 0xe0 && b0 <= 0xef) {
37
+ extra = 2;
38
+ min = 0x800;
39
+ codepointHigh = b0 & 0x0f;
40
+ } else if (b0 >= 0xf0 && b0 <= 0xf4) {
41
+ extra = 3;
42
+ min = 0x10000;
43
+ codepointHigh = b0 & 0x07;
44
+ } else {
45
+ // 0x80-0xC1 or 0xF5-0xFF: invalid lead byte.
46
+ return false;
47
+ }
48
+ if (i + extra >= len) return false;
49
+ let cp = codepointHigh;
50
+ for (let k = 1; k <= extra; k++) {
51
+ const b = buf[i + k];
52
+ if ((b & 0xc0) !== 0x80) return false; // not a continuation byte
53
+ cp = (cp << 6) | (b & 0x3f);
54
+ }
55
+ // Reject overlong encodings, surrogates, and out-of-range code points.
56
+ if (cp < min) return false;
57
+ if (cp >= 0xd800 && cp <= 0xdfff) return false;
58
+ if (cp > 0x10ffff) return false;
59
+ i += extra + 1;
60
+ }
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Decode a buffer to a string. Prefers UTF-8 (stripping a BOM). If the buffer
66
+ * is not valid UTF-8, falls back to a latin1 decode and reports that.
67
+ */
68
+ export function decode(buf: Uint8Array): {
69
+ text: string;
70
+ hadBom: boolean;
71
+ encoding: "utf8" | "utf8-bom" | "latin1-fallback";
72
+ } {
73
+ const bom = hasUtf8Bom(buf);
74
+ if (isValidUtf8(buf)) {
75
+ const body = bom ? buf.subarray(3) : buf;
76
+ const text = utf8Decode(body);
77
+ return { text, hadBom: bom, encoding: bom ? "utf8-bom" : "utf8" };
78
+ }
79
+ // latin1 fallback: each byte maps 1:1 to U+00xx.
80
+ const text = latin1Decode(buf);
81
+ return { text, hadBom: false, encoding: "latin1-fallback" };
82
+ }
83
+
84
+ function utf8Decode(body: Uint8Array): string {
85
+ // Prefer Node's TextDecoder when available (fast, correct).
86
+ if (typeof TextDecoder !== "undefined") {
87
+ return new TextDecoder("utf-8", { fatal: false }).decode(body);
88
+ }
89
+ // Fallback (should not be hit in Node/vitest): naive latin1.
90
+ return latin1Decode(body);
91
+ }
92
+
93
+ function latin1Decode(buf: Uint8Array): string {
94
+ // Build in chunks to avoid apply() stack limits on large buffers.
95
+ let out = "";
96
+ const CHUNK = 0x8000;
97
+ for (let i = 0; i < buf.length; i += CHUNK) {
98
+ const end = Math.min(i + CHUNK, buf.length);
99
+ let piece = "";
100
+ for (let j = i; j < end; j++) {
101
+ piece += String.fromCharCode(buf[j]);
102
+ }
103
+ out += piece;
104
+ }
105
+ return out;
106
+ }
@@ -0,0 +1,7 @@
1
+ // Public API of the parser layer. Import from here.
2
+
3
+ export * from "./cst.js";
4
+ export * from "./lexer.js";
5
+ export * from "./parser.js";
6
+ export * from "./locParser.js";
7
+ export * from "./encoding.js";
@@ -0,0 +1,245 @@
1
+ // Hand-written, allocation-light, error-tolerant lexer for Paradox script.
2
+ //
3
+ // Single pass over the source; no per-character regex. Offsets are UTF-16
4
+ // code-unit offsets into the source string.
5
+
6
+ export type TokenKind =
7
+ | "lbrace" // {
8
+ | "rbrace" // }
9
+ | "op" // = ?= == != < <= > >=
10
+ | "string" // quoted "..." (text WITHOUT quotes stored separately via range)
11
+ | "comment" // # to end of line (text includes leading #)
12
+ | "word" // scalar run
13
+ | "eof";
14
+
15
+ export interface Token {
16
+ kind: TokenKind;
17
+ start: number;
18
+ end: number;
19
+ // For "op": the operator text ("=", "?=", ...).
20
+ // For "word"/"string"/"comment": not populated (slice the source instead),
21
+ // but we DO record `unterminated` on unterminated strings.
22
+ value?: string;
23
+ // Only meaningful for "string" tokens: true if the closing quote was missing
24
+ // and the token was terminated at end-of-line / EOF.
25
+ unterminated?: boolean;
26
+ }
27
+
28
+ /**
29
+ * How many newlines one quoted string may cross while a `[` inside it is still
30
+ * open. The longest multi-line data function measured across the shipped gui
31
+ * trees is four lines; this is the backstop that keeps a genuinely broken `"[`
32
+ * from turning the remainder of a file into one token.
33
+ */
34
+ const MAX_STRING_SPAN = 32;
35
+
36
+ // Character classification helpers ----------------------------------------
37
+
38
+ function isWhitespace(c: number): boolean {
39
+ // space, tab, newline, carriage return, form feed, vertical tab, and the BOM.
40
+ // Callers strip a leading BOM where they can, but not every one does, and a
41
+ // BOM glued to the first top-level key would swallow that whole block.
42
+ return c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11 || c === 0xfeff;
43
+ }
44
+
45
+ // Characters that terminate a bare word.
46
+ function isWordTerminator(c: number): boolean {
47
+ return (
48
+ isWhitespace(c) ||
49
+ c === 123 /* { */ ||
50
+ c === 125 /* } */ ||
51
+ c === 35 /* # */ ||
52
+ c === 34 /* " */ ||
53
+ c === 61 /* = */ ||
54
+ c === 60 /* < */ ||
55
+ c === 62 /* > */ ||
56
+ c === 33 /* ! */
57
+ // NOTE: `?` is intentionally NOT a terminator on its own. `?=` is handled
58
+ // specially: a `?` only starts an operator when immediately followed by `=`.
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Lex the entire source into a token array (including a trailing eof token).
64
+ * Never throws. Comments and strings are emitted as tokens; the parser decides
65
+ * what to do with them.
66
+ */
67
+ export function tokenize(text: string): Token[] {
68
+ const tokens: Token[] = [];
69
+ const len = text.length;
70
+ let i = 0;
71
+
72
+ while (i < len) {
73
+ const c = text.charCodeAt(i);
74
+
75
+ // Whitespace (newlines are plain whitespace).
76
+ if (isWhitespace(c)) {
77
+ i++;
78
+ continue;
79
+ }
80
+
81
+ // Comment: # to end of line.
82
+ if (c === 35 /* # */) {
83
+ const start = i;
84
+ i++;
85
+ while (i < len) {
86
+ const cc = text.charCodeAt(i);
87
+ if (cc === 10 || cc === 13) break;
88
+ i++;
89
+ }
90
+ tokens.push({ kind: "comment", start, end: i });
91
+ continue;
92
+ }
93
+
94
+ // Braces.
95
+ if (c === 123 /* { */) {
96
+ tokens.push({ kind: "lbrace", start: i, end: i + 1 });
97
+ i++;
98
+ continue;
99
+ }
100
+ if (c === 125 /* } */) {
101
+ tokens.push({ kind: "rbrace", start: i, end: i + 1 });
102
+ i++;
103
+ continue;
104
+ }
105
+
106
+ // Quoted string.
107
+ if (c === 34 /* " */) {
108
+ const start = i;
109
+ i++;
110
+ let unterminated = false;
111
+ // A `[ … ]` data-function expression may be written across several lines
112
+ //, `visible = "[And(\n\tA,\n\tB\n)]"`, and the games accept it: one
113
+ // shipped gui tree writes it, and the largest community framework on it
114
+ // does so in 23 of its 52 .gui files. A newline therefore continues the
115
+ // string ONLY while a `[` is still open: an ordinary unterminated string
116
+ // recovers at end of line exactly as before, and a stray `"[` gives up
117
+ // after MAX_STRING_SPAN lines instead of swallowing the rest of the file.
118
+ let brackets = 0;
119
+ let spanned = 0;
120
+ while (true) {
121
+ if (i >= len) {
122
+ unterminated = true;
123
+ break;
124
+ }
125
+ const cc = text.charCodeAt(i);
126
+ if (cc === 92 /* backslash */) {
127
+ // Escape: skip the next char (handles \" and \\ etc.).
128
+ i += 2;
129
+ continue;
130
+ }
131
+ if (cc === 34 /* " */) {
132
+ i++; // consume closing quote
133
+ break;
134
+ }
135
+ if (cc === 10 || cc === 13) {
136
+ if (brackets === 0 || spanned >= MAX_STRING_SPAN) {
137
+ // Unterminated: recover at end of line.
138
+ unterminated = true;
139
+ break;
140
+ }
141
+ if (cc === 10) spanned++;
142
+ i++;
143
+ continue;
144
+ }
145
+ if (cc === 91 /* [ */) brackets++;
146
+ else if (cc === 93 /* ] */ && brackets > 0) brackets--;
147
+ i++;
148
+ }
149
+ const tok: Token = { kind: "string", start, end: i };
150
+ if (unterminated) tok.unterminated = true;
151
+ tokens.push(tok);
152
+ continue;
153
+ }
154
+
155
+ // Operators.
156
+ if (c === 61 /* = */) {
157
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) {
158
+ tokens.push({ kind: "op", start: i, end: i + 2, value: "==" });
159
+ i += 2;
160
+ } else {
161
+ tokens.push({ kind: "op", start: i, end: i + 1, value: "=" });
162
+ i += 1;
163
+ }
164
+ continue;
165
+ }
166
+ if (c === 33 /* ! */) {
167
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) {
168
+ tokens.push({ kind: "op", start: i, end: i + 2, value: "!=" });
169
+ i += 2;
170
+ continue;
171
+ }
172
+ // Lone `!`, treat as a one-char word (rare; be tolerant).
173
+ tokens.push({ kind: "word", start: i, end: i + 1 });
174
+ i += 1;
175
+ continue;
176
+ }
177
+ if (c === 60 /* < */) {
178
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) {
179
+ tokens.push({ kind: "op", start: i, end: i + 2, value: "<=" });
180
+ i += 2;
181
+ } else {
182
+ tokens.push({ kind: "op", start: i, end: i + 1, value: "<" });
183
+ i += 1;
184
+ }
185
+ continue;
186
+ }
187
+ if (c === 62 /* > */) {
188
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) {
189
+ tokens.push({ kind: "op", start: i, end: i + 2, value: ">=" });
190
+ i += 2;
191
+ } else {
192
+ tokens.push({ kind: "op", start: i, end: i + 1, value: ">" });
193
+ i += 1;
194
+ }
195
+ continue;
196
+ }
197
+ if (c === 63 /* ? */) {
198
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) {
199
+ tokens.push({ kind: "op", start: i, end: i + 2, value: "?=" });
200
+ i += 2;
201
+ continue;
202
+ }
203
+ // Lone `?` not followed by `=`, fall through and treat as a word char.
204
+ // (Do not `continue`; let the word scanner below pick it up.)
205
+ }
206
+
207
+ // Word (scalar). Includes inline-math `@[ ... ]` which may contain spaces.
208
+ {
209
+ const start = i;
210
+ while (i < len) {
211
+ const cc = text.charCodeAt(i);
212
+ // Inline math bracket: `@[` ... `]` is ONE token, spaces allowed inside.
213
+ if (cc === 64 /* @ */ && i + 1 < len && text.charCodeAt(i + 1) === 91 /* [ */) {
214
+ i += 2;
215
+ while (i < len && text.charCodeAt(i) !== 93 /* ] */) {
216
+ // stop runaway at newline to stay tolerant
217
+ const inner = text.charCodeAt(i);
218
+ if (inner === 10 || inner === 13) break;
219
+ i++;
220
+ }
221
+ if (i < len && text.charCodeAt(i) === 93) i++; // consume `]`
222
+ continue;
223
+ }
224
+ // A lone `?` (not `?=`) is part of the word.
225
+ if (cc === 63 /* ? */) {
226
+ if (i + 1 < len && text.charCodeAt(i + 1) === 61 /* = */) break;
227
+ i++;
228
+ continue;
229
+ }
230
+ if (isWordTerminator(cc)) break;
231
+ i++;
232
+ }
233
+ if (i === start) {
234
+ // Defensive: unknown char we didn't advance past, consume one char as a
235
+ // word so we never loop forever on pathological input.
236
+ i++;
237
+ }
238
+ tokens.push({ kind: "word", start, end: i });
239
+ continue;
240
+ }
241
+ }
242
+
243
+ tokens.push({ kind: "eof", start: len, end: len });
244
+ return tokens;
245
+ }