@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.
- package/LICENSE +674 -0
- package/README.md +334 -0
- package/THIRD-PARTY-NOTICES.md +83 -0
- package/data/ck3/dataTypes.json +2195 -0
- package/data/ck3/data_types/data_types_common.txt +2040 -0
- package/data/ck3/data_types/data_types_gui.txt +5264 -0
- package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
- package/data/ck3/data_types/data_types_script.txt +4251 -0
- package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
- package/data/ck3/freqs.json +1 -0
- package/data/ck3/guiSchema.json +6344 -0
- package/data/ck3/script_docs/effects.log +16059 -0
- package/data/ck3/script_docs/event_targets.log +2098 -0
- package/data/ck3/script_docs/modifiers.log +2228 -0
- package/data/ck3/script_docs/on_actions.log +5275 -0
- package/data/ck3/script_docs/triggers.log +11991 -0
- package/data/ck3/structures.json +9743 -0
- package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
- package/data/ck3/wikidocs/Data_types.md +2568 -0
- package/data/ck3/wikidocs/Effects_list.md +1176 -0
- package/data/ck3/wikidocs/Scopes_list.md +341 -0
- package/data/ck3/wikidocs/Triggers_list.md +1097 -0
- package/data/eu5/data_types/data_types_common.txt +2087 -0
- package/data/eu5/data_types/data_types_gui.txt +6732 -0
- package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
- package/data/eu5/data_types/data_types_script.txt +5688 -0
- package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
- package/data/vic3/data_types/data_types_common.txt +2021 -0
- package/data/vic3/data_types/data_types_gui.txt +5592 -0
- package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
- package/data/vic3/data_types/data_types_script.txt +2817 -0
- package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
- package/data/vic3/freqs.json +1 -0
- package/data/vic3/guiSchema.json +5578 -0
- package/data/vic3/script_docs/effects.log +38135 -0
- package/data/vic3/script_docs/event_targets.log +2028 -0
- package/data/vic3/script_docs/modifiers.log +18954 -0
- package/data/vic3/script_docs/on_actions.log +1561 -0
- package/data/vic3/script_docs/triggers.log +15738 -0
- package/data/vic3/structures.json +10189 -0
- package/dist/server.js +63668 -0
- package/media/px-lsp.svg +12 -0
- package/package.json +50 -0
- package/src/clientMode.ts +60 -0
- package/src/coa/coa.ts +184 -0
- package/src/coa/coaParse.ts +267 -0
- package/src/context.ts +78 -0
- package/src/contextKeywords.ts +224 -0
- package/src/data/dataBindingMacros.ts +82 -0
- package/src/data/dataFnDocs.ts +152 -0
- package/src/data/dataFnUsage.ts +431 -0
- package/src/data/dataTypes.ts +279 -0
- package/src/data/defines.ts +123 -0
- package/src/data/docsParser.ts +453 -0
- package/src/data/keywordDocs.ts +98 -0
- package/src/data/modifierTemplates.ts +143 -0
- package/src/data/textFormatting.ts +165 -0
- package/src/data/wikiDocs.ts +187 -0
- package/src/dds/decoder.ts +1007 -0
- package/src/dds/encode.ts +235 -0
- package/src/dds/index.ts +58 -0
- package/src/dds/png.ts +96 -0
- package/src/dds/tga.ts +62 -0
- package/src/documents.ts +35 -0
- package/src/features/assetPaths.ts +169 -0
- package/src/features/codeActions.ts +148 -0
- package/src/features/colors.ts +244 -0
- package/src/features/completion.ts +961 -0
- package/src/features/datafunction.ts +729 -0
- package/src/features/definition.ts +84 -0
- package/src/features/diagnostics.ts +244 -0
- package/src/features/folding.ts +106 -0
- package/src/features/formatting.ts +60 -0
- package/src/features/guiLanguage.ts +366 -0
- package/src/features/guiNavigation.ts +140 -0
- package/src/features/guiTree.ts +97 -0
- package/src/features/hover.ts +817 -0
- package/src/features/hoverRender.ts +222 -0
- package/src/features/inlayHints.ts +147 -0
- package/src/features/locFormatting.ts +127 -0
- package/src/features/references.ts +70 -0
- package/src/features/rename.ts +135 -0
- package/src/features/scopeAt.ts +65 -0
- package/src/features/semanticTokens.ts +188 -0
- package/src/features/signatureHelp.ts +72 -0
- package/src/features/symbols.ts +241 -0
- package/src/features/textureHover.ts +143 -0
- package/src/features/workspaceSymbols.ts +69 -0
- package/src/games/active.ts +19 -0
- package/src/games/ck3/ambientScopes.ts +273 -0
- package/src/games/ck3/index.ts +38 -0
- package/src/games/ck3/meta.ts +28 -0
- package/src/games/ck3/modifierPlaceholders.ts +61 -0
- package/src/games/ck3/saveSchema.ts +134 -0
- package/src/games/ck3/scaffolds.ts +197 -0
- package/src/games/ck3/schema.ts +422 -0
- package/src/games/ck3/structures.ts +887 -0
- package/src/games/eu5/index.ts +75 -0
- package/src/games/eu5/meta.ts +44 -0
- package/src/games/eu5/scaffolds.ts +49 -0
- package/src/games/eu5/schema.generated.ts +1043 -0
- package/src/games/jomini/variables.ts +134 -0
- package/src/games/profile.ts +205 -0
- package/src/games/registry.ts +27 -0
- package/src/games/vic3/index.ts +52 -0
- package/src/games/vic3/meta.ts +55 -0
- package/src/games/vic3/saveSchema.ts +77 -0
- package/src/games/vic3/scaffolds.ts +135 -0
- package/src/games/vic3/schema.ts +650 -0
- package/src/games/vic3/structures.ts +33 -0
- package/src/gui/anchorSpec.ts +66 -0
- package/src/gui/declMarkers.ts +30 -0
- package/src/gui/fillGeometry.ts +101 -0
- package/src/gui/guiDefs.ts +386 -0
- package/src/gui/guiDependencies.ts +352 -0
- package/src/gui/guiLinks.ts +64 -0
- package/src/gui/layoutEngine.ts +1998 -0
- package/src/gui/layoutService.ts +221 -0
- package/src/gui/measuredMetrics.ts +21 -0
- package/src/gui/previewService.ts +89 -0
- package/src/gui/saveSchema.ts +220 -0
- package/src/gui/saveValues.ts +399 -0
- package/src/gui/saveZip.ts +60 -0
- package/src/gui/sourceEdit.ts +535 -0
- package/src/gui/sourceEditService.ts +439 -0
- package/src/gui/sourceModel.ts +603 -0
- package/src/gui/textResolve.ts +145 -0
- package/src/gui/textureInfo.ts +106 -0
- package/src/gui/vocabulary.ts +149 -0
- package/src/gui/widgetEdit.ts +52 -0
- package/src/gui/widgetInfo.ts +245 -0
- package/src/index/docComments.ts +103 -0
- package/src/index/extract.ts +252 -0
- package/src/index/indexer.ts +369 -0
- package/src/index/intern.ts +101 -0
- package/src/index/lazyRefs.ts +145 -0
- package/src/index/modOrigin.ts +69 -0
- package/src/index/references.ts +534 -0
- package/src/overview/dependencies.ts +240 -0
- package/src/overview/eventBanner.ts +95 -0
- package/src/overview/eventDetail.ts +482 -0
- package/src/overview/eventGraph.ts +617 -0
- package/src/overview/eventVocabulary.ts +214 -0
- package/src/overview/locCoverage.ts +138 -0
- package/src/overview/modOverview.ts +29 -0
- package/src/overview/overrides.ts +89 -0
- package/src/parseCache.ts +81 -0
- package/src/parser/cst.ts +257 -0
- package/src/parser/encoding.ts +106 -0
- package/src/parser/index.ts +7 -0
- package/src/parser/lexer.ts +245 -0
- package/src/parser/locParser.ts +276 -0
- package/src/parser/parser.ts +360 -0
- package/src/schema/freqs.ts +70 -0
- package/src/schema/loader.ts +113 -0
- package/src/schema/types.ts +142 -0
- package/src/scopes/inference.ts +478 -0
- package/src/scopes/model.ts +148 -0
- package/src/scopes/varTypes.ts +290 -0
- package/src/server.ts +1894 -0
- package/src/serverData.ts +98 -0
- package/src/structure.ts +56 -0
- 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,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
|
+
}
|