deno.land / std@0.224.0 / ini / ini_map.ts

ini_map.ts
View Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.// This module is browser compatible.
/** Options for providing formatting marks. */export interface FormattingOptions { /** The character used to assign a value to a key; defaults to '='. */ assignment?: string; /** Character(s) used to break lines in the config file; defaults to '\n'. Ignored on parse. */ lineBreak?: "\n" | "\r\n"; /** Mark to use for setting comments; expects '#', ';', '//', defaults to '#' unless another mark is found. */ commentChar?: "#" | ";" | "//"; /** Use a plain assignment char or pad with spaces; defaults to false. Ignored on parse. */ pretty?: boolean; /** Filter duplicate keys from INI string output; defaults to false to preserve data parity. */ deduplicate?: boolean;}
type Formatting = Omit<FormattingOptions, "lineBreak" | "commentChar"> & { lineBreak?: string; commentChar?: string;};
/** Options for parsing INI strings. */export interface ParseOptions { /** The character used to assign a value to a key; defaults to '='. */ assignment?: FormattingOptions["assignment"]; /** Provide custom parsing of the value in a key/value pair. */ reviver?: ReviverFunction;}
/** Options for constructing INI strings. */export interface StringifyOptions extends FormattingOptions { /** Provide custom string conversion for the value in a key/value pair. */ replacer?: ReplacerFunction;}
/** Function for replacing JavaScript values with INI string values. */export type ReplacerFunction = ( key: string, // deno-lint-ignore no-explicit-any value: any, section?: string,) => string;
/** Function for replacing INI values with JavaScript values. */export type ReviverFunction = ( key: string, // deno-lint-ignore no-explicit-any value: any, section?: string, // deno-lint-ignore no-explicit-any) => any;
/** Class implementation for fine control of INI data structures. */export class IniMap { #global = new Map<string, LineValue>(); #sections = new Map<string, LineSection>(); #lines: Line[] = []; #comments: Comments = { clear: (): void => { this.#lines = this.#lines.filter((line) => line.type !== "comment"); for (const [i, line] of this.#lines.entries()) { if (line.type === "section") { line.end = line.end - line.num + i + 1; } line.num = i + 1; } }, deleteAtLine: (line: number): boolean => { const comment = this.#getComment(line); if (comment) { this.#appendOrDeleteLine(comment, LineOp.Del); return true; } return false; }, deleteAtKey: (keyOrSection: string, noneOrKey?: string): boolean => { const lineValue = this.#getValue(keyOrSection, noneOrKey); if (lineValue) { return this.comments.deleteAtLine(lineValue.num - 1); } return false; }, deleteAtSection: (sectionName: string): boolean => { const section = this.#sections.get(sectionName); if (section) { return this.comments.deleteAtLine(section.num - 1); } return false; }, getAtLine: (line: number): string | undefined => { return this.#getComment(line)?.val; }, getAtKey: ( keyOrSection: string, noneOrKey?: string, ): string | undefined => { const lineValue = this.#getValue(keyOrSection, noneOrKey); if (lineValue) { return this.comments.getAtLine(lineValue.num - 1); } }, getAtSection: (sectionName: string): string | undefined => { const section = this.#sections.get(sectionName); if (section) { return this.comments.getAtLine(section.num - 1); } }, setAtLine: (line: number, text: string): Comments => { const comment = this.#getComment(line); const mark = this.#formatting.commentChar ?? "#"; const formatted = text.startsWith(mark) || text === "" ? text : `${mark} ${text}`; if (comment) { comment.val = formatted; } else { if (line > this.#lines.length) { for (let i = this.#lines.length + 1; i < line; i += 1) { this.#appendOrDeleteLine({ type: "comment", num: i, val: "", }, LineOp.Add); } } this.#appendOrDeleteLine({ type: "comment", num: line, val: formatted, }, LineOp.Add); } return this.comments; }, setAtKey: ( keyOrSection: string, textOrKey: string, noneOrText?: string, ): Comments => { if (noneOrText !== undefined) { const lineValue = this.#getValue(keyOrSection, textOrKey); if (lineValue) { if (this.#getComment(lineValue.num - 1)) { this.comments.setAtLine(lineValue.num - 1, noneOrText); } else { this.comments.setAtLine(lineValue.num, noneOrText); } } } else { const lineValue = this.#getValue(keyOrSection); if (lineValue) { if (this.#getComment(lineValue.num - 1)) { this.comments.setAtLine(lineValue.num - 1, textOrKey); } else { this.comments.setAtLine(lineValue.num, textOrKey); } } } return this.comments; }, setAtSection: (sectionName: string, text: string): Comments => { const section = this.#sections.get(sectionName); if (section) { if (this.#getComment(section.num - 1)) { this.comments.setAtLine(section.num - 1, text); } else { this.comments.setAtLine(section.num, text); } } return this.comments; }, }; #formatting: Formatting;
constructor(formatting?: FormattingOptions) { this.#formatting = this.#cleanFormatting(formatting); }
/** Get the count of key/value pairs. */ get size(): number { let size = this.#global.size; for (const { map } of this.#sections.values()) { size += map.size; } return size; }
get formatting(): Formatting { return this.#formatting; }
/** Manage comments in the INI. */ get comments(): Comments { return this.#comments; }
/** Clear a single section or the entire INI. */ clear(sectionName?: string): void { if (sectionName) { const section = this.#sections.get(sectionName);
if (section) { section.map.clear(); this.#sections.delete(sectionName); this.#lines.splice(section.num - 1, section.end - section.num); } } else { this.#global.clear(); this.#sections.clear(); this.#lines.length = 0; } }
/** Delete a global key in the INI. */ delete(key: string): boolean; /** Delete a section key in the INI. */ delete(section: string, key: string): boolean; delete(keyOrSection: string, noneOrKey?: string): boolean { const exists = this.#getValue(keyOrSection, noneOrKey); if (exists) { this.#appendOrDeleteLine(exists, LineOp.Del); if (exists.sec) { return this.#sections.get(exists.sec)!.map.delete(exists.key); } else { return this.#global.delete(exists.key); } }
return false; }
/** Get a value from a global key in the INI. */ get(key: string): unknown; /** Get a value from a section key in the INI. */ get(section: string, key: string): unknown; get(keyOrSection: string, noneOrKey?: string): unknown { return this.#getValue(keyOrSection, noneOrKey)?.val; }
/** Check if a global key exists in the INI. */ has(key: string): boolean; /** Check if a section key exists in the INI. */ has(section: string, key: string): boolean; has(keyOrSection: string, noneOrKey?: string): boolean { return this.#getValue(keyOrSection, noneOrKey) !== undefined; }
/** Set the value of a global key in the INI. */ // deno-lint-ignore no-explicit-any set(key: string, value: any): this; /** Set the value of a section key in the INI. */ // deno-lint-ignore no-explicit-any set(section: string, key: string, value: any): this; // deno-lint-ignore no-explicit-any set(keyOrSection: string, valueOrKey: any, value?: any): this { if (typeof valueOrKey === "string" && value !== undefined) { const section = this.#getOrCreateSection(keyOrSection); const exists = section.map.get(valueOrKey);
if (exists) { exists.val = value; } else { section.end += 1; const lineValue: LineValue = { type: "value", num: section.end, sec: section.sec, key: valueOrKey, val: value, }; this.#appendValue(lineValue); section.map.set(valueOrKey, lineValue); } } else { const lineValue: LineValue = { type: "value", num: 0, // Simply set to zero since we have to find the end ofthe global keys key: keyOrSection, val: valueOrKey, }; this.#appendValue(lineValue); this.#global.set(keyOrSection, lineValue); }
return this; }
/** Iterate over each entry in the INI to retrieve key, value, and section. */ *entries(): Generator< [key: string, value: unknown, section?: string | undefined] > { for (const { key, val } of this.#global.values()) { yield [key, val]; } for (const { map } of this.#sections.values()) { for (const { key, val, sec } of map.values()) { yield [key, val, sec]; } } }
#getOrCreateSection(section: string): LineSection { const existing = this.#sections.get(section);
if (existing) { return existing; }
const lineSection: LineSection = { type: "section", num: this.#lines.length + 1, sec: section, map: new Map<string, LineValue>(), end: this.#lines.length + 1, }; this.#lines.push(lineSection); this.#sections.set(section, lineSection); return lineSection; }
#getValue(keyOrSection: string, noneOrKey?: string): LineValue | undefined { if (noneOrKey) { const section = this.#sections.get(keyOrSection);
return section?.map.get(noneOrKey); }
return this.#global.get(keyOrSection); }
#getComment(line: number): LineComment | undefined { const comment: Line | undefined = this.#lines[line - 1]; if (comment?.type === "comment") { return comment; } }
#appendValue(lineValue: LineValue): void { if (this.#lines.length === 0) { // For an empty array, just insert the line value lineValue.num = 1; this.#lines.push(lineValue); } else if (lineValue.sec) { // For line values in a section, the end of the section is known this.#appendOrDeleteLine(lineValue, LineOp.Add); } else { // For global values, find the line preceding the first section lineValue.num = this.#lines.length + 1; for (const [i, line] of this.#lines.entries()) { if (line.type === "section") { lineValue.num = i + 1; break; } } // Append the line value at the end of all global values this.#appendOrDeleteLine(lineValue, LineOp.Add); } }
#appendOrDeleteLine(input: Line, op: LineOp) { if (op === LineOp.Add) { this.#lines.splice(input.num - 1, 0, input); } else { this.#lines.splice(input.num - 1, 1); } // If the input is a comment, find the next section if any to update. let updateSection = input.type === "comment"; const start = op === LineOp.Add ? input.num : input.num - 1; for (const line of this.#lines.slice(start)) { line.num += op; if (line.type === "section") { line.end += op; // If the comment is before the nearest section, don't update the section further. updateSection = false; } if (updateSection) { // if the comment precedes a value in a section, get and update the section end. if (line.type === "value" && line.sec) { const section = this.#sections.get(line.sec);
if (section) { section.end += op; updateSection = false; } } } } }
*#readTextLines(text: string): Generator<string> { const lineBreak = "\r\n"; const { length } = text; let lineBreakLength = -1; let line = "";
for (let i = 0; i < length; i += 1) { const char = text[i]!;
if (lineBreak.includes(char)) { yield line; line = ""; if (lineBreakLength === -1) { const ahead = text[i + 1]; if ( ahead !== undefined && ahead !== char && lineBreak.includes(ahead) ) { if (!this.#formatting.lineBreak) { this.#formatting.lineBreak = char + ahead; } lineBreakLength = 1; } else { if (!this.#formatting.lineBreak) { this.#formatting.lineBreak = char; } lineBreakLength = 0; } } i += lineBreakLength; } else { line += char; } }
yield line; }
#cleanFormatting(options?: FormattingOptions): FormattingOptions { return Object.fromEntries( Object.entries(options ?? {}).filter(([key]) => FormattingKeys.includes(key as keyof FormattingOptions) ), ); }
/** Convert this `IniMap` to a plain object. */ toObject(): Record<string, unknown | Record<string, unknown>> { const obj: Record<string, unknown | Record<string, unknown>> = {};
for (const { key, val } of this.#global.values()) { Object.defineProperty(obj, key, { value: val, writable: true, enumerable: true, configurable: true, }); } for (const { sec, map } of this.#sections.values()) { const section: Record<string, unknown> = {}; Object.defineProperty(obj, sec, { value: section, writable: true, enumerable: true, configurable: true, }); for (const { key, val } of map.values()) { Object.defineProperty(section, key, { value: val, writable: true, enumerable: true, configurable: true, }); } }
return obj; }
/** Convenience method for `JSON.stringify`. */ toJSON(): Record<string, unknown | Record<string, unknown>> { return this.toObject(); }
/** Convert this `IniMap` to an INI string. */ toString(replacer?: ReplacerFunction): string { const replacerFunc: ReplacerFunction = typeof replacer === "function" ? replacer : (_key, value, _section) => `${value}`; const pretty = this.#formatting?.pretty ?? false; const assignmentMark = (this.#formatting?.assignment ?? "=")[0]; const assignment = pretty ? ` ${assignmentMark} ` : assignmentMark; const lines = this.#formatting.deduplicate ? this.#lines.filter((lineA, index, self) => { if (lineA.type === "value") { const lastIndex = self.findLastIndex((lineB) => { return lineA.sec === (lineB as LineValue).sec && lineA.key === (lineB as LineValue).key; }); return index === lastIndex; } return true; }) : this.#lines;
return lines.map((line) => { switch (line.type) { case "comment": return line.val; case "section": return `[${line.sec}]`; case "value": return line.key + assignment + replacerFunc(line.key, line.val, line.sec); } }).join(this.#formatting?.lineBreak ?? "\n"); }
/** Parse an INI string in this `IniMap`. */ parse(text: string, reviver?: ReviverFunction): this { if (typeof text !== "string") { throw new SyntaxError(`Unexpected token ${text} in INI at line 0`); } const reviverFunc: ReviverFunction = typeof reviver === "function" ? reviver : (_key, value, _section) => value; const assignment = (this.#formatting.assignment ?? "=").substring(0, 1); let lineNumber = 1; let currentSection: LineSection | undefined;
for (const line of this.#readTextLines(text)) { const trimmed = line.trim(); if (isComment(trimmed)) { // If comment formatting mark is not set, discover it. if (!this.#formatting.commentChar) { const mark = trimmed[0]; if (mark) { // if mark is truthy, use the character. this.#formatting.commentChar = mark === "/" ? "//" : mark; } } this.#lines.push({ type: "comment", num: lineNumber, val: trimmed, }); } else if (isSection(trimmed, lineNumber)) { const sec = trimmed.substring(1, trimmed.length - 1);
if (sec.trim() === "") { throw new SyntaxError( `Unexpected empty section name at line ${lineNumber}`, ); }
currentSection = { type: "section", num: lineNumber, sec, map: new Map<string, LineValue>(), end: lineNumber, }; this.#lines.push(currentSection); this.#sections.set(currentSection.sec, currentSection); } else { const assignmentPos = trimmed.indexOf(assignment);
if (assignmentPos === -1) { throw new SyntaxError( `Unexpected token ${trimmed[0]} in INI at line ${lineNumber}`, ); } if (assignmentPos === 0) { throw new SyntaxError( `Unexpected empty key name at line ${lineNumber}`, ); }
const leftHand = trimmed.substring(0, assignmentPos); const rightHand = trimmed.substring(assignmentPos + 1);
if (this.#formatting.pretty === undefined) { this.#formatting.pretty = leftHand.endsWith(" ") && rightHand.startsWith(" "); }
const key = leftHand.trim(); const value = rightHand.trim();
if (currentSection) { const lineValue: LineValue = { type: "value", num: lineNumber, sec: currentSection.sec, key, val: reviverFunc(key, value, currentSection.sec), }; currentSection.map.set(key, lineValue); this.#lines.push(lineValue); currentSection.end = lineNumber; } else { const lineValue: LineValue = { type: "value", num: lineNumber, key, val: reviverFunc(key, value), }; this.#global.set(key, lineValue); this.#lines.push(lineValue); } }
lineNumber += 1; }
return this; }
/** Create an `IniMap` from an INI string. */ static from( input: string, options?: ParseOptions & FormattingOptions, ): IniMap; /** Create an `IniMap` from a plain object. */ static from( // deno-lint-ignore no-explicit-any input: Record<string, any>, formatting?: FormattingOptions, ): IniMap; static from( // deno-lint-ignore no-explicit-any input: Record<string, any> | string, formatting?: ParseOptions & FormattingOptions, ): IniMap { const ini = new IniMap(formatting); if (typeof input === "object" && input !== null) { // deno-lint-ignore no-explicit-any const isRecord = (val: any): val is Record<string, any> => typeof val === "object" && val !== null; // deno-lint-ignore no-explicit-any const sort = ([_a, valA]: [string, any], [_b, valB]: [string, any]) => { if (isRecord(valA)) return 1; if (isRecord(valB)) return -1; return 0; };
for (const [key, val] of Object.entries(input).sort(sort)) { if (isRecord(val)) { for (const [sectionKey, sectionValue] of Object.entries(val)) { ini.set(key, sectionKey, sectionValue); } } else { ini.set(key, val); } } } else { ini.parse(input, formatting?.reviver); } return ini; }}
export interface Comments { /** Clear all comments in the INI. */ clear(): void; /** Delete a comment at a specific line in the INI. */ deleteAtLine(line: number): boolean; /** Delete a comment before a global key in the INI. */ deleteAtKey(key: string): boolean; /** Delete a comment before a section key in the INI. */ deleteAtKey(section: string, key: string): boolean; /** Delete a comment before a section line in the INI. */ deleteAtSection(section: string): boolean; /** Get a comment at a specific line in the INI. */ getAtLine(line: number): string | undefined; /** Get a comment before a global key in the INI. */ getAtKey(key: string): string | undefined; /** Get a comment before a section key in the INI. */ getAtKey(section: string, key: string): string | undefined; /** Get a comment before a section line in the INI. */ getAtSection(section: string): string | undefined; /** Set a comment at a specific line in the INI. */ setAtLine(line: number, text: string): Comments; /** Set a comment before a global key in the INI. */ setAtKey(key: string, text: string): Comments; /** Set a comment before a section key in the INI. */ setAtKey(section: string, key: string, text: string): Comments; /** Set a comment before a section line in the INI. */ setAtSection(section: string, text: string): Comments;}
/** Detect supported comment styles. */function isComment(input: string): boolean { return input === "" || input.startsWith("#") || input.startsWith(";") || input.startsWith("//");}
/** Detect a section start. */function isSection(input: string, lineNumber: number): boolean { if (input.startsWith("[")) { if (input.endsWith("]")) { return true; } throw new SyntaxError( `Unexpected end of INI section at line ${lineNumber}`, ); } return false;}
type LineOp = typeof LineOp[keyof typeof LineOp];const LineOp = { Del: -1, Add: 1,} as const;const DummyFormatting: Required<FormattingOptions> = { assignment: "", lineBreak: "\n", pretty: false, commentChar: "#", deduplicate: false,};const FormattingKeys = Object.keys( DummyFormatting,) as (keyof FormattingOptions)[];
interface LineComment { type: "comment"; num: number; val: string;}
interface LineSection { type: "section"; num: number; sec: string; map: Map<string, LineValue>; end: number;}
interface LineValue { type: "value"; num: number; sec?: string; key: string; // deno-lint-ignore no-explicit-any val: any;}
type Line = LineComment | LineSection | LineValue;
std

Version Info

Tagged at
6 months ago