Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
4.3.0 - Unreleased
=====================

* Add opt-in [Temporal](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Temporal) support via `toml.parse(input, { useTemporal: true })`, mapping offset date-times to `Temporal.ZonedDateTime` and local date-times/dates/times to `Temporal.PlainDateTime`/`PlainDate`/`PlainTime`. An implementation can be supplied via the `temporal` option on runtimes without a `Temporal` global. ([#69](https://github.com/BinaryMuse/toml-node/issues/69))

4.2.0 - July 13 2026
=====================

Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,52 @@ typeof data.ld // "string"
typeof data.lt // "string"
```

#### Temporal Support

Pass `useTemporal: true` to have date/time values returned as
[Temporal](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Temporal)
objects instead:

| TOML type | Returned as |
| ---------------- | ------------------------- |
| Offset date-time | `Temporal.ZonedDateTime` |
| Local date-time | `Temporal.PlainDateTime` |
| Local date | `Temporal.PlainDate` |
| Local time | `Temporal.PlainTime` |

```javascript
const data = toml.parse(`
odt = 1979-05-27T00:32:00-07:00
ldt = 1979-05-27T07:32:00
ld = 1979-05-27
lt = 07:32:00
`, { useTemporal: true });

data.odt.toString() // "1979-05-27T00:32:00-07:00[-07:00]"
data.ldt.toString() // "1979-05-27T07:32:00"
data.ld.toString() // "1979-05-27"
data.lt.toString() // "07:32:00"
```

Offset date-times become `Temporal.ZonedDateTime` values whose time zone is
the original UTC offset (`Z` maps to the `UTC` time zone), so the offset
written in the TOML document is preserved — unlike the default `Date`
representation, which loses it. Fractional seconds beyond nanosecond
precision are truncated, as permitted by the TOML spec.

`useTemporal` requires a runtime with the `Temporal` global. On runtimes
that don't provide it yet, pass an implementation such as
[`@js-temporal/polyfill`](https://www.npmjs.com/package/@js-temporal/polyfill)
via the `temporal` option:

```javascript
const { Temporal } = require('@js-temporal/polyfill');
const data = toml.parse(someTomlString, { useTemporal: true, temporal: Temporal });
```

Once `Temporal` is broadly available, Temporal output is expected to become
the default behavior in a future major version.

### Special Float Values

`inf` and `nan` are returned as JavaScript `Infinity` and `NaN`:
Expand Down
32 changes: 32 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
declare module 'toml' {
/**
* A minimal structural subset of the `Temporal` global, used to supply an
* alternate implementation (e.g. from `@js-temporal/polyfill`) on runtimes
* without native Temporal support.
*/
export interface TemporalLike {
ZonedDateTime: { from(item: string): any };
PlainDateTime: { from(item: string): any };
PlainDate: { from(item: string): any };
PlainTime: { from(item: string): any };
}

export interface ParseOptions {
/**
* Maximum nesting depth for arrays and inline tables. Parsing input nested
* deeper than this throws a parse error rather than overflowing the stack.
* Defaults to 500.
*/
maxDepth?: number;

/**
* When true, date/time values are returned as Temporal objects instead of
* the default representations:
*
* - Offset date-time: `Temporal.ZonedDateTime` (instead of `Date`)
* - Local date-time: `Temporal.PlainDateTime` (instead of `string`)
* - Local date: `Temporal.PlainDate` (instead of `string`)
* - Local time: `Temporal.PlainTime` (instead of `string`)
*
* Requires a global `Temporal` object, or an implementation passed via
* the `temporal` option.
*/
useTemporal?: boolean;

/**
* The Temporal implementation to use when `useTemporal` is set. Defaults
* to the global `Temporal` object if one is available.
*/
temporal?: TemporalLike;
}

export function parse(input: string, options?: ParseOptions): any;
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module.exports = {
parse: function(input, options) {
var str = input.toString();
var nodes = parser.parse(str, options);
return compiler.compile(nodes, str);
return compiler.compile(nodes, str, options);
}
};
38 changes: 35 additions & 3 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
"use strict";
function compile(nodes, inputText) {
function compile(nodes, inputText, options) {
options = options || {};
var temporal = null;
if (options.useTemporal) {
temporal = options.temporal || (typeof Temporal !== "undefined" ? Temporal : null);
if (!temporal) {
throw new Error(
"The `useTemporal` option was set, but no Temporal implementation is available. " +
"Use a runtime with global `Temporal` support, or provide an implementation " +
"(e.g. from the `@js-temporal/polyfill` package) via the `temporal` option."
);
}
}

var assignedPaths = new Set();
var valueAssignments = new Set();
var explicitTablePaths = new Set();
Expand Down Expand Up @@ -93,9 +106,28 @@ function compile(nodes, inputText) {
return reduceArray(node.value);
} else if (node.type === "InlineTable") {
return reduceInlineTableNode(node.value);
} else {
return node.value;
} else if (temporal) {
switch (node.type) {
case "Date":
return temporal.ZonedDateTime.from(
truncateFractionalSeconds(node.raw) + node.tz +
"[" + (node.tz === "Z" ? "UTC" : node.tz) + "]"
);
case "LocalDateTime":
return temporal.PlainDateTime.from(truncateFractionalSeconds(node.value));
case "LocalDate":
return temporal.PlainDate.from(node.value);
case "LocalTime":
return temporal.PlainTime.from(truncateFractionalSeconds(node.value));
}
}
return node.value;
}

// TOML allows arbitrary fractional-second precision (implementations may
// truncate); Temporal rejects more than 9 digits (nanoseconds).
function truncateFractionalSeconds(str) {
return str.replace(/\.(\d{9})\d+/, ".$1");
}

function reduceInlineTableNode(values) {
Expand Down
2 changes: 1 addition & 1 deletion lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ function peg$parse(input, options) {
function peg$f60(t, frac) { return frac ? t + frac : t }
function peg$f61(t) { return t + ':00' }
function peg$f62() { return "Z" }
function peg$f63(d, t, o) { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); return node('Date', new Date(d + "T" + t + o), off) }
function peg$f63(d, t, o) { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); var n = node('Date', new Date(d + "T" + t + o), off); n.raw = d + "T" + t; n.tz = o; return n }
function peg$f64(d, t) { var off = offset(); validateDate(d, off); validateTime(t, off); return node('LocalDateTime', d + "T" + t, off) }
function peg$f65(d) { var off = offset(); validateDate(d, off); return node('LocalDate', d, off) }
function peg$f66(t) { var off = offset(); validateTime(t, off); return node('LocalTime', t, off) }
Expand Down
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"author": "Michelle Tilley <michelle@michelletilley.net>",
"license": "MIT",
"devDependencies": {
"@js-temporal/polyfill": "^0.5.1",
"peggy": "^5.1.0"
}
}
2 changes: 1 addition & 1 deletion src/toml.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ offset
/ $(sign:[+-] DIGIT DIGIT ':' DIGIT DIGIT)

datetime
= d:date_part datetime_delim t:time_part o:offset { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); return node('Date', new Date(d + "T" + t + o), off) }
= d:date_part datetime_delim t:time_part o:offset { var off = offset(); validateDate(d, off); validateTime(t, off); validateOffset(o, off); var n = node('Date', new Date(d + "T" + t + o), off); n.raw = d + "T" + t; n.tz = o; return n }
/ d:date_part datetime_delim t:time_part { var off = offset(); validateDate(d, off); validateTime(t, off); return node('LocalDateTime', d + "T" + t, off) }
/ d:date_part !datetime_delim { var off = offset(); validateDate(d, off); return node('LocalDate', d, off) }
/ t:time_part { var off = offset(); validateTime(t, off); return node('LocalTime', t, off) }
Expand Down
113 changes: 113 additions & 0 deletions test/test_toml.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,119 @@ describe("datetimes", function () {
});
});

describe("temporal support", function () {
var Temporal = require("@js-temporal/polyfill").Temporal;

function parseTemporal(str) {
return toml.parse(str, { useTemporal: true, temporal: Temporal });
}

it("parses offset date-times as Temporal.ZonedDateTime", function () {
var result = parseTemporal("a = 1979-05-27T07:32:00Z");
assert.ok(result.a instanceof Temporal.ZonedDateTime);
assert.strictEqual(result.a.toString(), "1979-05-27T07:32:00+00:00[UTC]");
assert.strictEqual(
result.a.epochMilliseconds,
new Date("1979-05-27T07:32:00Z").getTime()
);
});

it("preserves the offset of offset date-times", function () {
var result = parseTemporal(
"a = 1979-05-27T00:32:00-07:00\nb = 1979-05-27T07:32:00+02:00"
);
assert.strictEqual(result.a.toString(), "1979-05-27T00:32:00-07:00[-07:00]");
assert.strictEqual(result.b.toString(), "1979-05-27T07:32:00+02:00[+02:00]");
});

it("parses offset date-times with lowercase delimiters and space delimiter", function () {
var result = parseTemporal(
"a = 1979-05-27t07:32:00z\nb = 1979-05-27 07:32:00Z"
);
assert.strictEqual(result.a.toString(), "1979-05-27T07:32:00+00:00[UTC]");
assert.strictEqual(result.b.toString(), "1979-05-27T07:32:00+00:00[UTC]");
});

it("parses local date-times as Temporal.PlainDateTime", function () {
var result = parseTemporal("a = 1979-05-27T07:32:00\nb = 1979-05-27T00:32:00.999999");
assert.ok(result.a instanceof Temporal.PlainDateTime);
assert.strictEqual(result.a.toString(), "1979-05-27T07:32:00");
assert.strictEqual(result.b.toString(), "1979-05-27T00:32:00.999999");
});

it("parses local dates as Temporal.PlainDate", function () {
var result = parseTemporal("a = 1979-05-27");
assert.ok(result.a instanceof Temporal.PlainDate);
assert.strictEqual(result.a.toString(), "1979-05-27");
});

it("parses local times as Temporal.PlainTime", function () {
var result = parseTemporal("a = 07:32:00\nb = 00:32:00.999999");
assert.ok(result.a instanceof Temporal.PlainTime);
assert.strictEqual(result.a.toString(), "07:32:00");
assert.strictEqual(result.b.toString(), "00:32:00.999999");
});

it("parses times with omitted seconds", function () {
var result = parseTemporal("a = 07:32\nb = 1979-05-27T07:32");
assert.strictEqual(result.a.toString(), "07:32:00");
assert.strictEqual(result.b.toString(), "1979-05-27T07:32:00");
});

it("truncates fractional seconds beyond nanosecond precision", function () {
var result = parseTemporal(
"a = 07:32:00.123456789999\n" +
"b = 1979-05-27T07:32:00.123456789999\n" +
"c = 1979-05-27T07:32:00.123456789999-07:00"
);
assert.strictEqual(result.a.toString(), "07:32:00.123456789");
assert.strictEqual(result.b.toString(), "1979-05-27T07:32:00.123456789");
assert.strictEqual(
result.c.toString(),
"1979-05-27T07:32:00.123456789-07:00[-07:00]"
);
});

it("converts date-times inside arrays and inline tables", function () {
var result = parseTemporal(
"arr = [ 1979-05-27, 07:32:00 ]\ninline = { when = 1979-05-27T07:32:00Z }"
);
assert.ok(result.arr[0] instanceof Temporal.PlainDate);
assert.ok(result.arr[1] instanceof Temporal.PlainTime);
assert.ok(result.inline.when instanceof Temporal.ZonedDateTime);
});

it("does not affect parsing when useTemporal is not set", function () {
var result = toml.parse("a = 1979-05-27T07:32:00Z\nb = 1979-05-27\nc = 07:32:00");
assert.ok(result.a instanceof Date);
assert.strictEqual(result.b, "1979-05-27");
assert.strictEqual(result.c, "07:32:00");
});

it("uses the global Temporal object when no implementation is passed", function () {
var hadGlobal = "Temporal" in globalThis;
var previous = globalThis.Temporal;
globalThis.Temporal = Temporal;
try {
var result = toml.parse("a = 1979-05-27", { useTemporal: true });
assert.ok(result.a instanceof Temporal.PlainDate);
} finally {
if (hadGlobal) globalThis.Temporal = previous;
else delete globalThis.Temporal;
}
});

it(
"throws a helpful error when no Temporal implementation is available",
{ skip: typeof globalThis.Temporal !== "undefined" },
function () {
assert.throws(function () {
toml.parse("a = 1979-05-27", { useTemporal: true });
}, /no Temporal implementation is available/);
}
);
});

describe("quoted keys", function () {
it("simple quoted key", function () {
parsesToml('["ʞ"]\na = 1', { ʞ: { a: 1 } });
Expand Down
Loading