@effected/toml 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/TomlFormat.js ADDED
@@ -0,0 +1,611 @@
1
+ import { isRawTomlError } from "./internal/diagnostics.js";
2
+ import { isGuardExceeded } from "./internal/limits.js";
3
+ import { TomlArray, TomlArrayTableHeader, TomlInlineTable, TomlKeyValue, TomlString, TomlTableHeader, TomlTrivia } from "./TomlNode.js";
4
+ import { parseExpressions } from "./internal/parser.js";
5
+ import { analyze } from "./internal/semantic.js";
6
+ import { renderInlineValue, renderKey } from "./internal/stringifyValue.js";
7
+ import { TomlDiagnostic } from "./TomlDiagnostic.js";
8
+ import { TomlDocument } from "./TomlDocument.js";
9
+ import { TomlEdit } from "./TomlEdit.js";
10
+ import { Effect, Schema } from "effect";
11
+
12
+ //#region src/TomlFormat.ts
13
+ /**
14
+ * Options controlling formatting and modification behavior. The only knob is
15
+ * `newline`: for `format` it normalizes every newline outside multi-line
16
+ * strings; for `modify` it overrides the dominant newline inherited by
17
+ * inserted lines.
18
+ *
19
+ * @public
20
+ */
21
+ var TomlFormattingOptions = class extends Schema.Class("TomlFormattingOptions")({ newline: Schema.optionalKey(Schema.Literals(["\n", "\r\n"])) }) {};
22
+ /**
23
+ * Raised when `TomlFormat.modify` cannot resolve the requested path against
24
+ * the document's semantic view, when the insertion target refuses (an inline
25
+ * table or an implicitly created table), or when the replacement value
26
+ * cannot render as TOML. Carries one structured {@link TomlDiagnostic} —
27
+ * never a collapsed `reason` string.
28
+ *
29
+ * @public
30
+ */
31
+ var TomlModificationError = class extends Schema.TaggedErrorClass()("TomlModificationError", { diagnostic: TomlDiagnostic }) {
32
+ get message() {
33
+ return `TOML modification failed: ${this.diagnostic.code} ${this.diagnostic.message}`;
34
+ }
35
+ };
36
+ const TAB = 9;
37
+ const LF = 10;
38
+ const CR = 13;
39
+ const SPACE = 32;
40
+ const BANG = 33;
41
+ const HASH = 35;
42
+ const BOM = 65279;
43
+ const isWs = (code) => code === SPACE || code === TAB;
44
+ const scanWs = (source, pos, end) => {
45
+ let i = pos;
46
+ while (i < end && isWs(source.charCodeAt(i))) i++;
47
+ return i;
48
+ };
49
+ const scanWsBack = (source, pos, start) => {
50
+ let i = pos;
51
+ while (i > start && isWs(source.charCodeAt(i - 1))) i--;
52
+ return i;
53
+ };
54
+ /** The document's dominant newline: CRLF only when CRLF pairs outnumber bare LFs. */
55
+ const dominantNewline = (source) => {
56
+ let lf = 0;
57
+ let crlf = 0;
58
+ for (let i = 0; i < source.length; i++) if (source.charCodeAt(i) === LF) if (i > 0 && source.charCodeAt(i - 1) === CR) crlf++;
59
+ else lf++;
60
+ return crlf > lf ? "\r\n" : "\n";
61
+ };
62
+ /** Multi-line string value spans — the bytes formatting must never touch. */
63
+ const collectMultilineSpans = (node, out) => {
64
+ if (node instanceof TomlString) {
65
+ if (node.style === "multiline-basic" || node.style === "multiline-literal") out.push([node.offset, node.offset + node.length]);
66
+ return;
67
+ }
68
+ if (node instanceof TomlArray) {
69
+ for (const item of node.items) collectMultilineSpans(item, out);
70
+ return;
71
+ }
72
+ if (node instanceof TomlInlineTable) for (const entry of node.entries) collectMultilineSpans(entry.value, out);
73
+ };
74
+ /** Accumulates format edits for one document; drops no-op splices to keep format idempotent. */
75
+ var FormatEmitter = class {
76
+ source;
77
+ edits = [];
78
+ constructor(source) {
79
+ this.source = source;
80
+ }
81
+ push(expr, offset, length, newText) {
82
+ if (this.source.slice(offset, offset + length) !== newText) this.edits.push({
83
+ offset,
84
+ length,
85
+ newText,
86
+ exprOffset: expr.offset,
87
+ exprEnd: expr.offset + expr.length
88
+ });
89
+ }
90
+ };
91
+ /** Rule 2: strip the expression's leading indentation (the first line's BOM excluded). */
92
+ const formatLeading = (source, emit, expr) => {
93
+ let start = expr.offset;
94
+ if (start === 0 && source.charCodeAt(0) === BOM) start = 1;
95
+ const end = scanWs(source, start, expr.offset + expr.length);
96
+ if (end > start) emit.push(expr, start, end - start, "");
97
+ };
98
+ /** Rule 4 (space after `#`) and the rule-3 trailing-whitespace strip, for one comment body. */
99
+ const formatCommentBody = (source, emit, expr, bodyStart, lineEnd) => {
100
+ const bodyEnd = scanWsBack(source, lineEnd, bodyStart);
101
+ if (bodyEnd < lineEnd) emit.push(expr, bodyEnd, lineEnd - bodyEnd, "");
102
+ if (bodyEnd > bodyStart) {
103
+ const code = source.charCodeAt(bodyStart);
104
+ if (code !== SPACE && code !== TAB && code !== BANG) emit.push(expr, bodyStart, 0, " ");
105
+ }
106
+ };
107
+ /** Rule 3: the run between an expression's content end and its newline — trailing ws and the comment gap. */
108
+ const formatTail = (source, emit, expr, contentEnd) => {
109
+ let nlStart = expr.offset + expr.length;
110
+ if (nlStart > contentEnd && source.charCodeAt(nlStart - 1) === LF) {
111
+ nlStart -= 1;
112
+ if (nlStart > contentEnd && source.charCodeAt(nlStart - 1) === CR) nlStart -= 1;
113
+ }
114
+ const hashIdx = scanWs(source, contentEnd, nlStart);
115
+ if (hashIdx >= nlStart || source.charCodeAt(hashIdx) !== HASH) {
116
+ if (nlStart > contentEnd) emit.push(expr, contentEnd, nlStart - contentEnd, "");
117
+ return;
118
+ }
119
+ emit.push(expr, contentEnd, hashIdx - contentEnd, " ");
120
+ formatCommentBody(source, emit, expr, hashIdx + 1, nlStart);
121
+ };
122
+ /** Rules 2–4 over a trivia run's blank and comment-only lines. */
123
+ const formatTrivia = (source, emit, expr) => {
124
+ const end = expr.offset + expr.length;
125
+ let i = expr.offset;
126
+ while (i < end) {
127
+ const lineStart = i;
128
+ let j = i;
129
+ while (j < end && source.charCodeAt(j) !== LF && source.charCodeAt(j) !== CR) j++;
130
+ let start = lineStart;
131
+ if (start === 0 && source.charCodeAt(0) === BOM) start = 1;
132
+ const wsEnd = scanWs(source, start, j);
133
+ if (wsEnd >= j) {
134
+ if (j > start) emit.push(expr, start, j - start, "");
135
+ } else {
136
+ if (wsEnd > start) emit.push(expr, start, wsEnd - start, "");
137
+ formatCommentBody(source, emit, expr, wsEnd + 1, j);
138
+ }
139
+ if (j < end && source.charCodeAt(j) === CR && j + 1 < end && source.charCodeAt(j + 1) === LF) j += 2;
140
+ else if (j < end) j += 1;
141
+ i = j;
142
+ }
143
+ };
144
+ /** Rule 6: normalize every newline within the expression span, skipping the protected string spans. */
145
+ const normalizeNewlines = (source, emit, expr, protectedSpans, target) => {
146
+ const end = expr.offset + expr.length;
147
+ let p = 0;
148
+ let i = expr.offset;
149
+ while (i < end) {
150
+ while (p < protectedSpans.length && protectedSpans[p][1] <= i) p++;
151
+ if (p < protectedSpans.length && i >= protectedSpans[p][0]) {
152
+ i = protectedSpans[p][1];
153
+ continue;
154
+ }
155
+ const code = source.charCodeAt(i);
156
+ if (code === CR && i + 1 < end && source.charCodeAt(i + 1) === LF) {
157
+ if (target === "\n") emit.push(expr, i, 2, "\n");
158
+ i += 2;
159
+ } else if (code === LF) {
160
+ if (target === "\r\n") emit.push(expr, i, 1, "\r\n");
161
+ i += 1;
162
+ } else i += 1;
163
+ }
164
+ };
165
+ /** The position just past a header's closing bracket(s). */
166
+ const headerContentEnd = (source, expr) => {
167
+ const lastKey = expr.keyPath[expr.keyPath.length - 1];
168
+ return scanWs(source, lastKey.offset + lastKey.length, expr.offset + expr.length) + (expr instanceof TomlArrayTableHeader ? 2 : 1);
169
+ };
170
+ /** All six format rules over the expression list; `[]` on malformed input (never corrupt it). */
171
+ const computeFormatEdits = (source, options) => {
172
+ let expressions;
173
+ try {
174
+ expressions = parseExpressions(source);
175
+ } catch {
176
+ return [];
177
+ }
178
+ const emit = new FormatEmitter(source);
179
+ const target = options?.newline;
180
+ for (const expr of expressions) {
181
+ let protectedSpans = [];
182
+ if (expr instanceof TomlTrivia) formatTrivia(source, emit, expr);
183
+ else if (expr instanceof TomlKeyValue) {
184
+ formatLeading(source, emit, expr);
185
+ const lastKey = expr.keyPath[expr.keyPath.length - 1];
186
+ const keyEnd = lastKey.offset + lastKey.length;
187
+ emit.push(expr, keyEnd, expr.value.offset - keyEnd, " = ");
188
+ formatTail(source, emit, expr, expr.value.offset + expr.value.length);
189
+ if (target !== void 0) {
190
+ const spans = [];
191
+ collectMultilineSpans(expr.value, spans);
192
+ protectedSpans = spans;
193
+ }
194
+ } else {
195
+ formatLeading(source, emit, expr);
196
+ formatTail(source, emit, expr, headerContentEnd(source, expr));
197
+ }
198
+ if (target !== void 0) normalizeNewlines(source, emit, expr, protectedSpans, target);
199
+ }
200
+ if (expressions.length > 0 && source.charCodeAt(source.length - 1) !== LF) {
201
+ const last = expressions[expressions.length - 1];
202
+ emit.push(last, source.length, 0, target ?? dominantNewline(source));
203
+ }
204
+ return emit.edits;
205
+ };
206
+ const mkTable = (origin, sectionIndex = 0, relPath = []) => ({
207
+ kind: "table",
208
+ origin,
209
+ entries: /* @__PURE__ */ new Map(),
210
+ sectionIndex,
211
+ relPath
212
+ });
213
+ /** Descend into a navigable node: tables pass through, array-of-tables yield their last element. */
214
+ const intoTable = (node) => {
215
+ if (node !== void 0 && node.kind === "array-tables") return node.elements[node.elements.length - 1];
216
+ if (node !== void 0 && node.kind === "table") return node;
217
+ throw new Error("invariant: semantic pass admitted a value where a table was navigated");
218
+ };
219
+ /**
220
+ * Build the resolution tree and section list by riding `analyze`'s visitor.
221
+ * The expressions were validated at parse time, so the walk cannot throw;
222
+ * `onKeyValue` paths are name-only, so array-of-tables element association
223
+ * comes from descending into the LAST element — expressions arrive in
224
+ * document order, so the last element is always the current one.
225
+ */
226
+ const buildSemanticIndex = (expressions) => {
227
+ const sections = [{
228
+ header: void 0,
229
+ insertAfter: void 0
230
+ }];
231
+ for (const expr of expressions) if (expr instanceof TomlTableHeader || expr instanceof TomlArrayTableHeader) sections.push({
232
+ header: expr,
233
+ insertAfter: expr.offset + expr.length
234
+ });
235
+ else if (!(expr instanceof TomlTrivia)) sections[sections.length - 1].insertAfter = expr.offset + expr.length;
236
+ const root = mkTable("root");
237
+ let sectionCounter = 0;
238
+ const navigateHeaderPrefix = (path) => {
239
+ let current = root;
240
+ for (let i = 0; i < path.length - 1; i++) {
241
+ let child = current.entries.get(path[i]);
242
+ if (child === void 0) {
243
+ child = mkTable("implicit");
244
+ current.entries.set(path[i], child);
245
+ }
246
+ current = intoTable(child);
247
+ }
248
+ return current;
249
+ };
250
+ analyze(expressions, {
251
+ onTableStart: (path, header) => {
252
+ if (header === void 0) return;
253
+ sectionCounter += 1;
254
+ const parent = navigateHeaderPrefix(path);
255
+ const name = path[path.length - 1];
256
+ const existing = parent.entries.get(name);
257
+ if (existing !== void 0 && existing.kind === "table") {
258
+ existing.origin = "explicit";
259
+ existing.sectionIndex = sectionCounter;
260
+ } else parent.entries.set(name, mkTable("explicit", sectionCounter));
261
+ },
262
+ onArrayTableStart: (path, _index, _header) => {
263
+ sectionCounter += 1;
264
+ const parent = navigateHeaderPrefix(path);
265
+ const name = path[path.length - 1];
266
+ const existing = parent.entries.get(name);
267
+ if (existing !== void 0 && existing.kind === "array-tables") existing.elements.push(mkTable("element", sectionCounter));
268
+ else parent.entries.set(name, {
269
+ kind: "array-tables",
270
+ elements: [mkTable("element", sectionCounter)]
271
+ });
272
+ },
273
+ onKeyValue: (path, expr) => {
274
+ const names = expr.keyPath.map((key) => key.value);
275
+ let current = root;
276
+ for (let i = 0; i < path.length - names.length; i++) current = intoTable(current.entries.get(path[i]));
277
+ for (let j = 0; j < names.length - 1; j++) {
278
+ let child = current.entries.get(names[j]);
279
+ if (child === void 0) {
280
+ child = mkTable("dotted", sectionCounter, names.slice(0, j + 1));
281
+ current.entries.set(names[j], child);
282
+ }
283
+ current = intoTable(child);
284
+ }
285
+ current.entries.set(names[names.length - 1], {
286
+ kind: "value",
287
+ node: expr.value,
288
+ expr
289
+ });
290
+ }
291
+ });
292
+ return {
293
+ root,
294
+ sections
295
+ };
296
+ };
297
+ /** Thrown by the pure resolution helpers; `modify` materializes {@link TomlModificationError}. */
298
+ var ModifyFailure = class extends Error {
299
+ code;
300
+ offset;
301
+ len;
302
+ constructor(code, message, offset, len) {
303
+ super(message);
304
+ this.code = code;
305
+ this.offset = offset;
306
+ this.len = len;
307
+ this.name = "ModifyFailure";
308
+ }
309
+ };
310
+ const failResolve = (code, message, offset = 0, length = 0) => {
311
+ throw new ModifyFailure(code, message, offset, length);
312
+ };
313
+ const requireIndex = (segment, what, offset, length) => {
314
+ if (typeof segment !== "number" || !Number.isInteger(segment) || segment < 0) return failResolve("DottedKeyConflict", `${what} requires a non-negative integer index, received "${String(segment)}"`, offset, length);
315
+ return segment;
316
+ };
317
+ /** Wrap a CST value as a cursor; inline tables open as an entry scope (dotted keys included). */
318
+ const cstCursor = (node, del) => node instanceof TomlInlineTable ? {
319
+ t: "inline",
320
+ table: node,
321
+ candidates: node.entries.map((entry, index) => ({
322
+ entry,
323
+ index
324
+ })),
325
+ depth: 0
326
+ } : {
327
+ t: "cst",
328
+ node,
329
+ del
330
+ };
331
+ const stepInline = (cur, key) => {
332
+ const matches = cur.candidates.filter((candidate) => candidate.entry.keyPath[cur.depth].value === key);
333
+ return {
334
+ matches,
335
+ full: matches.length === 1 && matches[0].entry.keyPath.length === cur.depth + 1 ? matches[0] : void 0
336
+ };
337
+ };
338
+ /** One navigation step (never the terminal segment). */
339
+ const step = (cur, segment) => {
340
+ if (cur.t === "table") {
341
+ const key = String(segment);
342
+ const child = cur.table.entries.get(key);
343
+ if (child === void 0) return failResolve("DottedKeyConflict", `key "${key}" does not resolve — intermediate tables are never auto-created`);
344
+ if (child.kind === "table") return {
345
+ t: "table",
346
+ table: child
347
+ };
348
+ if (child.kind === "array-tables") return {
349
+ t: "array-tables",
350
+ node: child
351
+ };
352
+ return cstCursor(child.node, {
353
+ kind: "expression",
354
+ expr: child.expr
355
+ });
356
+ }
357
+ if (cur.t === "array-tables") {
358
+ const idx = requireIndex(segment, "an array of tables", 0, 0);
359
+ if (idx >= cur.node.elements.length) return failResolve("DottedKeyConflict", `array-of-tables index ${idx} is out of bounds`);
360
+ return {
361
+ t: "table",
362
+ table: cur.node.elements[idx]
363
+ };
364
+ }
365
+ if (cur.t === "inline") {
366
+ const key = String(segment);
367
+ const { matches, full } = stepInline(cur, key);
368
+ if (matches.length === 0) return failResolve("DottedKeyConflict", `key "${key}" does not resolve in the inline table`, cur.table.offset, cur.table.length);
369
+ if (full !== void 0) return cstCursor(full.entry.value, {
370
+ kind: "inline-entry",
371
+ table: cur.table,
372
+ index: full.index
373
+ });
374
+ return {
375
+ t: "inline",
376
+ table: cur.table,
377
+ candidates: matches,
378
+ depth: cur.depth + 1
379
+ };
380
+ }
381
+ const node = cur.node;
382
+ if (node instanceof TomlArray) {
383
+ const idx = requireIndex(segment, "an array", node.offset, node.length);
384
+ if (idx >= node.items.length) return failResolve("DottedKeyConflict", `array index ${idx} is out of bounds`, node.offset, node.length);
385
+ return cstCursor(node.items[idx], {
386
+ kind: "array-item",
387
+ array: node,
388
+ index: idx
389
+ });
390
+ }
391
+ return failResolve("DottedKeyConflict", "cannot navigate through a scalar value", node.offset, node.length);
392
+ };
393
+ /** Delete one array item, splicing the separator: leading comma for a last item, trailing for the rest. */
394
+ const spliceArrayItem = (array, index) => {
395
+ const items = array.items;
396
+ if (items.length === 1) return {
397
+ offset: array.offset + 1,
398
+ length: array.length - 2,
399
+ newText: ""
400
+ };
401
+ if (index < items.length - 1) return {
402
+ offset: items[index].offset,
403
+ length: items[index + 1].offset - items[index].offset,
404
+ newText: ""
405
+ };
406
+ const prevEnd = items[index - 1].offset + items[index - 1].length;
407
+ return {
408
+ offset: prevEnd,
409
+ length: items[index].offset + items[index].length - prevEnd,
410
+ newText: ""
411
+ };
412
+ };
413
+ /** Delete one inline-table entry with the same separator-splicing rule. */
414
+ const spliceInlineEntry = (table, index) => {
415
+ const entries = table.entries;
416
+ if (entries.length === 1) return {
417
+ offset: table.offset + 1,
418
+ length: table.length - 2,
419
+ newText: ""
420
+ };
421
+ if (index < entries.length - 1) return {
422
+ offset: entries[index].offset,
423
+ length: entries[index + 1].offset - entries[index].offset,
424
+ newText: ""
425
+ };
426
+ const prevEnd = entries[index - 1].offset + entries[index - 1].length;
427
+ return {
428
+ offset: prevEnd,
429
+ length: entries[index].offset + entries[index].length - prevEnd,
430
+ newText: ""
431
+ };
432
+ };
433
+ /** The pinned insertion-placement rules: where a new `key = value` line lands and how its key renders. */
434
+ const insertEdit = (table, key, value, ctx) => {
435
+ if (table.origin === "implicit") return failResolve("DottedKeyConflict", "the table was created implicitly by a longer header and has no section of its own to insert into");
436
+ const line = `${(table.origin === "dotted" ? [...table.relPath, key] : [key]).map(renderKey).join(".")} = ${renderInlineValue(value)}`;
437
+ let offset;
438
+ if (table.sectionIndex === 0) {
439
+ const firstHeader = ctx.sections.length > 1 ? ctx.sections[1].header : void 0;
440
+ offset = ctx.sections[0].insertAfter ?? firstHeader?.offset ?? 0;
441
+ } else offset = ctx.sections[table.sectionIndex].insertAfter ?? 0;
442
+ const needLeading = offset > 0 && ctx.source.charCodeAt(offset - 1) !== LF;
443
+ return {
444
+ offset,
445
+ length: 0,
446
+ newText: `${needLeading ? ctx.nl : ""}${line}${ctx.nl}`
447
+ };
448
+ };
449
+ /** Resolve the final segment against the parent cursor and produce the terminal edits. */
450
+ const terminal = (cur, segment, value, ctx) => {
451
+ if (cur.t === "table") {
452
+ const key = String(segment);
453
+ const existing = cur.table.entries.get(key);
454
+ if (value === void 0) {
455
+ if (existing === void 0) return [];
456
+ if (existing.kind !== "value") return failResolve("DottedKeyConflict", `"${key}" is a table section, not a deletable value`);
457
+ return [{
458
+ offset: existing.expr.offset,
459
+ length: existing.expr.length,
460
+ newText: ""
461
+ }];
462
+ }
463
+ if (existing === void 0) return [insertEdit(cur.table, key, value, ctx)];
464
+ if (existing.kind !== "value") return failResolve("DottedKeyConflict", `"${key}" is already defined as a table and cannot become a value`);
465
+ return [{
466
+ offset: existing.node.offset,
467
+ length: existing.node.length,
468
+ newText: renderInlineValue(value)
469
+ }];
470
+ }
471
+ if (cur.t === "array-tables") {
472
+ const idx = requireIndex(segment, "an array of tables", 0, 0);
473
+ if (value === void 0 && idx >= cur.node.elements.length) return [];
474
+ return failResolve("DottedKeyConflict", idx < cur.node.elements.length ? `array-of-tables element ${idx} is a table section, not an addressable value` : `array-of-tables index ${idx} is out of bounds`);
475
+ }
476
+ if (cur.t === "inline") {
477
+ const key = String(segment);
478
+ const { matches, full } = stepInline(cur, key);
479
+ if (value === void 0) {
480
+ if (matches.length === 0) return [];
481
+ if (full === void 0) return failResolve("DottedKeyConflict", `"${key}" is a dotted-key group inside an inline table, not a single entry`, cur.table.offset, cur.table.length);
482
+ return [spliceInlineEntry(cur.table, full.index)];
483
+ }
484
+ if (matches.length === 0) return failResolve("InlineTableExtended", `inline table cannot be extended with new key "${key}"`, cur.table.offset, cur.table.length);
485
+ if (full === void 0) return failResolve("DottedKeyConflict", `"${key}" is a dotted-key group inside an inline table, not a single entry`, cur.table.offset, cur.table.length);
486
+ return [{
487
+ offset: full.entry.value.offset,
488
+ length: full.entry.value.length,
489
+ newText: renderInlineValue(value)
490
+ }];
491
+ }
492
+ const node = cur.node;
493
+ if (node instanceof TomlArray) {
494
+ const idx = requireIndex(segment, "an array", node.offset, node.length);
495
+ if (value === void 0) {
496
+ if (idx >= node.items.length) return [];
497
+ return [spliceArrayItem(node, idx)];
498
+ }
499
+ if (idx >= node.items.length) return failResolve("DottedKeyConflict", `array index ${idx} is out of bounds — modify never appends array elements`, node.offset, node.length);
500
+ return [{
501
+ offset: node.items[idx].offset,
502
+ length: node.items[idx].length,
503
+ newText: renderInlineValue(value)
504
+ }];
505
+ }
506
+ return failResolve("DottedKeyConflict", "cannot address a key beneath a scalar value", node.offset, node.length);
507
+ };
508
+ /**
509
+ * Formatting and modification statics. Not instantiable.
510
+ *
511
+ * @remarks
512
+ * `format`/`formatToString` are pure and total: malformed input yields no
513
+ * edits rather than corrupting the document, and every edit derives from an
514
+ * expression or value span, so bytes inside multi-line strings are
515
+ * untouchable by construction. `modify`/`modifyToString` carry a real error
516
+ * channel — {@link TomlParseError} when the source does not parse and
517
+ * {@link TomlModificationError} for path-resolution and insertion-target
518
+ * failures — and every document they produce reparses cleanly.
519
+ *
520
+ * @public
521
+ */
522
+ var TomlFormat = class TomlFormat {
523
+ constructor() {}
524
+ /**
525
+ * Compute conservative formatting edits: one space around `=`, leading
526
+ * indentation stripped, trailing whitespace stripped with one space before
527
+ * a trailing `#`, one space after a non-empty `#` (unless it starts with
528
+ * space, tab or `!`), a single final newline, and — when
529
+ * `options.newline` is set — every newline normalized outside multi-line
530
+ * strings. Nothing else: no reordering, no blank-line collapsing, no value
531
+ * rewriting. `range` restricts edits to the expressions intersecting it.
532
+ * Non-mutating — apply with `TomlEdit.applyAll` (or use
533
+ * {@link TomlFormat.formatToString}).
534
+ */
535
+ static format(text, range, options) {
536
+ const tagged = computeFormatEdits(text, options);
537
+ return (range === void 0 ? tagged : tagged.filter((edit) => Math.max(edit.exprOffset, range.offset) <= Math.min(edit.exprEnd, range.offset + range.length))).map((edit) => TomlEdit.make({
538
+ offset: edit.offset,
539
+ length: edit.length,
540
+ content: edit.newText
541
+ }));
542
+ }
543
+ /**
544
+ * Format `text` and apply the resulting edits in one step
545
+ * (`TomlEdit.applyAll ∘ format`). Pure and total.
546
+ */
547
+ static formatToString(text, range, options) {
548
+ return TomlEdit.applyAll(text, TomlFormat.format(text, range, options));
549
+ }
550
+ /**
551
+ * Compute the edits that replace, delete, or insert a value at `path`,
552
+ * resolved through the document's semantic view. Every segment but the
553
+ * last must resolve — intermediate tables are never auto-created. A
554
+ * `value` of `undefined` deletes: a key-value's whole line, an inline
555
+ * entry, or an array item, splicing separators. A new key inserts per the
556
+ * pinned placement rules: root keys land after the last root expression
557
+ * (before the first header), section keys after the section's last
558
+ * expression, dotted-table keys as dotted keys appended to the defining
559
+ * section; inline and implicitly created tables refuse. Inserted lines
560
+ * inherit the document's dominant newline unless `options.newline`
561
+ * overrides it. Every modified document reparses cleanly.
562
+ */
563
+ static modify = Effect.fn("TomlFormat.modify")(function* (text, path, value, options) {
564
+ const failWith = (code, message, offset = 0, length = 0) => new TomlModificationError({ diagnostic: TomlDiagnostic.fromRaw(text, {
565
+ code,
566
+ message,
567
+ offset,
568
+ length
569
+ }) });
570
+ if (path.length === 0) return yield* failWith("DottedKeyConflict", "an empty path does not address a value");
571
+ if (path.length > 256) return yield* failWith("NestingDepthExceeded", `path depth ${path.length} exceeds the ${256} cap`);
572
+ const doc = yield* TomlDocument.parse(text);
573
+ if (doc.diagnostics.length > 0) return yield* new TomlModificationError({ diagnostic: doc.diagnostics[0] });
574
+ const { root, sections } = buildSemanticIndex(doc.expressions);
575
+ const ctx = {
576
+ source: text,
577
+ sections,
578
+ nl: options?.newline ?? dominantNewline(text)
579
+ };
580
+ let raw;
581
+ try {
582
+ let cursor = {
583
+ t: "table",
584
+ table: root
585
+ };
586
+ for (let i = 0; i < path.length - 1; i++) cursor = step(cursor, path[i]);
587
+ raw = terminal(cursor, path[path.length - 1], value, ctx);
588
+ } catch (defect) {
589
+ if (defect instanceof ModifyFailure) return yield* failWith(defect.code, defect.message, defect.offset, defect.len);
590
+ if (isRawTomlError(defect)) return yield* new TomlModificationError({ diagnostic: TomlDiagnostic.fromRaw(text, defect.diagnostic) });
591
+ if (isGuardExceeded(defect)) return yield* failWith("NestingDepthExceeded", defect.message, defect.offset);
592
+ throw defect;
593
+ }
594
+ return raw.map((edit) => TomlEdit.make({
595
+ offset: edit.offset,
596
+ length: edit.length,
597
+ content: edit.newText
598
+ }));
599
+ });
600
+ /**
601
+ * Modify `text` and apply the resulting edits in one step
602
+ * (`TomlEdit.applyAll ∘ modify`).
603
+ */
604
+ static modifyToString = Effect.fn("TomlFormat.modifyToString")(function* (text, path, value, options) {
605
+ const edits = yield* TomlFormat.modify(text, path, value, options);
606
+ return TomlEdit.applyAll(text, edits);
607
+ });
608
+ };
609
+
610
+ //#endregion
611
+ export { TomlFormat, TomlFormattingOptions, TomlModificationError };