@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 +21 -0
- package/README.md +140 -0
- package/Toml.js +165 -0
- package/TomlDateTime.js +147 -0
- package/TomlDiagnostic.js +106 -0
- package/TomlDocument.js +133 -0
- package/TomlEdit.js +52 -0
- package/TomlFormat.js +611 -0
- package/TomlNode.js +212 -0
- package/TomlVisitor.js +152 -0
- package/index.d.ts +897 -0
- package/index.js +10 -0
- package/internal/diagnostics.js +53 -0
- package/internal/limits.js +24 -0
- package/internal/parser.js +408 -0
- package/internal/scanner.js +493 -0
- package/internal/semantic.js +253 -0
- package/internal/stringifyValue.js +191 -0
- package/package.json +45 -0
- package/tsdoc-metadata.json +11 -0
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
|
+
[](https://www.npmjs.com/package/@effected/toml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](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 };
|
package/TomlDateTime.js
ADDED
|
@@ -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 };
|