@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @effected/toml
2
+
3
+ [![npm](https://img.shields.io/npm/v/@effected%2Ftoml?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/toml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ Zero-dependency TOML 1.0.0 parsing, editing and formatting expressed as Effect schemas and pure functions. Parse TOML into plain values or a byte-exact linear CST, compute comment-preserving edits, format, modify by path, walk a document as a `Stream`, and decode straight into a validated domain schema.
9
+
10
+ > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
+ > development against a single pinned Effect v4 beta. Packages graduate to
12
+ > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
+ > exactly the ones the kit is built and tested against, install
14
+ > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
+ >
16
+ > **Stability: unstable.** This package's API surface is not yet considered
17
+ > complete and may change across `0.x` releases. Pin an exact version — even a
18
+ > package marked *stable* before `1.0.0` can introduce a breaking change by
19
+ > accident, and an exact pin turns that into a type-check error rather than a
20
+ > runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
21
+
22
+ ## Why @effected/toml
23
+
24
+ TOML is the config format behind `Cargo.toml`, `pyproject.toml` and a large share of the Rust and Python toolchains — files that people maintain by hand and comment heavily. Most JavaScript TOML libraries round-trip through a plain-object model, which loses every comment and blank line the moment you write the file back out.
25
+
26
+ The engine here is written from scratch against the TOML 1.0.0 spec, and it is the only format package in this repo that vendors no upstream code at all. It parses into a lossless linear CST whose expression spans tile the source byte-exact, so `TomlDocument.parse(text).stringify()` reproduces the original text exactly for any valid document. There is no separate re-serialization path that can drift from the source. Edits are byte-minimal splices against that CST, so formatting and path-based modification preserve every comment and every byte you did not touch.
27
+
28
+ The value model is honest about TOML's types rather than flattening them into JavaScript's. Integers past ±(2^53 − 1) decode to `bigint` instead of silently losing precision, and TOML's four date-time types decode to four calendar-validated value classes instead of a `Date` that cannot represent a local time. TOML has no null, so `Toml.stringify` on a value containing `null` fails with a structured `UnsupportedValue` diagnostic naming the offending path rather than dropping the key. Every fallible entry point carries a typed error built from `TomlDiagnostic`, and nesting-depth guards on both the parse and the stringify side mean hostile input fails through that channel rather than as a stack overflow.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install @effected/toml effect
34
+ ```
35
+
36
+ ```bash
37
+ pnpm add @effected/toml effect
38
+ ```
39
+
40
+ Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package itself adds no other runtime dependencies.
41
+
42
+ ## Quick start
43
+
44
+ Compose your schema with `Toml.schema` to decode TOML straight into a validated domain value, or reach for the pre-bound `Toml.TomlFromString` codec when you just want the plain value:
45
+
46
+ ```ts
47
+ import { Toml } from "@effected/toml";
48
+ import { Effect, Schema } from "effect";
49
+
50
+ const Config = Schema.Struct({ name: Schema.String, port: Schema.Number });
51
+ const ConfigFromToml = Toml.schema(Config);
52
+
53
+ const program = Effect.gen(function* () {
54
+ return yield* Schema.decodeUnknownEffect(ConfigFromToml)(`
55
+ name = "api"
56
+ port = 3000
57
+ `);
58
+ });
59
+
60
+ Effect.runPromise(program).then(console.log);
61
+ // { name: "api", port: 3000 }
62
+ ```
63
+
64
+ `Toml.stringify` goes the other way, emitting canonical TOML:
65
+
66
+ ```ts
67
+ import { Toml } from "@effected/toml";
68
+ import { Effect } from "effect";
69
+
70
+ Effect.runPromise(Toml.stringify({ title: "app", server: { port: 8080 } })).then(console.log);
71
+ // title = "app"
72
+ //
73
+ // [server]
74
+ // port = 8080
75
+ ```
76
+
77
+ ## Editing without losing comments
78
+
79
+ `TomlFormat.modify` computes a `TomlEdit` array against the parsed CST; `modifyToString` applies it in one step. Comments, blank lines and layout that an edit does not cover come through byte-identical:
80
+
81
+ ```ts
82
+ import { TomlFormat } from "@effected/toml";
83
+ import { Effect } from "effect";
84
+
85
+ const source = `# server config
86
+ name = "api"
87
+ port = 3000 # dev default
88
+ `;
89
+
90
+ Effect.runPromise(TomlFormat.modifyToString(source, ["port"], 8080)).then(console.log);
91
+ // # server config
92
+ // name = "api"
93
+ // port = 8080 # dev default
94
+ ```
95
+
96
+ ## Value model
97
+
98
+ TOML's types do not all have a JavaScript equivalent, so the ones that do not get a real one:
99
+
100
+ | TOML | Decodes to |
101
+ | ---- | ---------- |
102
+ | Integer within ±(2^53 − 1) | `number` |
103
+ | Integer beyond ±(2^53 − 1), inside the 64-bit range | `bigint` — precision is never silently lost |
104
+ | Integer outside the 64-bit range | fails with an `IntegerOutOfRange` diagnostic |
105
+ | Offset date-time (`1979-05-27T07:32:00Z`) | `TomlOffsetDateTime` |
106
+ | Local date-time (`1979-05-27T07:32:00`) | `TomlLocalDateTime` |
107
+ | Local date (`1979-05-27`) | `TomlLocalDate` |
108
+ | Local time (`07:32:00`) | `TomlLocalTime` |
109
+
110
+ The four date-time classes are `Schema.Class` value objects with real Gregorian-calendar validation and structural equality, not `Date` subclasses: JavaScript's `Date` has no way to represent a date with no time, or a time with no offset.
111
+
112
+ TOML cannot represent `null` at all. Rather than dropping the key or writing an empty string, `Toml.stringify` fails:
113
+
114
+ ```ts
115
+ import { Toml } from "@effected/toml";
116
+ import { Effect } from "effect";
117
+
118
+ Effect.runPromise(Effect.result(Toml.stringify({ a: null }))).then(console.log);
119
+ // Failure with TomlStringifyError, whose `diagnostic` carries:
120
+ // { code: "UnsupportedValue", message: "unsupported null value at a", ... }
121
+ ```
122
+
123
+ ## Features
124
+
125
+ - `Toml.parse` / `Toml.stringify` — value-level parse and canonical stringify, both carrying typed `TomlParseError` / `TomlStringifyError` channels, including nesting-depth guards on adversarial input.
126
+ - `Toml.fromString` / `Toml.TomlFromString` / `Toml.schema` — string→domain schema factories that decode TOML directly into a validated Effect `Schema` value.
127
+ - `TomlDocument` — the lossless document: `parse`, `schema`, `toValue` and `stringify`, backed by the linear CST whose expression spans reconstruct the source byte-exact.
128
+ - `TomlEdit` / `TomlRange` (with `applyAll`) — the non-mutating text-edit vocabulary shared by the formatter and the modifier, and identical in shape to `@effected/jsonc`'s and `@effected/yaml`'s.
129
+ - `TomlFormat` — `format` and `formatToString` compute conservative, comment-preserving formatting edits; `modify` and `modifyToString` compute the edits to replace, delete or insert a value at a path.
130
+ - `TomlVisitor` — walk a parsed document as a `Stream` of visitor events.
131
+ - `TomlDiagnostic` — the structured diagnostic (`code`, `message`, `offset`, `length`, `line`, `character`) every typed error carries, across five staged error-code unions.
132
+ - `TomlLocalDate` / `TomlLocalTime` / `TomlLocalDateTime` / `TomlOffsetDateTime` — TOML's date-time types as calendar-validated `Schema.Class` value objects.
133
+
134
+ ## Conformance
135
+
136
+ The engine runs the [toml-test](https://github.com/toml-lang/toml-test) 1.0.0 compliance corpus — every valid case and every invalid case — to a 100% pass rate with no skip list, and a differential property suite cross-checks it against an independent TOML parser. Both oracles are devDependency-only; neither reaches your runtime.
137
+
138
+ ## License
139
+
140
+ [MIT](LICENSE)
package/Toml.js ADDED
@@ -0,0 +1,165 @@
1
+ import { isRawTomlError } from "./internal/diagnostics.js";
2
+ import { isGuardExceeded } from "./internal/limits.js";
3
+ import { parseExpressions } from "./internal/parser.js";
4
+ import { buildValue } from "./internal/semantic.js";
5
+ import { stringifyValue } from "./internal/stringifyValue.js";
6
+ import { TomlDiagnostic } from "./TomlDiagnostic.js";
7
+ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
+
9
+ //#region src/Toml.ts
10
+ /**
11
+ * Options controlling stringify behavior. The only knob is `newline` —
12
+ * omitted, it resolves to `"\n"`.
13
+ *
14
+ * @public
15
+ */
16
+ var TomlStringifyOptions = class extends Schema.Class("TomlStringifyOptions")({ newline: Schema.optionalKey(Schema.Literals(["\n", "\r\n"])) }) {};
17
+ /**
18
+ * Parse failure: the {@link TomlDiagnostic} entries describing why the
19
+ * document was rejected (first violation wins, so there is one today; the
20
+ * array shape matches `@effected/yaml`'s aggregate contract). Raised by
21
+ * {@link Toml.parse} and the decode direction of the schema factories.
22
+ *
23
+ * @public
24
+ */
25
+ var TomlParseError = class extends Schema.TaggedErrorClass()("TomlParseError", { diagnostics: Schema.Array(TomlDiagnostic) }) {
26
+ get message() {
27
+ const count = this.diagnostics.length;
28
+ const first = this.diagnostics[0];
29
+ const detail = first === void 0 ? "" : `: ${first.code} at ${first.line}:${first.character} ${first.message}`;
30
+ return `TOML parse failed with ${count} error${count === 1 ? "" : "s"}${detail}`;
31
+ }
32
+ };
33
+ /**
34
+ * Stringification failure: an unsupported value, an out-of-range integer, a
35
+ * circular reference or a tripped depth guard, as one structured
36
+ * {@link TomlDiagnostic} (offset `0` — there is no source text). Raised by
37
+ * {@link Toml.stringify} and the encode direction of the schema factories.
38
+ *
39
+ * @public
40
+ */
41
+ var TomlStringifyError = class extends Schema.TaggedErrorClass()("TomlStringifyError", { diagnostic: TomlDiagnostic }) {
42
+ get message() {
43
+ return `TOML stringify failed: ${this.diagnostic.code} ${this.diagnostic.message}`;
44
+ }
45
+ };
46
+ /**
47
+ * Run the parser and semantic pass, materializing the engine's raw carriers
48
+ * into the typed error: `RawTomlError` becomes a positioned diagnostic and a
49
+ * `GuardExceeded` depth trip becomes a `NestingDepthExceeded` diagnostic
50
+ * (never an unhandled defect). Anything else is a genuine defect and rethrows.
51
+ */
52
+ const parseOrFail = (text) => Effect.try({
53
+ try: () => buildValue(parseExpressions(text)),
54
+ catch: (defect) => {
55
+ if (isRawTomlError(defect)) return new TomlParseError({ diagnostics: [TomlDiagnostic.fromRaw(text, defect.diagnostic)] });
56
+ if (isGuardExceeded(defect)) return new TomlParseError({ diagnostics: [TomlDiagnostic.fromRaw(text, {
57
+ code: "NestingDepthExceeded",
58
+ message: defect.message,
59
+ offset: defect.offset,
60
+ length: 0
61
+ })] });
62
+ throw defect;
63
+ }
64
+ });
65
+ const stringifyOrFail = (value, options) => Effect.try({
66
+ try: () => stringifyValue(value, options?.newline ?? "\n"),
67
+ catch: (defect) => {
68
+ if (isRawTomlError(defect)) return new TomlStringifyError({ diagnostic: TomlDiagnostic.fromRaw("", defect.diagnostic) });
69
+ if (isGuardExceeded(defect)) return new TomlStringifyError({ diagnostic: TomlDiagnostic.fromRaw("", {
70
+ code: "NestingDepthExceeded",
71
+ message: defect.message,
72
+ offset: 0,
73
+ length: 0
74
+ }) });
75
+ throw defect;
76
+ }
77
+ });
78
+ /**
79
+ * Static entry points for TOML parsing, stringification and the schema
80
+ * factories. Not instantiable.
81
+ *
82
+ * @remarks
83
+ * `parse`, `stringify` and the schema factories carry real typed error
84
+ * channels — including the hardening guards (nesting-depth caps on both
85
+ * sides, circular-reference detection on encode) that keep malformed or
86
+ * adversarial input on the typed channel instead of surfacing as an
87
+ * unhandled defect. `parse` takes no options: TOML 1.0.0 parsing has no
88
+ * knobs.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * import { Toml } from "@effected/toml";
93
+ * import { Effect } from "effect";
94
+ *
95
+ * const program = Effect.gen(function* () {
96
+ * const value = yield* Toml.parse('name = "Alice"\nage = 30');
97
+ * return value; // { name: "Alice", age: 30 }
98
+ * });
99
+ * ```
100
+ *
101
+ * @public
102
+ */
103
+ var Toml = class Toml {
104
+ constructor() {}
105
+ /**
106
+ * Parse a TOML 1.0.0 document into a plain JavaScript value: tables and
107
+ * inline tables become plain objects (`__proto__` lands as an own data
108
+ * property), arrays become plain arrays, integers become `number` (or
109
+ * `bigint` past 2^53) and date-times become the four `TomlDateTime`
110
+ * classes. Fails with {@link TomlParseError} at the first violation;
111
+ * returns `unknown`, never `any`.
112
+ *
113
+ * A nesting-depth bomb (arrays or inline tables past the engine cap) also
114
+ * fails through {@link TomlParseError} with a `NestingDepthExceeded`
115
+ * diagnostic, never as an unhandled defect.
116
+ */
117
+ static parse = Effect.fn("Toml.parse")(function* (text) {
118
+ return yield* parseOrFail(text);
119
+ });
120
+ /**
121
+ * Stringify a plain JavaScript value as a canonical TOML document: within
122
+ * a table, non-table pairs first, then sub-tables as `[dotted.header]`
123
+ * sections depth-first, then arrays of tables as `[[dotted.header]]`
124
+ * sections, a blank line before every header except at document start.
125
+ * Fails with {@link TomlStringifyError} on unsupported values (TOML has no
126
+ * null), out-of-int64-range `bigint`s, circular references and
127
+ * depth-guard trips — all on the typed channel.
128
+ */
129
+ static stringify = Effect.fn("Toml.stringify")(function* (value, options) {
130
+ return yield* stringifyOrFail(value, options);
131
+ });
132
+ /**
133
+ * A `Schema<unknown, string>` decoding a TOML document and encoding values
134
+ * back to canonical TOML text.
135
+ *
136
+ * Schema-producing: each call returns a fresh schema whose derivation
137
+ * caches are not shared across calls. Bind the result to a `const` on hot
138
+ * paths; the pre-bound {@link Toml.TomlFromString} covers the common case.
139
+ */
140
+ static fromString() {
141
+ return Schema.String.pipe(Schema.decodeTo(Schema.Unknown, SchemaTransformation.transformOrFail({
142
+ decode: (input) => Toml.parse(input).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
143
+ encode: (value) => stringifyOrFail(value).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(value), { message: error.message })))
144
+ })));
145
+ }
146
+ /**
147
+ * The zero-config `Schema<unknown, string>` — `Toml.fromString()`
148
+ * pre-bound so the common case needs no memoization discipline.
149
+ */
150
+ static TomlFromString = Toml.fromString();
151
+ /**
152
+ * Compose {@link Toml.fromString} with a target schema, yielding a
153
+ * `Schema<A, string>` that decodes TOML straight into a validated domain
154
+ * value. The target's decoding/encoding service requirements flow through.
155
+ *
156
+ * Schema-producing: bind the result to a `const` on hot paths (see
157
+ * {@link Toml.fromString}).
158
+ */
159
+ static schema(target) {
160
+ return Toml.TomlFromString.pipe(Schema.decodeTo(target));
161
+ }
162
+ };
163
+
164
+ //#endregion
165
+ export { Toml, TomlParseError, TomlStringifyError, TomlStringifyOptions };
@@ -0,0 +1,147 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/TomlDateTime.ts
4
+ /** Zero-pad `value` to `width` digits (never truncates a wider value). */
5
+ function pad(value, width) {
6
+ return String(value).padStart(width, "0");
7
+ }
8
+ /** Whether `year` is a Gregorian leap year (div-4, except centuries unless div-400). */
9
+ function isLeapYear(year) {
10
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
11
+ }
12
+ const MONTH_LENGTHS = [
13
+ 31,
14
+ 28,
15
+ 31,
16
+ 30,
17
+ 31,
18
+ 30,
19
+ 31,
20
+ 31,
21
+ 30,
22
+ 31,
23
+ 30,
24
+ 31
25
+ ];
26
+ /** The number of days in `month` (1-12) of `year`, accounting for leap years. */
27
+ function daysInMonth(year, month) {
28
+ if (month === 2 && isLeapYear(year)) return 29;
29
+ return MONTH_LENGTHS[month - 1] ?? 31;
30
+ }
31
+ /** Class-level filter shared by every class carrying a `{ year, month, day }` triple. */
32
+ const isRealCalendarDate = Schema.makeFilter(({ year, month, day }) => {
33
+ const max = daysInMonth(year, month);
34
+ return day <= max || `day ${day} does not exist in ${pad(year, 4)}-${pad(month, 2)} (month has ${max} days)`;
35
+ }, { title: "a real calendar date" });
36
+ const dateFields = {
37
+ year: Schema.Int.check(Schema.isBetween({
38
+ minimum: 0,
39
+ maximum: 9999
40
+ })),
41
+ month: Schema.Int.check(Schema.isBetween({
42
+ minimum: 1,
43
+ maximum: 12
44
+ })),
45
+ day: Schema.Int.check(Schema.isBetween({
46
+ minimum: 1,
47
+ maximum: 31
48
+ }))
49
+ };
50
+ const timeFields = {
51
+ hour: Schema.Int.check(Schema.isBetween({
52
+ minimum: 0,
53
+ maximum: 23
54
+ })),
55
+ minute: Schema.Int.check(Schema.isBetween({
56
+ minimum: 0,
57
+ maximum: 59
58
+ })),
59
+ second: Schema.Int.check(Schema.isBetween({
60
+ minimum: 0,
61
+ maximum: 60
62
+ })),
63
+ nanosecond: Schema.Int.check(Schema.isBetween({
64
+ minimum: 0,
65
+ maximum: 999999999
66
+ }))
67
+ };
68
+ /** `YYYY-MM-DD`. */
69
+ function formatDate(date) {
70
+ return `${pad(date.year, 4)}-${pad(date.month, 2)}-${pad(date.day, 2)}`;
71
+ }
72
+ /**
73
+ * `HH:MM:SS[.fraction]` — the fractional part is present only when
74
+ * `nanosecond > 0`, trimmed to the shortest exact representation.
75
+ */
76
+ function formatTime(time) {
77
+ const base = `${pad(time.hour, 2)}:${pad(time.minute, 2)}:${pad(time.second, 2)}`;
78
+ if (time.nanosecond === 0) return base;
79
+ return `${base}.${pad(time.nanosecond, 9).replace(/0+$/, "")}`;
80
+ }
81
+ /** `Z` for a zero offset, else `+hh:mm` / `-hh:mm`. */
82
+ function formatOffset(offsetMinutes) {
83
+ if (offsetMinutes === 0) return "Z";
84
+ const sign = offsetMinutes < 0 ? "-" : "+";
85
+ const magnitude = Math.abs(offsetMinutes);
86
+ const hh = Math.trunc(magnitude / 60);
87
+ const mm = magnitude % 60;
88
+ return `${sign}${pad(hh, 2)}:${pad(mm, 2)}`;
89
+ }
90
+ /**
91
+ * A TOML local date: `year`-`month`-`day` with no time-of-day or offset,
92
+ * validated against the real Gregorian calendar.
93
+ *
94
+ * @public
95
+ */
96
+ var TomlLocalDate = class extends Schema.Class("TomlLocalDate")(Schema.Struct(dateFields).check(isRealCalendarDate)) {
97
+ toString() {
98
+ return formatDate(this);
99
+ }
100
+ };
101
+ /**
102
+ * A TOML local time: `hour`:`minute`:`second`[.`nanosecond`] with no date or
103
+ * offset. `second` tolerates the RFC 3339 leap second (0-60).
104
+ *
105
+ * @public
106
+ */
107
+ var TomlLocalTime = class extends Schema.Class("TomlLocalTime")(timeFields) {
108
+ toString() {
109
+ return formatTime(this);
110
+ }
111
+ };
112
+ /**
113
+ * A TOML local date-time: a {@link TomlLocalDate} and a {@link TomlLocalTime}
114
+ * combined, with no offset.
115
+ *
116
+ * @public
117
+ */
118
+ var TomlLocalDateTime = class extends Schema.Class("TomlLocalDateTime")(Schema.Struct({
119
+ ...dateFields,
120
+ ...timeFields
121
+ }).check(isRealCalendarDate)) {
122
+ toString() {
123
+ return `${formatDate(this)}T${formatTime(this)}`;
124
+ }
125
+ };
126
+ /**
127
+ * A TOML offset date-time: a {@link TomlLocalDateTime} plus `offsetMinutes`
128
+ * (-1439-1439). Parsing enforces `hh <= 23` / `mm <= 59` before construction;
129
+ * this class only bounds the combined minute count.
130
+ *
131
+ * @public
132
+ */
133
+ var TomlOffsetDateTime = class extends Schema.Class("TomlOffsetDateTime")(Schema.Struct({
134
+ ...dateFields,
135
+ ...timeFields,
136
+ offsetMinutes: Schema.Int.check(Schema.isBetween({
137
+ minimum: -1439,
138
+ maximum: 1439
139
+ }))
140
+ }).check(isRealCalendarDate)) {
141
+ toString() {
142
+ return `${formatDate(this)}T${formatTime(this)}${formatOffset(this.offsetMinutes)}`;
143
+ }
144
+ };
145
+
146
+ //#endregion
147
+ export { TomlLocalDate, TomlLocalDateTime, TomlLocalTime, TomlOffsetDateTime };
@@ -0,0 +1,106 @@
1
+ import { TOML_LEX_ERROR_CODES, TOML_PARSE_ERROR_CODES, TOML_SEMANTIC_ERROR_CODES, TOML_STRINGIFY_ERROR_CODES } from "./internal/diagnostics.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/TomlDiagnostic.ts
5
+ /**
6
+ * Error codes emitted by the lexer stage.
7
+ *
8
+ * @public
9
+ */
10
+ const TomlLexErrorCode = Schema.Literals(TOML_LEX_ERROR_CODES);
11
+ /**
12
+ * Error codes emitted by the parser stage.
13
+ *
14
+ * @public
15
+ */
16
+ const TomlParseErrorCode = Schema.Literals(TOML_PARSE_ERROR_CODES);
17
+ /**
18
+ * Error codes emitted by the semantic (table/key conflict) stage.
19
+ *
20
+ * @public
21
+ */
22
+ const TomlSemanticErrorCode = Schema.Literals(TOML_SEMANTIC_ERROR_CODES);
23
+ /**
24
+ * Error codes emitted by the stringifier stage.
25
+ *
26
+ * @public
27
+ */
28
+ const TomlStringifyErrorCode = Schema.Literals(TOML_STRINGIFY_ERROR_CODES);
29
+ /**
30
+ * Union of all TOML error codes across all pipeline stages. Stage
31
+ * discrimination lives here (in the code), not in separate error classes.
32
+ *
33
+ * @public
34
+ */
35
+ const TomlErrorCode = Schema.Union([
36
+ TomlLexErrorCode,
37
+ TomlParseErrorCode,
38
+ TomlSemanticErrorCode,
39
+ TomlStringifyErrorCode
40
+ ]);
41
+ /**
42
+ * One structured diagnostic: its {@link (TomlErrorCode:type)}, a
43
+ * human-readable `message`, and its exact position (`offset`/`length`, plus
44
+ * zero-based `line`/`character`).
45
+ *
46
+ * @remarks
47
+ * The five-field positional core (`code`/`offset`/`length`/`line`/`character`)
48
+ * is structurally identical to `@effected/jsonc`'s parse-error detail shape
49
+ * and `@effected/yaml`'s `YamlDiagnostic`; `message` is this package's
50
+ * additive extra.
51
+ *
52
+ * @public
53
+ */
54
+ var TomlDiagnostic = class TomlDiagnostic extends Schema.Class("TomlDiagnostic")({
55
+ code: TomlErrorCode,
56
+ message: Schema.String,
57
+ offset: Schema.Number,
58
+ length: Schema.Number,
59
+ line: Schema.Number,
60
+ character: Schema.Number
61
+ }) {
62
+ /**
63
+ * Materialize an engine record, deriving `line`/`character` (0-based)
64
+ * from `offset` against the source `text`. Advanced — the parse/stringify
65
+ * entry points call this for you.
66
+ */
67
+ static fromRaw(source, raw) {
68
+ const { line, character } = lineChar(source, raw.offset);
69
+ return TomlDiagnostic.make({
70
+ code: raw.code,
71
+ message: raw.message,
72
+ offset: raw.offset,
73
+ length: raw.length,
74
+ line,
75
+ character
76
+ });
77
+ }
78
+ };
79
+ /**
80
+ * Compute the zero-based line/character position of `offset` within `text`.
81
+ * Recognizes `\n`, `\r` and `\r\n` as line breaks (TOML's newline grammar);
82
+ * a CRLF pair counts as a single newline.
83
+ */
84
+ function lineChar(text, offset) {
85
+ let line = 0;
86
+ let lineStart = 0;
87
+ const limit = Math.min(offset, text.length);
88
+ for (let i = 0; i < limit; i++) {
89
+ const ch = text.charCodeAt(i);
90
+ if (ch === 10) {
91
+ line++;
92
+ lineStart = i + 1;
93
+ } else if (ch === 13) {
94
+ if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) i++;
95
+ line++;
96
+ lineStart = i + 1;
97
+ }
98
+ }
99
+ return {
100
+ line,
101
+ character: offset - lineStart
102
+ };
103
+ }
104
+
105
+ //#endregion
106
+ export { TomlDiagnostic, TomlErrorCode, TomlLexErrorCode, TomlParseErrorCode, TomlSemanticErrorCode, TomlStringifyErrorCode };