@amxts/config-core 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 +21 -0
- package/PAWN.md +13 -0
- package/PAWN.ru.md +13 -0
- package/README.md +249 -0
- package/README.ru.md +249 -0
- package/include/universal_config.inc +355 -0
- package/package.json +67 -0
- package/prebuilt/linux/config-core.aot +0 -0
- package/prebuilt/manifest.json +2281 -0
- package/prebuilt/windows/config-core.aot +0 -0
- package/scripts/docs/src/index.ts +197 -0
- package/scripts/docs/src/types.ts +20 -0
- package/src/files.ts +27 -0
- package/src/index.ts +360 -0
- package/src/ini-tree.ts +144 -0
- package/src/ini.ts +723 -0
- package/src/internal.ts +44 -0
- package/src/json.ts +386 -0
- package/src/natives.ts +271 -0
- package/src/tree.ts +171 -0
- package/src/typed.ts +419 -0
- package/src/types.ts +22 -0
- package/src/yaml.ts +840 -0
package/src/ini.ts
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The INI engine of the original universal_config: files of [sections] with
|
|
3
|
+
* keys, blocks and rows, read and written back with their comments, keys
|
|
4
|
+
* found by "a/b/c" paths and values by their index. The 28 cfg_* natives are
|
|
5
|
+
* made of it (natives.ts), and `read()` reads an INI file through it. Not
|
|
6
|
+
* part of the API: a TypeScript plugin reads a config with `load()` or
|
|
7
|
+
* `read()`.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "~/fs";
|
|
10
|
+
import { configPath, writeLines } from "./files";
|
|
11
|
+
import { writeComments } from "./tree";
|
|
12
|
+
|
|
13
|
+
/** A `key = value` line, or a `key = { ... }` block. */
|
|
14
|
+
export type EntryKind = "value" | "block";
|
|
15
|
+
|
|
16
|
+
/** What a block holds: a line of strings, or rows. The original's CFG_CONTENT_SIMPLE is a line of one string. */
|
|
17
|
+
export type ContentKind = "strings" | "entries";
|
|
18
|
+
|
|
19
|
+
/** A line of a section or a row of a block. */
|
|
20
|
+
export interface Entry {
|
|
21
|
+
/** The name before `=`; "" for a row of a block. */
|
|
22
|
+
key: string;
|
|
23
|
+
kind: EntryKind;
|
|
24
|
+
content: ContentKind;
|
|
25
|
+
/** The entry's values, in order; empty for a block of rows. */
|
|
26
|
+
values: string[];
|
|
27
|
+
/** A block's rows; empty otherwise. */
|
|
28
|
+
rows: Entry[];
|
|
29
|
+
/** The comment and blank lines read before it; null for one made at run time. */
|
|
30
|
+
comments: string[] | null;
|
|
31
|
+
/** The line of the file it was read from, from 1; 0 for one made at run time. */
|
|
32
|
+
line: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A [section] of a config file. */
|
|
36
|
+
export interface Section {
|
|
37
|
+
name: string;
|
|
38
|
+
/** The comment and blank lines read before it; null for one made at run time. */
|
|
39
|
+
comments: string[] | null;
|
|
40
|
+
/** The section's lines, in file order. */
|
|
41
|
+
entries: Entry[];
|
|
42
|
+
/** The line of the file it was read from, from 1; 0 for one made at run time. */
|
|
43
|
+
line: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A loaded config file. */
|
|
47
|
+
export interface Config {
|
|
48
|
+
/** The name it was loaded under, ".ini" included. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** In file order; a name the file has twice is there twice. */
|
|
51
|
+
sections: Section[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One entry of a section as `entries()` lists it. */
|
|
55
|
+
export interface SectionEntry {
|
|
56
|
+
key: string;
|
|
57
|
+
/** The entry's values; empty for a block. */
|
|
58
|
+
values: string[];
|
|
59
|
+
block: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A dump of one section - the heading, then the entries - as `dump_config` prints it. */
|
|
63
|
+
export interface SectionDump {
|
|
64
|
+
/** "Section 0: NAME". */
|
|
65
|
+
heading: string;
|
|
66
|
+
lines: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where a path leads: the entry, and the line of a block it means. */
|
|
70
|
+
interface Found {
|
|
71
|
+
entry: Entry;
|
|
72
|
+
line: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Every config and every section loaded, in the order they came: a Pawn
|
|
76
|
+
// plugin knows them by their place here.
|
|
77
|
+
const configs: Config[] = [];
|
|
78
|
+
const sections: Section[] = [];
|
|
79
|
+
|
|
80
|
+
function entry(key: string, kind: EntryKind, values: string[], comments: string[] | null, line = 0) {
|
|
81
|
+
const made: Entry = { key, kind, content: kind == "block" ? "entries" : "strings", values, rows: [], comments, line };
|
|
82
|
+
return made;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function addSection(config: Config, name: string, comments: string[] | null, line = 0) {
|
|
86
|
+
const made: Section = { name, comments, entries: [], line };
|
|
87
|
+
config.sections.push(made);
|
|
88
|
+
return made;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Keys are compared case-insensitively, as equali does. */
|
|
92
|
+
function sameKey(a: string, b: string) {
|
|
93
|
+
return a.toLowerCase() == b.toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** "text" without its quotes; anything else as it is. */
|
|
97
|
+
function unquote(text: string) {
|
|
98
|
+
return text.length >= 2 && text.startsWith("\"") && text.endsWith("\"") ? text.slice(1, -1) : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Words split on spaces and tabs; a "quoted" one is kept whole, an empty "" dropped. */
|
|
102
|
+
function splitValues(text: string) {
|
|
103
|
+
const words: string[] = [];
|
|
104
|
+
let rest = text.replaceAll("\t", " ").trim();
|
|
105
|
+
while (rest.length > 0) {
|
|
106
|
+
if (rest.startsWith("\"")) {
|
|
107
|
+
let close = rest.indexOf("\"", 1);
|
|
108
|
+
if (close < 0) close = rest.length;
|
|
109
|
+
const word = rest.slice(1, close);
|
|
110
|
+
if (word.length > 0) words.push(word);
|
|
111
|
+
rest = rest.slice(close + 1).trim();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let space = rest.indexOf(" ");
|
|
116
|
+
if (space < 0) space = rest.length;
|
|
117
|
+
words.push(rest.slice(0, space));
|
|
118
|
+
rest = rest.slice(space).trim();
|
|
119
|
+
}
|
|
120
|
+
return words;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A row of "quoted" "values"; only spaces may stand between them - a tab ends the row. */
|
|
124
|
+
function quotedRow(line: string) {
|
|
125
|
+
const values: string[] = [];
|
|
126
|
+
let rest = line;
|
|
127
|
+
while (rest.startsWith("\"")) {
|
|
128
|
+
let close = rest.indexOf("\"", 1);
|
|
129
|
+
if (close < 0) close = rest.length;
|
|
130
|
+
values.push(rest.slice(1, close).trim());
|
|
131
|
+
rest = rest.slice(close + 1);
|
|
132
|
+
while (rest.startsWith(" ")) rest = rest.slice(1);
|
|
133
|
+
}
|
|
134
|
+
return values;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** `key = value ; comment` as the key and the value. */
|
|
138
|
+
function keyAndValue(line: string) {
|
|
139
|
+
const equals = line.indexOf("=");
|
|
140
|
+
const key = equals < 0 ? "" : line.slice(0, equals).trim();
|
|
141
|
+
const value = equals < 0 ? line : line.slice(equals + 1);
|
|
142
|
+
return [key, value.split(";")[0].trim()];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** "a/b/c" as its parts, empty ones skipped. */
|
|
146
|
+
function splitPath(path: string) {
|
|
147
|
+
return path.split("/").map(part => part.trim()).filter(part => part.length > 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function withIni(name: string) {
|
|
151
|
+
return name.includes(".ini") ? name : `${name}.ini`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The whole number the text starts with; 0 when there is none. */
|
|
155
|
+
function toInt(text: string) {
|
|
156
|
+
const value = parseInt(text, 10);
|
|
157
|
+
return isNaN(value) ? 0 : value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The number the text starts with; 0 when there is none. */
|
|
161
|
+
function toNumber(text: string) {
|
|
162
|
+
const value = parseFloat(text.trim());
|
|
163
|
+
return isNaN(value) ? 0 : value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function named(entries: Entry[], key: string) {
|
|
167
|
+
return entries.filter(each => sameKey(each.key, key));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** How many a key holds: rows of a block of rows, values otherwise. */
|
|
171
|
+
function sizeOf(entry: Entry) {
|
|
172
|
+
return entry.kind == "block" && entry.content == "entries" ? entry.rows.length : entry.values.length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The row a path's parts lead to under `rows`. Where a part matches several
|
|
177
|
+
* rows, `line` picks one, and the parts after it start again from the first.
|
|
178
|
+
*/
|
|
179
|
+
function follow(rows: Entry[], parts: string[], line: number) {
|
|
180
|
+
let current = rows;
|
|
181
|
+
let target = line;
|
|
182
|
+
let found: Entry | null = null;
|
|
183
|
+
for (const part of parts) {
|
|
184
|
+
const matches = named(current, part);
|
|
185
|
+
if (matches.length == 0) return null;
|
|
186
|
+
|
|
187
|
+
if (matches.length == 1) {
|
|
188
|
+
found = matches[0];
|
|
189
|
+
} else {
|
|
190
|
+
if (target >= matches.length) return null;
|
|
191
|
+
found = matches[target];
|
|
192
|
+
target = 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
current = found.rows;
|
|
196
|
+
}
|
|
197
|
+
if (found == null) return null;
|
|
198
|
+
const result: Found = { entry: found, line: target };
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The values of row `line` of a block, when it is a line of values. */
|
|
203
|
+
function rowValues(rows: Entry[], line: number) {
|
|
204
|
+
if (line >= rows.length || rows[line].kind != "value") return null;
|
|
205
|
+
return rows[line].values;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The `index`-th value without its quotes, or null past the end. */
|
|
209
|
+
function pick(values: string[] | null, index: number) {
|
|
210
|
+
if (values == null || index >= values.length) return null;
|
|
211
|
+
return unquote(values[index]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The values a path leads to, `line` in its block. With the key in the
|
|
216
|
+
* section more than once and no line asked for, `index` picks which one -
|
|
217
|
+
* and, as in the original, still picks the value in it.
|
|
218
|
+
*/
|
|
219
|
+
function valuesAt(section: Section, path: string, index: number, line: number) {
|
|
220
|
+
const parts = splitPath(path);
|
|
221
|
+
if (parts.length == 0 || index < 0 || line < 0) return null;
|
|
222
|
+
const matches = named(section.entries, parts[0]);
|
|
223
|
+
if (matches.length == 0) return null;
|
|
224
|
+
|
|
225
|
+
let occurrence = line;
|
|
226
|
+
let target = line;
|
|
227
|
+
|
|
228
|
+
if (matches.length == 1) {
|
|
229
|
+
occurrence = 0;
|
|
230
|
+
} else if (line == 0) {
|
|
231
|
+
occurrence = index;
|
|
232
|
+
target = 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (occurrence >= matches.length) return null;
|
|
236
|
+
|
|
237
|
+
let top = matches[occurrence];
|
|
238
|
+
|
|
239
|
+
if (parts.length > 1) {
|
|
240
|
+
const found = follow(top.rows, parts.slice(1), target);
|
|
241
|
+
if (found == null) return null;
|
|
242
|
+
top = found.entry;
|
|
243
|
+
target = found.line;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return top.kind == "block" ? rowValues(top.rows, target) : top.values;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A plain key's value: the first entry of that name, when it is a line of values. */
|
|
250
|
+
function plainValue(section: Section, key: string, index: number) {
|
|
251
|
+
const matches = named(section.entries, key);
|
|
252
|
+
if (matches.length == 0 || matches[0].kind != "value" || index < 0) return null;
|
|
253
|
+
return pick(matches[0].values, index);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The `index`-th value of a key's line, without its quotes; null when there is none. A key with "/" is a path. */
|
|
257
|
+
export function getValue(section: Section, key: string, index = 0) {
|
|
258
|
+
return key.includes("/") ? pick(valuesAt(section, key, index, 0), index) : plainValue(section, key, index);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function putValue(values: string[], index: number, value: string) {
|
|
262
|
+
while (values.length <= index) values.push("");
|
|
263
|
+
values[index] = value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** A value of row `line` of a block, the rows up to it made when missing. */
|
|
267
|
+
function setRow(block: Entry, value: string, index: number, line: number) {
|
|
268
|
+
if (line < 0) return false;
|
|
269
|
+
while (block.rows.length <= line) block.rows.push(entry("", "value", [], null));
|
|
270
|
+
putValue(block.rows[line].values, index, value);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** A plain key: a line of values - `index` picks which, when it is there more than once - or a row of its block. */
|
|
275
|
+
function setPlain(section: Section, key: string, value: string, index: number, line: number) {
|
|
276
|
+
const matches = named(section.entries, key);
|
|
277
|
+
let target = matches.find((each, i) => each.kind == "block" || i == index);
|
|
278
|
+
|
|
279
|
+
if (target == null) {
|
|
280
|
+
target = entry(key, "value", [], null);
|
|
281
|
+
section.entries.push(target);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (target.kind == "block" && target.content == "entries") return setRow(target, value, index, line);
|
|
285
|
+
putValue(target.values, index, value);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A path "a/b/c": the blocks on the way are made when missing; `index` picks every part's occurrence and the value. */
|
|
290
|
+
function setByPath(section: Section, parts: string[], value: string, index: number) {
|
|
291
|
+
let top: Entry | null = null;
|
|
292
|
+
const matches = named(section.entries, parts[0]);
|
|
293
|
+
if (index < matches.length) top = matches[index];
|
|
294
|
+
|
|
295
|
+
if (top == null) {
|
|
296
|
+
top = entry(parts[0], "block", [], null);
|
|
297
|
+
section.entries.push(top);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (top.kind != "block") return false;
|
|
301
|
+
|
|
302
|
+
let rows = top.rows;
|
|
303
|
+
for (let level = 1; level < parts.length; level++) {
|
|
304
|
+
const last = level == parts.length - 1;
|
|
305
|
+
const found = named(rows, parts[level]);
|
|
306
|
+
let row: Entry | null = index < found.length ? found[index] : null;
|
|
307
|
+
|
|
308
|
+
if (row == null) {
|
|
309
|
+
row = entry(parts[level], last ? "value" : "block", [], null);
|
|
310
|
+
rows.push(row);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (last) {
|
|
314
|
+
if (row.kind != "value") return false;
|
|
315
|
+
putValue(row.values, index, value);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (row.kind != "block") return false;
|
|
320
|
+
rows = row.rows;
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Reads an INI text into the config's sections. */
|
|
326
|
+
export function readSections(config: Config, text: string) {
|
|
327
|
+
let section: Section | null = null;
|
|
328
|
+
// The blocks open where the reader is, innermost last.
|
|
329
|
+
const open: Entry[] = [];
|
|
330
|
+
// Comments and blank lines wait for what comes next and are written back before it.
|
|
331
|
+
let pending: string[] | null = null;
|
|
332
|
+
|
|
333
|
+
const rows = text.split("\n");
|
|
334
|
+
for (let lineNumber = 1; lineNumber <= rows.length; lineNumber++) {
|
|
335
|
+
const line = rows[lineNumber - 1].trim();
|
|
336
|
+
|
|
337
|
+
if (line.length == 0 || line.startsWith(";")) {
|
|
338
|
+
pending ??= [];
|
|
339
|
+
pending.push(line);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (line.startsWith("[")) {
|
|
344
|
+
const close = line.indexOf("]");
|
|
345
|
+
section = addSection(config, close < 0 ? line.slice(1) : line.slice(1, close), pending, lineNumber);
|
|
346
|
+
open.length = 0;
|
|
347
|
+
pending = null;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Lines before the first section belong to none.
|
|
352
|
+
if (section == null) continue;
|
|
353
|
+
|
|
354
|
+
if (line.startsWith("}")) {
|
|
355
|
+
open.pop();
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const into = open.length > 0 ? open[open.length - 1].rows : section.entries;
|
|
360
|
+
|
|
361
|
+
if (line.includes("=") && line.endsWith("{")) {
|
|
362
|
+
const block = entry(keyAndValue(line)[0], "block", [], pending, lineNumber);
|
|
363
|
+
pending = null;
|
|
364
|
+
into.push(block);
|
|
365
|
+
open.push(block);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (open.length > 0 && line.startsWith("\"")) {
|
|
370
|
+
into.push(entry("", "value", quotedRow(line), pending, lineNumber));
|
|
371
|
+
pending = null;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pair = keyAndValue(line);
|
|
376
|
+
into.push(entry(pair[0], "value", splitValues(pair[1]), pending, lineNumber));
|
|
377
|
+
pending = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function quoted(values: string[]) {
|
|
382
|
+
return values.map(value => `"${value}"`).join(" ");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** An entry and what is inside it; a keyless row goes out as "quoted" "values". */
|
|
386
|
+
function writeEntry(lines: string[], written: Entry, level: number) {
|
|
387
|
+
const indent = "\t".repeat(level);
|
|
388
|
+
writeComments(lines, written.comments, indent);
|
|
389
|
+
|
|
390
|
+
if (written.kind == "value") {
|
|
391
|
+
const values = written.key.length == 0 ? quoted(written.values) : `${written.key} = ${written.values.join(" ")}`;
|
|
392
|
+
lines.push(`${indent}${values}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
lines.push(`${indent}${written.key} = {`);
|
|
397
|
+
if (written.content == "strings") lines.push(`${indent}\t${quoted(written.values)}`);
|
|
398
|
+
|
|
399
|
+
if (written.content == "entries") {
|
|
400
|
+
for (const row of written.rows) writeEntry(lines, row, level + 1);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lines.push(`${indent}}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function writeSectionLines(lines: string[], written: Section, spaced: boolean) {
|
|
407
|
+
writeComments(lines, written.comments, "");
|
|
408
|
+
lines.push(`[${written.name}]`);
|
|
409
|
+
for (const each of written.entries) {
|
|
410
|
+
// As the original saves a file: an entry with no comment lines before it
|
|
411
|
+
// gets a blank one. Writing a single section does not.
|
|
412
|
+
if (spaced && each.comments == null) lines.push("");
|
|
413
|
+
writeEntry(lines, each, 0);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function dumpEntry(lines: string[], dumped: Entry, level: number) {
|
|
418
|
+
const indent = "\t".repeat(level);
|
|
419
|
+
|
|
420
|
+
if (dumped.kind == "value") {
|
|
421
|
+
lines.push(`${indent}${dumped.key} = ${dumped.values.join(" ")}`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
lines.push(`${indent}${dumped.key} = {`);
|
|
426
|
+
if (dumped.content == "strings") lines.push(`${indent} ${dumped.values.join(" ")}`);
|
|
427
|
+
|
|
428
|
+
if (dumped.content == "entries") {
|
|
429
|
+
for (const row of dumped.rows) dumpEntry(lines, row, level + 1);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
lines.push(`${indent}}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Loads `configs/<baseDir>/<name>`; ".ini" is added when the name has none.
|
|
437
|
+
* A file that is not there loads empty, for a plugin to fill and save.
|
|
438
|
+
*/
|
|
439
|
+
export function loadFile(name: string) {
|
|
440
|
+
const config: Config = { name: withIni(name.trim()), sections: [] };
|
|
441
|
+
configs.push(config);
|
|
442
|
+
const text = fs.readFileSync(configPath(config.name));
|
|
443
|
+
if (text != null) readSections(config, text);
|
|
444
|
+
for (const each of config.sections) sections.push(each);
|
|
445
|
+
return config;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** A section of a config by its name - the last one, when the file has two. */
|
|
449
|
+
export function section(config: Config, name: string) {
|
|
450
|
+
return config.sections.findLast(each => each.name == name);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The section, made when the config does not have it. */
|
|
454
|
+
export function createSection(config: Config, name: string) {
|
|
455
|
+
const found = section(config, name);
|
|
456
|
+
if (found != null) return found;
|
|
457
|
+
const made = addSection(config, name, null);
|
|
458
|
+
sections.push(made);
|
|
459
|
+
return made;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Writes every section of a config to `configs/<baseDir>/<name>`, with the
|
|
464
|
+
* comments and blank lines it was read with. A section or an entry made at
|
|
465
|
+
* run time gets a blank line before it. False without a name.
|
|
466
|
+
*/
|
|
467
|
+
export function saveFile(config: Config, name: string) {
|
|
468
|
+
const file = name.trim();
|
|
469
|
+
if (file.length == 0) return false;
|
|
470
|
+
const lines: string[] = [];
|
|
471
|
+
for (const each of config.sections) {
|
|
472
|
+
// A name the file has twice is the last section of that name.
|
|
473
|
+
if (section(config, each.name) != each) continue;
|
|
474
|
+
if (lines.length > 0 && each.comments == null) lines.push("");
|
|
475
|
+
writeSectionLines(lines, each, true);
|
|
476
|
+
}
|
|
477
|
+
return writeLines(configPath(withIni(file)), lines);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Writes one section alone to `configs/<baseDir>/<name>`; `saveFile()` writes them all. */
|
|
481
|
+
export function writeSection(written: Section, name: string) {
|
|
482
|
+
if (name.length == 0) return false;
|
|
483
|
+
const lines: string[] = [];
|
|
484
|
+
writeSectionLines(lines, written, false);
|
|
485
|
+
lines.push("");
|
|
486
|
+
return writeLines(configPath(withIni(name)), lines);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** A dump of every loaded config - each section with its entries, as `dump_config` prints them. */
|
|
490
|
+
export function dump() {
|
|
491
|
+
const dumped: SectionDump[] = [];
|
|
492
|
+
for (let i = 0; i < sections.length; i++) {
|
|
493
|
+
const lines: string[] = [];
|
|
494
|
+
for (const each of sections[i].entries) dumpEntry(lines, each, 0);
|
|
495
|
+
dumped.push({ heading: `Section ${i}: ${sections[i].name}`, lines });
|
|
496
|
+
}
|
|
497
|
+
return dumped;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A value by a path, `line` a row of its block. With the key in the section
|
|
502
|
+
* more than once and no line asked for, `index` picks which one - and, as in
|
|
503
|
+
* the original, still picks the value in it.
|
|
504
|
+
*/
|
|
505
|
+
export function getValueByPath(section: Section, path: string, index = 0, line = 0) {
|
|
506
|
+
return pick(valuesAt(section, path, index, line), index);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** A whole number; 0 when there is none. */
|
|
510
|
+
export function getInt(section: Section, key: string, index = 0) {
|
|
511
|
+
const text = getValue(section, key, index);
|
|
512
|
+
return text != null ? toInt(text) : 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** A number; 0 when there is none. */
|
|
516
|
+
export function getNumber(section: Section, key: string, index = 0) {
|
|
517
|
+
const text = getValue(section, key, index);
|
|
518
|
+
return text != null ? toNumber(text) : 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** cfg_get_bool's rule: true for a whole number other than 0, e.g. "1" or "2"; false for anything else, "true" included. */
|
|
522
|
+
export function getBoolean(section: Section, key: string, index = 0) {
|
|
523
|
+
return getInt(section, key, index) != 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The words of a key's line - of its `index`-th line when the key is there
|
|
528
|
+
* more than once. For a block, every value of every row. Null when there is
|
|
529
|
+
* no such key or nothing in it.
|
|
530
|
+
*/
|
|
531
|
+
export function getWords(section: Section, key: string, index = 0) {
|
|
532
|
+
const matches = named(section.entries, key);
|
|
533
|
+
if (index >= matches.length) return null;
|
|
534
|
+
const found = matches[index];
|
|
535
|
+
|
|
536
|
+
const list: string[] = [];
|
|
537
|
+
|
|
538
|
+
if (found.kind == "block" && found.content == "entries") {
|
|
539
|
+
for (const row of found.rows) {
|
|
540
|
+
if (row.kind == "value") {
|
|
541
|
+
for (const each of row.values) list.push(unquote(each));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else if (found.kind == "block") {
|
|
545
|
+
for (const each of found.values) list.push(unquote(each));
|
|
546
|
+
} else if (found.values.length > 0) {
|
|
547
|
+
// The first value, split again: `KEY = "a b" c` gives a and b.
|
|
548
|
+
for (const word of splitValues(found.values[0])) list.push(word);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return list.length > 0 ? list : null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** The numbers in the value `index` - "1.0 2.0 3.0" is three; null when there is no value. */
|
|
555
|
+
export function getNumbers(section: Section, key: string, index = 0) {
|
|
556
|
+
const text = getValue(section, key, index);
|
|
557
|
+
if (text == null) return null;
|
|
558
|
+
return splitValues(text).map(toNumber);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Every value of the line a path leads to, quotes and all; [] for an empty line, null for none. */
|
|
562
|
+
export function getValues(section: Section, path: string, index = 0, line = 0) {
|
|
563
|
+
const found = valuesAt(section, path, index, line);
|
|
564
|
+
return found != null ? found.slice(0) : null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** The section's keys, in file order. */
|
|
568
|
+
export function keys(section: Section) {
|
|
569
|
+
return section.entries.map(each => each.key);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Every entry: its key, and its values when it is a line. */
|
|
573
|
+
export function entries(section: Section) {
|
|
574
|
+
const list: SectionEntry[] = [];
|
|
575
|
+
for (const each of section.entries) {
|
|
576
|
+
const block = each.kind == "block";
|
|
577
|
+
list.push({ key: each.key, values: block ? [] : each.values.slice(0), block });
|
|
578
|
+
}
|
|
579
|
+
return list;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The number of values in a line, of rows in a block, or - for a key that is
|
|
584
|
+
* there more than once - of times it is there. A path counts what it leads to.
|
|
585
|
+
*/
|
|
586
|
+
export function size(section: Section, key: string) {
|
|
587
|
+
const parts = splitPath(key);
|
|
588
|
+
if (parts.length == 0) return 0;
|
|
589
|
+
const matches = named(section.entries, parts[0]);
|
|
590
|
+
if (matches.length == 0) return 0;
|
|
591
|
+
if (parts.length == 1) return matches.length > 1 ? matches.length : sizeOf(matches[0]);
|
|
592
|
+
const found = follow(matches[0].rows, parts.slice(1), 0);
|
|
593
|
+
return found != null ? sizeOf(found.entry) : 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Whether the section has the key. A key with "/" is not a path here. */
|
|
597
|
+
export function has(section: Section, key: string) {
|
|
598
|
+
return named(section.entries, key).length > 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Sets a value. A key that is not there is made; a value past the end of its
|
|
603
|
+
* line is added with empty ones before it. For a block, `line` is the row -
|
|
604
|
+
* made, with the rows before it, when missing. A path makes the blocks on its
|
|
605
|
+
* way.
|
|
606
|
+
*/
|
|
607
|
+
export function set(section: Section, key: string, text: string, index = 0, line = 0) {
|
|
608
|
+
const parts = splitPath(key);
|
|
609
|
+
if (parts.length == 0 || index < 0) return false;
|
|
610
|
+
if (parts.length == 1) return setPlain(section, parts[0], text, index, line);
|
|
611
|
+
return setByPath(section, parts, text, index);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Sets a whole number; the fraction is dropped: 2.7 is written as 2. */
|
|
615
|
+
export function setInt(section: Section, key: string, value: number, index = 0) {
|
|
616
|
+
return set(section, key, `${Math.trunc(value)}`, index);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Sets a number, written as is: 2.5, not 2.500000. */
|
|
620
|
+
export function setNumber(section: Section, key: string, value: number, index = 0) {
|
|
621
|
+
return set(section, key, `${value}`, index);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Written as 1 or 0. */
|
|
625
|
+
export function setBoolean(section: Section, key: string, value: boolean, index = 0) {
|
|
626
|
+
return set(section, key, value ? "1" : "0", index);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Removes every entry of the key; false when there was none. */
|
|
630
|
+
export function remove(section: Section, key: string) {
|
|
631
|
+
const kept = section.entries.filter(each => !sameKey(each.key, key));
|
|
632
|
+
const removed = kept.length != section.entries.length;
|
|
633
|
+
section.entries = kept;
|
|
634
|
+
return removed;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Makes the key a line of values or a block, emptying it when it changes; a
|
|
639
|
+
* key that is not there is made. Turning it into a block drops the other
|
|
640
|
+
* entries of the key.
|
|
641
|
+
*/
|
|
642
|
+
export function setKind(section: Section, key: string, kind: EntryKind) {
|
|
643
|
+
const matches = named(section.entries, key);
|
|
644
|
+
|
|
645
|
+
if (matches.length == 0) {
|
|
646
|
+
section.entries.push(entry(key, kind, [], null));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const target = matches[0];
|
|
651
|
+
|
|
652
|
+
if (target.kind != kind) {
|
|
653
|
+
target.values = [];
|
|
654
|
+
target.rows = [];
|
|
655
|
+
target.content = kind == "block" ? "entries" : "strings";
|
|
656
|
+
target.kind = kind;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (kind == "block") {
|
|
660
|
+
for (let i = 1; i < matches.length; i++) section.entries.splice(section.entries.indexOf(matches[i]), 1);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Says what the key holds; between rows and text it is emptied. A key that is not there is made. */
|
|
665
|
+
export function setContent(section: Section, key: string, content: ContentKind) {
|
|
666
|
+
const matches = named(section.entries, key);
|
|
667
|
+
|
|
668
|
+
if (matches.length == 0) {
|
|
669
|
+
const made = entry(key, "value", [], null);
|
|
670
|
+
made.content = content;
|
|
671
|
+
section.entries.push(made);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const target = matches[0];
|
|
676
|
+
|
|
677
|
+
if (target.content != content) {
|
|
678
|
+
target.values = [];
|
|
679
|
+
target.rows = [];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
target.content = content;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** The comment written before row `row` of the block `key`; "" removes it. False when there is no such row. */
|
|
686
|
+
export function setRowComment(section: Section, key: string, row: number, comment: string) {
|
|
687
|
+
const matches = named(section.entries, key);
|
|
688
|
+
if (matches.length == 0) return false;
|
|
689
|
+
const block = matches[0];
|
|
690
|
+
if (block.kind != "block" || block.content != "entries" || row < 0 || row >= block.rows.length) return false;
|
|
691
|
+
block.rows[row].comments = comment.length > 0 ? [comment] : null;
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** A config's handle for Pawn plugins: its number among the loaded ones; -1 for none. */
|
|
696
|
+
export function configHandle(config: Config) {
|
|
697
|
+
return configs.indexOf(config);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** The config a Pawn plugin's handle stands for; null for none. */
|
|
701
|
+
export function configByHandle(handle: number) {
|
|
702
|
+
return handle >= 0 && handle < configs.length ? configs[handle] : null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** A section's handle for Pawn plugins: its number among every loaded one; -1 for none. */
|
|
706
|
+
export function sectionHandle(found: Section) {
|
|
707
|
+
return sections.indexOf(found);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** The section a Pawn plugin's handle stands for; null for none. */
|
|
711
|
+
export function sectionByHandle(handle: number) {
|
|
712
|
+
return handle >= 0 && handle < sections.length ? sections[handle] : null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** A section of that name in any config: the last one loaded. */
|
|
716
|
+
export function findSection(name: string) {
|
|
717
|
+
return sections.findLast(each => each.name == name);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** How many sections are loaded, across every config. */
|
|
721
|
+
export function sectionCount() {
|
|
722
|
+
return sections.length;
|
|
723
|
+
}
|