@fedify/vocab-runtime 2.0.35 → 2.0.36-dev.2551

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.0.35",
3
+ "version": "2.0.36-dev.2551+8b012df4",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
package/dist/mod.cjs CHANGED
@@ -4502,7 +4502,7 @@ const preloadedContexts = {
4502
4502
  //#endregion
4503
4503
  //#region deno.json
4504
4504
  var name = "@fedify/vocab-runtime";
4505
- var version = "2.0.35";
4505
+ var version = "2.0.36-dev.2551+8b012df4";
4506
4506
  //#endregion
4507
4507
  //#region src/request.ts
4508
4508
  /**
package/dist/mod.js CHANGED
@@ -4498,7 +4498,7 @@ const preloadedContexts = {
4498
4498
  //#endregion
4499
4499
  //#region deno.json
4500
4500
  var name = "@fedify/vocab-runtime";
4501
- var version = "2.0.35";
4501
+ var version = "2.0.36-dev.2551+8b012df4";
4502
4502
  //#endregion
4503
4503
  //#region src/request.ts
4504
4504
  /**
package/dist/temporal.cjs CHANGED
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  //#region src/temporal.ts
4
4
  /**
5
- * Type guards for `Temporal` namespace objects.
5
+ * Type guards for `Temporal` namespace objects and normalization of
6
+ * `xsd:dateTime` literals.
6
7
  *
7
8
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
8
9
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -55,6 +56,23 @@ function isTemporalInstant(value) {
55
56
  function isTemporalDuration(value) {
56
57
  return typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Temporal.Duration]" && "sign" in value && (value.sign === -1 || value.sign === 0 || value.sign === 1) && "toString" in value && typeof value.toString === "function" && value.toString !== Object.prototype.toString;
57
58
  }
59
+ /**
60
+ * Normalizes an `xsd:dateTime` literal into a form that
61
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
62
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
63
+ * 2.0 allows, are filled in.
64
+ *
65
+ * @param value The date-time literal to normalize.
66
+ * @returns The normalized literal.
67
+ */
68
+ function normalizeDateTimeLiteral(value) {
69
+ const timeStart = value.search(/[Tt ]/) + 1;
70
+ if (timeStart < 1) return value;
71
+ const date = value.substring(0, timeStart);
72
+ const time = value.substring(timeStart).replace(/^(\d{2}:\d{2})(?=\.)/, "$1:00");
73
+ return date + (/[Zz+-]/.test(time) ? time : time + "Z");
74
+ }
58
75
  //#endregion
59
76
  exports.isTemporalDuration = isTemporalDuration;
60
77
  exports.isTemporalInstant = isTemporalInstant;
78
+ exports.normalizeDateTimeLiteral = normalizeDateTimeLiteral;
@@ -1,7 +1,8 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  //#region src/temporal.d.ts
3
3
  /**
4
- * Type guards for `Temporal` namespace objects.
4
+ * Type guards for `Temporal` namespace objects and normalization of
5
+ * `xsd:dateTime` literals.
5
6
  *
6
7
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
7
8
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -50,5 +51,15 @@ declare function isTemporalInstant(value: unknown): value is Temporal.Instant;
50
51
  * and overrides `toString`; `false` otherwise.
51
52
  */
52
53
  declare function isTemporalDuration(value: unknown): value is Temporal.Duration;
54
+ /**
55
+ * Normalizes an `xsd:dateTime` literal into a form that
56
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
57
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
58
+ * 2.0 allows, are filled in.
59
+ *
60
+ * @param value The date-time literal to normalize.
61
+ * @returns The normalized literal.
62
+ */
63
+ declare function normalizeDateTimeLiteral(value: string): string;
53
64
  //#endregion
54
- export { isTemporalDuration, isTemporalInstant };
65
+ export { isTemporalDuration, isTemporalInstant, normalizeDateTimeLiteral };
@@ -1,7 +1,8 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  //#region src/temporal.d.ts
3
3
  /**
4
- * Type guards for `Temporal` namespace objects.
4
+ * Type guards for `Temporal` namespace objects and normalization of
5
+ * `xsd:dateTime` literals.
5
6
  *
6
7
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
7
8
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -50,5 +51,15 @@ declare function isTemporalInstant(value: unknown): value is Temporal.Instant;
50
51
  * and overrides `toString`; `false` otherwise.
51
52
  */
52
53
  declare function isTemporalDuration(value: unknown): value is Temporal.Duration;
54
+ /**
55
+ * Normalizes an `xsd:dateTime` literal into a form that
56
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
57
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
58
+ * 2.0 allows, are filled in.
59
+ *
60
+ * @param value The date-time literal to normalize.
61
+ * @returns The normalized literal.
62
+ */
63
+ declare function normalizeDateTimeLiteral(value: string): string;
53
64
  //#endregion
54
- export { isTemporalDuration, isTemporalInstant };
65
+ export { isTemporalDuration, isTemporalInstant, normalizeDateTimeLiteral };
package/dist/temporal.js CHANGED
@@ -1,7 +1,8 @@
1
1
 
2
2
  //#region src/temporal.ts
3
3
  /**
4
- * Type guards for `Temporal` namespace objects.
4
+ * Type guards for `Temporal` namespace objects and normalization of
5
+ * `xsd:dateTime` literals.
5
6
  *
6
7
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
7
8
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -54,5 +55,21 @@ function isTemporalInstant(value) {
54
55
  function isTemporalDuration(value) {
55
56
  return typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Temporal.Duration]" && "sign" in value && (value.sign === -1 || value.sign === 0 || value.sign === 1) && "toString" in value && typeof value.toString === "function" && value.toString !== Object.prototype.toString;
56
57
  }
58
+ /**
59
+ * Normalizes an `xsd:dateTime` literal into a form that
60
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
61
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
62
+ * 2.0 allows, are filled in.
63
+ *
64
+ * @param value The date-time literal to normalize.
65
+ * @returns The normalized literal.
66
+ */
67
+ function normalizeDateTimeLiteral(value) {
68
+ const timeStart = value.search(/[Tt ]/) + 1;
69
+ if (timeStart < 1) return value;
70
+ const date = value.substring(0, timeStart);
71
+ const time = value.substring(timeStart).replace(/^(\d{2}:\d{2})(?=\.)/, "$1:00");
72
+ return date + (/[Zz+-]/.test(time) ? time : time + "Z");
73
+ }
57
74
  //#endregion
58
- export { isTemporalDuration, isTemporalInstant };
75
+ export { isTemporalDuration, isTemporalInstant, normalizeDateTimeLiteral };
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-B7Iyskql.cjs");
2
+ const require_request = require("./request-BNAT7gZ3.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
4
  //#region src/body.ts
5
5
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,4 +1,4 @@
1
- import { t as FetchError } from "./request-pGToUa6Q.mjs";
1
+ import { t as FetchError } from "./request-BiuryHCs.mjs";
2
2
  import { getLogger } from "@logtape/logtape";
3
3
  //#region src/body.ts
4
4
  /** Decoded JSON body limit: 16 MiB. @internal */
@@ -1,5 +1,5 @@
1
1
  require("./chunk-Do9eywBl.cjs");
2
- const require_body = require("./body-Cul4-oB8.cjs");
2
+ const require_body = require("./body-5wwZdMK0.cjs");
3
3
  let node_assert_strict = require("node:assert/strict");
4
4
  let node_test = require("node:test");
5
5
  let node_zlib = require("node:zlib");
@@ -1,4 +1,4 @@
1
- import { r as readBoundedText, t as BodyTooLargeError } from "./body-ByUnLtuw.mjs";
1
+ import { r as readBoundedText, t as BodyTooLargeError } from "./body-DqkK9XBY.mjs";
2
2
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
3
3
  import { test } from "node:test";
4
4
  import { gunzipSync, gzipSync } from "node:zlib";
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-B7Iyskql.cjs");
3
- const require_body = require("./body-Cul4-oB8.cjs");
2
+ const require_request = require("./request-BNAT7gZ3.cjs");
3
+ const require_body = require("./body-5wwZdMK0.cjs");
4
4
  const require_esm = require("./esm-4DuocLvD.cjs");
5
5
  const require_link = require("./link-qDTmjPw_.cjs");
6
6
  const require_url = require("./url-DLuSj57U.cjs");
@@ -1,6 +1,6 @@
1
1
  import { t as esm_default } from "./esm-CnLt0wAF.mjs";
2
- import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-pGToUa6Q.mjs";
3
- import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-ByUnLtuw.mjs";
2
+ import { a as name, i as logRequest, n as createActivityPubRequest, o as version, t as FetchError } from "./request-BiuryHCs.mjs";
3
+ import { n as MAX_BODY_SIZE, r as readBoundedText } from "./body-DqkK9XBY.mjs";
4
4
  import { t as HttpHeaderLink } from "./link-CYszIegt.mjs";
5
5
  import { t as UrlError } from "./url-DjsVF44Q.mjs";
6
6
  import { n as openDirectFetch, r as openPublicFetch, t as createPublicFetch } from "./public-fetch-BUSPlh1C.mjs";
@@ -3,7 +3,7 @@ let node_process = require("node:process");
3
3
  node_process = require_chunk.__toESM(node_process);
4
4
  //#region deno.json
5
5
  var name = "@fedify/vocab-runtime";
6
- var version = "2.0.35";
6
+ var version = "2.0.36-dev.2551+8b012df4";
7
7
  //#endregion
8
8
  //#region src/request.ts
9
9
  /**
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
2
  //#region deno.json
3
3
  var name = "@fedify/vocab-runtime";
4
- var version = "2.0.35";
4
+ var version = "2.0.36-dev.2551+8b012df4";
5
5
  //#endregion
6
6
  //#region src/request.ts
7
7
  /**
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Do9eywBl.cjs");
2
- const require_request = require("./request-B7Iyskql.cjs");
2
+ const require_request = require("./request-BNAT7gZ3.cjs");
3
3
  let node_test = require("node:test");
4
4
  let node_process = require("node:process");
5
5
  node_process = require_chunk.__toESM(node_process);
@@ -1,4 +1,4 @@
1
- import { o as version, r as getUserAgent } from "./request-pGToUa6Q.mjs";
1
+ import { o as version, r as getUserAgent } from "./request-BiuryHCs.mjs";
2
2
  import { test } from "node:test";
3
3
  import process from "node:process";
4
4
  import { deepStrictEqual } from "node:assert";
@@ -4,7 +4,8 @@ let node_assert = require("node:assert");
4
4
  let _js_temporal_polyfill = require("@js-temporal/polyfill");
5
5
  //#region src/temporal.ts
6
6
  /**
7
- * Type guards for `Temporal` namespace objects.
7
+ * Type guards for `Temporal` namespace objects and normalization of
8
+ * `xsd:dateTime` literals.
8
9
  *
9
10
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
10
11
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -57,6 +58,22 @@ function isTemporalInstant(value) {
57
58
  function isTemporalDuration(value) {
58
59
  return typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Temporal.Duration]" && "sign" in value && (value.sign === -1 || value.sign === 0 || value.sign === 1) && "toString" in value && typeof value.toString === "function" && value.toString !== Object.prototype.toString;
59
60
  }
61
+ /**
62
+ * Normalizes an `xsd:dateTime` literal into a form that
63
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
64
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
65
+ * 2.0 allows, are filled in.
66
+ *
67
+ * @param value The date-time literal to normalize.
68
+ * @returns The normalized literal.
69
+ */
70
+ function normalizeDateTimeLiteral(value) {
71
+ const timeStart = value.search(/[Tt ]/) + 1;
72
+ if (timeStart < 1) return value;
73
+ const date = value.substring(0, timeStart);
74
+ const time = value.substring(timeStart).replace(/^(\d{2}:\d{2})(?=\.)/, "$1:00");
75
+ return date + (/[Zz+-]/.test(time) ? time : time + "Z");
76
+ }
60
77
  //#endregion
61
78
  //#region src/temporal.test.ts
62
79
  (0, node_test.test)("isTemporalInstant() accepts polyfill instances", () => {
@@ -131,4 +148,71 @@ function isTemporalDuration(value) {
131
148
  sign: 0
132
149
  }), false);
133
150
  });
151
+ const literalsWithOffset = [
152
+ "2024-01-01T10:00Z",
153
+ "2024-01-01T10:00+09:00",
154
+ "2024-01-01T10:00-05:00",
155
+ "2024-01-01T10:00:00Z",
156
+ "2024-01-01T10:00:00+09:00",
157
+ "2024-01-01T10:00:00-05:00",
158
+ "2024-01-01T10:00:00+0900",
159
+ "2024-01-01T10:00:00.123456789Z",
160
+ "2024-01-01t10:00z",
161
+ "2024-01-01 10:00:00Z"
162
+ ];
163
+ const literalsWithoutOffset = [
164
+ "2024-01-01T10:00",
165
+ "2024-01-01T10:00:00",
166
+ "2024-01-01T10:00:00.123",
167
+ "2025-01-01 12:34:56"
168
+ ];
169
+ const literalsWithoutSeconds = [
170
+ ["2024-01-01T10:00.5Z", "2024-01-01T10:00:00.5Z"],
171
+ ["2024-01-01T10:00.5+09:00", "2024-01-01T10:00:00.5+09:00"],
172
+ ["2024-01-01T10:00.5-05:00", "2024-01-01T10:00:00.5-05:00"],
173
+ ["2024-01-01T10:00.5", "2024-01-01T10:00:00.5Z"],
174
+ ["2024-01-01T10:00.123456789Z", "2024-01-01T10:00:00.123456789Z"]
175
+ ];
176
+ const literalsWithExpandedYear = [["+002024-01-01T10:00", "+002024-01-01T10:00Z"], ["-000001-01-01T10:00.5Z", "-000001-01-01T10:00:00.5Z"]];
177
+ (0, node_test.test)("normalizeDateTimeLiteral() keeps literals with a UTC offset", () => {
178
+ for (const literal of literalsWithOffset) {
179
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral(literal), literal);
180
+ (0, node_assert.strictEqual)(typeof _js_temporal_polyfill.Temporal.Instant.from(literal), "object");
181
+ }
182
+ });
183
+ (0, node_test.test)("normalizeDateTimeLiteral() treats literals without an offset as UTC", () => {
184
+ for (const literal of literalsWithoutOffset) {
185
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral(literal), `${literal}Z`);
186
+ (0, node_assert.strictEqual)(typeof _js_temporal_polyfill.Temporal.Instant.from(normalizeDateTimeLiteral(literal)), "object");
187
+ }
188
+ });
189
+ (0, node_test.test)("normalizeDateTimeLiteral() keeps literals without a time part", () => {
190
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral("2024-01-01"), "2024-01-01");
191
+ });
192
+ (0, node_test.test)("normalizeDateTimeLiteral() inserts omitted seconds before a fraction", () => {
193
+ for (const [literal, normalized] of literalsWithoutSeconds) {
194
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral(literal), normalized);
195
+ (0, node_assert.strictEqual)(typeof _js_temporal_polyfill.Temporal.Instant.from(normalized), "object");
196
+ }
197
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral("2024-01-01T10:00:00.5Z"), "2024-01-01T10:00:00.5Z");
198
+ });
199
+ (0, node_test.test)("normalizeDateTimeLiteral() handles expanded years", () => {
200
+ for (const [literal, normalized] of literalsWithExpandedYear) {
201
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral(literal), normalized);
202
+ (0, node_assert.strictEqual)(typeof _js_temporal_polyfill.Temporal.Instant.from(normalized), "object");
203
+ }
204
+ });
205
+ (0, node_test.test)("normalizeDateTimeLiteral() is idempotent", () => {
206
+ const literals = [
207
+ ...literalsWithOffset,
208
+ ...literalsWithoutOffset,
209
+ ...literalsWithoutSeconds.map(([literal]) => literal),
210
+ ...literalsWithExpandedYear.map(([literal]) => literal),
211
+ "2024-01-01"
212
+ ];
213
+ for (const literal of literals) {
214
+ const normalized = normalizeDateTimeLiteral(literal);
215
+ (0, node_assert.strictEqual)(normalizeDateTimeLiteral(normalized), normalized);
216
+ }
217
+ });
134
218
  //#endregion
@@ -3,7 +3,8 @@ import { strictEqual } from "node:assert";
3
3
  import { Temporal } from "@js-temporal/polyfill";
4
4
  //#region src/temporal.ts
5
5
  /**
6
- * Type guards for `Temporal` namespace objects.
6
+ * Type guards for `Temporal` namespace objects and normalization of
7
+ * `xsd:dateTime` literals.
7
8
  *
8
9
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
9
10
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -56,6 +57,22 @@ function isTemporalInstant(value) {
56
57
  function isTemporalDuration(value) {
57
58
  return typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Temporal.Duration]" && "sign" in value && (value.sign === -1 || value.sign === 0 || value.sign === 1) && "toString" in value && typeof value.toString === "function" && value.toString !== Object.prototype.toString;
58
59
  }
60
+ /**
61
+ * Normalizes an `xsd:dateTime` literal into a form that
62
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
63
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
64
+ * 2.0 allows, are filled in.
65
+ *
66
+ * @param value The date-time literal to normalize.
67
+ * @returns The normalized literal.
68
+ */
69
+ function normalizeDateTimeLiteral(value) {
70
+ const timeStart = value.search(/[Tt ]/) + 1;
71
+ if (timeStart < 1) return value;
72
+ const date = value.substring(0, timeStart);
73
+ const time = value.substring(timeStart).replace(/^(\d{2}:\d{2})(?=\.)/, "$1:00");
74
+ return date + (/[Zz+-]/.test(time) ? time : time + "Z");
75
+ }
59
76
  //#endregion
60
77
  //#region src/temporal.test.ts
61
78
  test("isTemporalInstant() accepts polyfill instances", () => {
@@ -130,5 +147,72 @@ test("isTemporalDuration() rejects default Object.prototype.toString", () => {
130
147
  sign: 0
131
148
  }), false);
132
149
  });
150
+ const literalsWithOffset = [
151
+ "2024-01-01T10:00Z",
152
+ "2024-01-01T10:00+09:00",
153
+ "2024-01-01T10:00-05:00",
154
+ "2024-01-01T10:00:00Z",
155
+ "2024-01-01T10:00:00+09:00",
156
+ "2024-01-01T10:00:00-05:00",
157
+ "2024-01-01T10:00:00+0900",
158
+ "2024-01-01T10:00:00.123456789Z",
159
+ "2024-01-01t10:00z",
160
+ "2024-01-01 10:00:00Z"
161
+ ];
162
+ const literalsWithoutOffset = [
163
+ "2024-01-01T10:00",
164
+ "2024-01-01T10:00:00",
165
+ "2024-01-01T10:00:00.123",
166
+ "2025-01-01 12:34:56"
167
+ ];
168
+ const literalsWithoutSeconds = [
169
+ ["2024-01-01T10:00.5Z", "2024-01-01T10:00:00.5Z"],
170
+ ["2024-01-01T10:00.5+09:00", "2024-01-01T10:00:00.5+09:00"],
171
+ ["2024-01-01T10:00.5-05:00", "2024-01-01T10:00:00.5-05:00"],
172
+ ["2024-01-01T10:00.5", "2024-01-01T10:00:00.5Z"],
173
+ ["2024-01-01T10:00.123456789Z", "2024-01-01T10:00:00.123456789Z"]
174
+ ];
175
+ const literalsWithExpandedYear = [["+002024-01-01T10:00", "+002024-01-01T10:00Z"], ["-000001-01-01T10:00.5Z", "-000001-01-01T10:00:00.5Z"]];
176
+ test("normalizeDateTimeLiteral() keeps literals with a UTC offset", () => {
177
+ for (const literal of literalsWithOffset) {
178
+ strictEqual(normalizeDateTimeLiteral(literal), literal);
179
+ strictEqual(typeof Temporal.Instant.from(literal), "object");
180
+ }
181
+ });
182
+ test("normalizeDateTimeLiteral() treats literals without an offset as UTC", () => {
183
+ for (const literal of literalsWithoutOffset) {
184
+ strictEqual(normalizeDateTimeLiteral(literal), `${literal}Z`);
185
+ strictEqual(typeof Temporal.Instant.from(normalizeDateTimeLiteral(literal)), "object");
186
+ }
187
+ });
188
+ test("normalizeDateTimeLiteral() keeps literals without a time part", () => {
189
+ strictEqual(normalizeDateTimeLiteral("2024-01-01"), "2024-01-01");
190
+ });
191
+ test("normalizeDateTimeLiteral() inserts omitted seconds before a fraction", () => {
192
+ for (const [literal, normalized] of literalsWithoutSeconds) {
193
+ strictEqual(normalizeDateTimeLiteral(literal), normalized);
194
+ strictEqual(typeof Temporal.Instant.from(normalized), "object");
195
+ }
196
+ strictEqual(normalizeDateTimeLiteral("2024-01-01T10:00:00.5Z"), "2024-01-01T10:00:00.5Z");
197
+ });
198
+ test("normalizeDateTimeLiteral() handles expanded years", () => {
199
+ for (const [literal, normalized] of literalsWithExpandedYear) {
200
+ strictEqual(normalizeDateTimeLiteral(literal), normalized);
201
+ strictEqual(typeof Temporal.Instant.from(normalized), "object");
202
+ }
203
+ });
204
+ test("normalizeDateTimeLiteral() is idempotent", () => {
205
+ const literals = [
206
+ ...literalsWithOffset,
207
+ ...literalsWithoutOffset,
208
+ ...literalsWithoutSeconds.map(([literal]) => literal),
209
+ ...literalsWithExpandedYear.map(([literal]) => literal),
210
+ "2024-01-01"
211
+ ];
212
+ for (const literal of literals) {
213
+ const normalized = normalizeDateTimeLiteral(literal);
214
+ strictEqual(normalizeDateTimeLiteral(normalized), normalized);
215
+ }
216
+ });
133
217
  //#endregion
134
218
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-runtime",
3
- "version": "2.0.35",
3
+ "version": "2.0.36-dev.2551+8b012df4",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,7 +1,11 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { strictEqual } from "node:assert";
3
3
  import { test } from "node:test";
4
- import { isTemporalDuration, isTemporalInstant } from "./temporal.ts";
4
+ import {
5
+ isTemporalDuration,
6
+ isTemporalInstant,
7
+ normalizeDateTimeLiteral,
8
+ } from "./temporal.ts";
5
9
 
6
10
  test("isTemporalInstant() accepts polyfill instances", () => {
7
11
  strictEqual(
@@ -119,3 +123,89 @@ test("isTemporalDuration() rejects default Object.prototype.toString", () => {
119
123
  };
120
124
  strictEqual(isTemporalDuration(decoy), false);
121
125
  });
126
+
127
+ const literalsWithOffset = [
128
+ "2024-01-01T10:00Z",
129
+ "2024-01-01T10:00+09:00",
130
+ "2024-01-01T10:00-05:00",
131
+ "2024-01-01T10:00:00Z",
132
+ "2024-01-01T10:00:00+09:00",
133
+ "2024-01-01T10:00:00-05:00",
134
+ "2024-01-01T10:00:00+0900",
135
+ "2024-01-01T10:00:00.123456789Z",
136
+ "2024-01-01t10:00z",
137
+ "2024-01-01 10:00:00Z",
138
+ ] as const;
139
+
140
+ const literalsWithoutOffset = [
141
+ "2024-01-01T10:00",
142
+ "2024-01-01T10:00:00",
143
+ "2024-01-01T10:00:00.123",
144
+ "2025-01-01 12:34:56",
145
+ ] as const;
146
+
147
+ const literalsWithoutSeconds = [
148
+ ["2024-01-01T10:00.5Z", "2024-01-01T10:00:00.5Z"],
149
+ ["2024-01-01T10:00.5+09:00", "2024-01-01T10:00:00.5+09:00"],
150
+ ["2024-01-01T10:00.5-05:00", "2024-01-01T10:00:00.5-05:00"],
151
+ ["2024-01-01T10:00.5", "2024-01-01T10:00:00.5Z"],
152
+ ["2024-01-01T10:00.123456789Z", "2024-01-01T10:00:00.123456789Z"],
153
+ ] as const;
154
+
155
+ const literalsWithExpandedYear = [
156
+ ["+002024-01-01T10:00", "+002024-01-01T10:00Z"],
157
+ ["-000001-01-01T10:00.5Z", "-000001-01-01T10:00:00.5Z"],
158
+ ] as const;
159
+
160
+ test("normalizeDateTimeLiteral() keeps literals with a UTC offset", () => {
161
+ for (const literal of literalsWithOffset) {
162
+ strictEqual(normalizeDateTimeLiteral(literal), literal);
163
+ strictEqual(typeof Temporal.Instant.from(literal), "object");
164
+ }
165
+ });
166
+
167
+ test("normalizeDateTimeLiteral() treats literals without an offset as UTC", () => {
168
+ for (const literal of literalsWithoutOffset) {
169
+ strictEqual(normalizeDateTimeLiteral(literal), `${literal}Z`);
170
+ strictEqual(
171
+ typeof Temporal.Instant.from(normalizeDateTimeLiteral(literal)),
172
+ "object",
173
+ );
174
+ }
175
+ });
176
+
177
+ test("normalizeDateTimeLiteral() keeps literals without a time part", () => {
178
+ strictEqual(normalizeDateTimeLiteral("2024-01-01"), "2024-01-01");
179
+ });
180
+
181
+ test("normalizeDateTimeLiteral() inserts omitted seconds before a fraction", () => {
182
+ for (const [literal, normalized] of literalsWithoutSeconds) {
183
+ strictEqual(normalizeDateTimeLiteral(literal), normalized);
184
+ strictEqual(typeof Temporal.Instant.from(normalized), "object");
185
+ }
186
+ strictEqual(
187
+ normalizeDateTimeLiteral("2024-01-01T10:00:00.5Z"),
188
+ "2024-01-01T10:00:00.5Z",
189
+ );
190
+ });
191
+
192
+ test("normalizeDateTimeLiteral() handles expanded years", () => {
193
+ for (const [literal, normalized] of literalsWithExpandedYear) {
194
+ strictEqual(normalizeDateTimeLiteral(literal), normalized);
195
+ strictEqual(typeof Temporal.Instant.from(normalized), "object");
196
+ }
197
+ });
198
+
199
+ test("normalizeDateTimeLiteral() is idempotent", () => {
200
+ const literals = [
201
+ ...literalsWithOffset,
202
+ ...literalsWithoutOffset,
203
+ ...literalsWithoutSeconds.map(([literal]) => literal),
204
+ ...literalsWithExpandedYear.map(([literal]) => literal),
205
+ "2024-01-01",
206
+ ];
207
+ for (const literal of literals) {
208
+ const normalized = normalizeDateTimeLiteral(literal);
209
+ strictEqual(normalizeDateTimeLiteral(normalized), normalized);
210
+ }
211
+ });
package/src/temporal.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Type guards for `Temporal` namespace objects.
2
+ * Type guards for `Temporal` namespace objects and normalization of
3
+ * `xsd:dateTime` literals.
3
4
  *
4
5
  * Fedify accepts both runtime polyfills (e.g. `@js-temporal/polyfill`,
5
6
  * `temporal-polyfill`) and the host's native `Temporal` implementation
@@ -72,3 +73,21 @@ export function isTemporalDuration(value: unknown): value is Temporal.Duration {
72
73
  value.toString !== Object.prototype.toString
73
74
  );
74
75
  }
76
+
77
+ /**
78
+ * Normalizes an `xsd:dateTime` literal into a form that
79
+ * `Temporal.Instant.from()` accepts. A literal without a UTC offset is
80
+ * treated as UTC, and seconds omitted before a fraction, which Activity Streams
81
+ * 2.0 allows, are filled in.
82
+ *
83
+ * @param value The date-time literal to normalize.
84
+ * @returns The normalized literal.
85
+ */
86
+ export function normalizeDateTimeLiteral(value: string): string {
87
+ const timeStart = value.search(/[Tt ]/) + 1;
88
+ if (timeStart < 1) return value;
89
+ const date = value.substring(0, timeStart);
90
+ const time = value.substring(timeStart)
91
+ .replace(/^(\d{2}:\d{2})(?=\.)/, "$1:00");
92
+ return date + (/[Zz+-]/.test(time) ? time : time + "Z");
93
+ }