@nextera.one/tps-standard 0.5.34 → 0.7.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +133 -56
  3. package/dist/driver-manager.d.ts +34 -0
  4. package/dist/driver-manager.js +53 -0
  5. package/dist/driver-manager.js.map +1 -0
  6. package/dist/drivers/chinese.d.ts +25 -0
  7. package/dist/drivers/chinese.js +485 -0
  8. package/dist/drivers/chinese.js.map +1 -0
  9. package/dist/esm/date.js +170 -0
  10. package/dist/esm/date.js.map +1 -0
  11. package/dist/esm/driver-manager.js +49 -0
  12. package/dist/esm/driver-manager.js.map +1 -0
  13. package/dist/esm/drivers/chinese.js +481 -0
  14. package/dist/esm/drivers/chinese.js.map +1 -0
  15. package/dist/esm/drivers/gregorian.js +160 -0
  16. package/dist/esm/drivers/gregorian.js.map +1 -0
  17. package/dist/esm/drivers/hijri.js +184 -0
  18. package/dist/esm/drivers/hijri.js.map +1 -0
  19. package/dist/esm/drivers/holocene.js +115 -0
  20. package/dist/esm/drivers/holocene.js.map +1 -0
  21. package/dist/esm/drivers/julian.js +161 -0
  22. package/dist/esm/drivers/julian.js.map +1 -0
  23. package/dist/esm/drivers/persian.js +190 -0
  24. package/dist/esm/drivers/persian.js.map +1 -0
  25. package/dist/esm/drivers/tps.js +181 -0
  26. package/dist/esm/drivers/tps.js.map +1 -0
  27. package/dist/esm/drivers/unix.js +50 -0
  28. package/dist/esm/drivers/unix.js.map +1 -0
  29. package/dist/esm/index.js +873 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/types.js +28 -0
  32. package/dist/esm/types.js.map +1 -0
  33. package/dist/esm/uid.js +221 -0
  34. package/dist/esm/uid.js.map +1 -0
  35. package/dist/esm/utils/calendar.js +126 -0
  36. package/dist/esm/utils/calendar.js.map +1 -0
  37. package/dist/esm/utils/env.js +76 -0
  38. package/dist/esm/utils/env.js.map +1 -0
  39. package/dist/esm/utils/timezone.js +168 -0
  40. package/dist/esm/utils/timezone.js.map +1 -0
  41. package/dist/esm/utils/tps-string.js +160 -0
  42. package/dist/esm/utils/tps-string.js.map +1 -0
  43. package/dist/index.d.ts +91 -2
  44. package/dist/index.js +412 -132
  45. package/dist/index.js.map +1 -1
  46. package/dist/types.d.ts +19 -1
  47. package/dist/types.js +1 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/uid.js +1 -1
  50. package/dist/uid.js.map +1 -1
  51. package/dist/utils/timezone.d.ts +32 -0
  52. package/dist/utils/timezone.js +173 -0
  53. package/dist/utils/timezone.js.map +1 -0
  54. package/package.json +20 -5
  55. package/src/driver-manager.ts +54 -0
  56. package/src/drivers/chinese.ts +542 -0
  57. package/src/index.ts +379 -123
  58. package/src/types.ts +26 -2
  59. package/src/uid.ts +2 -2
  60. package/src/utils/timezone.ts +182 -0
package/dist/index.js CHANGED
@@ -3,10 +3,18 @@
3
3
  * TPS: Temporal Positioning System
4
4
  * The Universal Protocol for Space-Time Coordinates.
5
5
  * @packageDocumentation
6
- * @version 0.5.34
6
+ * @version 0.6.0
7
7
  * @license Apache-2.0
8
8
  * @copyright 2026 TPS Standards Working Group
9
9
  *
10
+ * v0.5.35 Changes:
11
+ * - Added TPS.now(), TPS.diff(), TPS.add() convenience methods
12
+ * - Added Chinese Lunisolar (chin) calendar driver
13
+ * - Added DriverManager (driver registry separated from TPS class)
14
+ * - Added timezone utility (src/utils/timezone.ts) with IANA + offset support
15
+ * - TPS.toDate() now respects ;tz= extensions when present
16
+ * - ESM dual-mode exports + browser IIFE bundle
17
+ *
10
18
  * v0.5.0 Changes:
11
19
  * - Added Actor anchor (A:) for provenance tracking
12
20
  * - Added Signature (!) for cryptographic verification
@@ -28,7 +36,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
28
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
29
37
  };
30
38
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.TPS = exports.Env = void 0;
39
+ exports.TPS = exports.getOffsetString = exports.localToUtc = exports.utcToLocal = exports.DriverManager = exports.Env = void 0;
32
40
  // built-in drivers are registered automatically; importing them here
33
41
  // ensures they are included when the library bundler/tree-shaker runs.
34
42
  const gregorian_1 = require("./drivers/gregorian");
@@ -38,12 +46,21 @@ const persian_1 = require("./drivers/persian");
38
46
  const hijri_1 = require("./drivers/hijri");
39
47
  const julian_1 = require("./drivers/julian");
40
48
  const holocene_1 = require("./drivers/holocene");
49
+ const chinese_1 = require("./drivers/chinese");
41
50
  __exportStar(require("./types"), exports);
42
51
  __exportStar(require("./uid"), exports);
43
52
  __exportStar(require("./date"), exports);
44
53
  var env_1 = require("./utils/env");
45
54
  Object.defineProperty(exports, "Env", { enumerable: true, get: function () { return env_1.Env; } });
55
+ var driver_manager_1 = require("./driver-manager");
56
+ Object.defineProperty(exports, "DriverManager", { enumerable: true, get: function () { return driver_manager_1.DriverManager; } });
57
+ var timezone_1 = require("./utils/timezone");
58
+ Object.defineProperty(exports, "utcToLocal", { enumerable: true, get: function () { return timezone_1.utcToLocal; } });
59
+ Object.defineProperty(exports, "localToUtc", { enumerable: true, get: function () { return timezone_1.localToUtc; } });
60
+ Object.defineProperty(exports, "getOffsetString", { enumerable: true, get: function () { return timezone_1.getOffsetString; } });
61
+ const driver_manager_2 = require("./driver-manager");
46
62
  const tps_string_1 = require("./utils/tps-string");
63
+ const timezone_2 = require("./utils/timezone");
47
64
  const types_1 = require("./types");
48
65
  class TPS {
49
66
  /**
@@ -51,7 +68,7 @@ class TPS {
51
68
  * @param driver - The driver instance to register.
52
69
  */
53
70
  static registerDriver(driver) {
54
- this.drivers.set(driver.code, driver);
71
+ this.driverManager.register(driver);
55
72
  }
56
73
  /**
57
74
  * Gets a registered calendar driver.
@@ -59,7 +76,7 @@ class TPS {
59
76
  * @returns The driver or undefined.
60
77
  */
61
78
  static getDriver(code) {
62
- return this.drivers.get(code);
79
+ return this.driverManager.get(code);
63
80
  }
64
81
  // --- CORE METHODS ---
65
82
  /**
@@ -75,6 +92,24 @@ class TPS {
75
92
  let s = input.trim().replace(/\s+/g, "");
76
93
  if (!s)
77
94
  return s;
95
+ // ── 1.2 Compact scheme normalization (v0.6.0) ──────────────────────────
96
+ // TPS:... → tps://... (generic compact)
97
+ // NIP4:x → tps://net:ip4:x (IPv4 shorthand)
98
+ // NIP6:x → tps://net:ip6:x (IPv6 shorthand)
99
+ // NODE:x → tps://node:x (logical node shorthand)
100
+ if (/^TPS:/i.test(s) && !s.toLowerCase().startsWith("tps://")) {
101
+ // TPS:L:... or TPS:lat,lon... → tps://...
102
+ s = "tps://" + s.slice(4); // strip 'TPS:'
103
+ }
104
+ else if (/^NIP4:/i.test(s)) {
105
+ s = "tps://net:ip4:" + s.slice(5);
106
+ }
107
+ else if (/^NIP6:/i.test(s)) {
108
+ s = "tps://net:ip6:" + s.slice(5);
109
+ }
110
+ else if (/^NODE:/i.test(s)) {
111
+ s = "tps://node:" + s.slice(5);
112
+ }
78
113
  // ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
79
114
  // The input may contain "/T:" from older versions; we normalise early so
80
115
  // subsequent logic can assume only the '@' form.
@@ -230,6 +265,10 @@ class TPS {
230
265
  signature = sigMatch.groups.sig;
231
266
  timeOnly = input.split(/[!;?#]/)[0];
232
267
  }
268
+ else {
269
+ // Strip extension/query/fragment suffix so parseTimeString sees only tokens
270
+ timeOnly = input.split(/[;?#]/)[0];
271
+ }
233
272
  const parsed = (0, tps_string_1.parseTimeString)(timeOnly);
234
273
  if (!parsed)
235
274
  return null;
@@ -237,6 +276,23 @@ class TPS {
237
276
  if (signature)
238
277
  comp.signature = signature;
239
278
  comp.order = parsed.order;
279
+ // Route through the same group mapper used by REGEX_URI for consistency
280
+ // (handles extensions ;KEY:val and context #C:key=val)
281
+ const syntheticGroups = {
282
+ calendar: match.groups.calendar ?? "",
283
+ signature: match.groups.signature ?? "",
284
+ extensions: match.groups.extensions ?? "",
285
+ context: match.groups.context ?? "",
286
+ location: "", // no location in time-only string
287
+ actor: "",
288
+ };
289
+ const mappedComp = this._mapGroupsToComponents(syntheticGroups);
290
+ // Merge temporal components from parseTimeString with mapped metadata
291
+ Object.assign(comp, {
292
+ signature: mappedComp.signature || comp.signature,
293
+ extensions: mappedComp.extensions || comp.extensions,
294
+ context: mappedComp.context,
295
+ });
240
296
  return comp;
241
297
  }
242
298
  /**
@@ -245,64 +301,99 @@ class TPS {
245
301
  * @returns Full URI string (e.g. "tps://...").
246
302
  */
247
303
  static toURI(comp) {
248
- // 1. Build Space Part (L: anchor)
249
- let spacePart = "L:-"; // Default: unknown
250
- if (comp.spaceAnchor) {
251
- spacePart = comp.spaceAnchor;
252
- }
253
- else if (comp.isHiddenLocation) {
254
- spacePart = "L:~";
304
+ // ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
305
+ // Build an ordered list of location layer strings, then join with ";"
306
+ const layers = [];
307
+ // Privacy shorthand takes priority
308
+ if (comp.isHiddenLocation) {
309
+ layers.push("L:~");
255
310
  }
256
311
  else if (comp.isRedactedLocation) {
257
- spacePart = "L:redacted";
312
+ layers.push("L:redacted");
258
313
  }
259
314
  else if (comp.isUnknownLocation) {
260
- spacePart = "L:-";
315
+ layers.push("L:-");
316
+ }
317
+ else if (comp.spaceAnchor) {
318
+ // Generic / legacy anchor (adm:, planet:, etc.)
319
+ layers.push(comp.spaceAnchor);
320
+ }
321
+ else if (comp.ipv4) {
322
+ layers.push(`net:ip4:${comp.ipv4}`);
323
+ }
324
+ else if (comp.ipv6) {
325
+ layers.push(`net:ip6:${comp.ipv6}`);
326
+ }
327
+ else if (comp.nodeName) {
328
+ layers.push(`node:${comp.nodeName}`);
261
329
  }
262
330
  else if (comp.s2Cell) {
263
- spacePart = `L:s2=${comp.s2Cell}`;
331
+ layers.push(`S2:${comp.s2Cell}`);
264
332
  }
265
333
  else if (comp.h3Cell) {
266
- spacePart = `L:h3=${comp.h3Cell}`;
267
- }
268
- else if (comp.plusCode) {
269
- spacePart = `L:plus=${comp.plusCode}`;
334
+ layers.push(`H3:${comp.h3Cell}`);
270
335
  }
271
336
  else if (comp.what3words) {
272
- spacePart = `L:w3w=${comp.what3words}`;
337
+ layers.push(`3W:${comp.what3words}`);
338
+ }
339
+ else if (comp.plusCode) {
340
+ layers.push(`plus:${comp.plusCode}`);
273
341
  }
274
342
  else if (comp.building) {
275
- spacePart = `L:bldg=${comp.building}`;
343
+ layers.push(`bldg:${comp.building}`);
276
344
  if (comp.floor)
277
- spacePart += `.floor=${comp.floor}`;
345
+ layers.push(`floor:${comp.floor}`);
278
346
  if (comp.room)
279
- spacePart += `.room=${comp.room}`;
347
+ layers.push(`room:${comp.room}`);
348
+ if (comp.door)
349
+ layers.push(`door:${comp.door}`);
280
350
  if (comp.zone)
281
- spacePart += `.zone=${comp.zone}`;
351
+ layers.push(`zone:${comp.zone}`);
282
352
  }
283
353
  else if (comp.latitude !== undefined && comp.longitude !== undefined) {
284
- spacePart = `L:${comp.latitude},${comp.longitude}`;
285
- if (comp.altitude !== undefined) {
286
- spacePart += `,${comp.altitude}m`;
287
- }
288
- }
289
- // 2. Build Actor Part (A: anchor) - optional
290
- let actorPart = "";
291
- if (comp.actor) {
292
- actorPart = `/A:${comp.actor}`;
354
+ let gps = `L:${comp.latitude},${comp.longitude}`;
355
+ if (comp.altitude !== undefined)
356
+ gps += `,${comp.altitude}m`;
357
+ layers.push(gps);
293
358
  }
294
- // 3. Build Time Part (handles order & signature)
359
+ else {
360
+ layers.push("L:-"); // unknown fallback
361
+ }
362
+ // Place layer (P:) — appended after primary location
363
+ if (comp.placeCountryCode || comp.placeCountryName ||
364
+ comp.placeCityCode || comp.placeCityName) {
365
+ const pParts = [];
366
+ if (comp.placeCountryCode)
367
+ pParts.push(`cc=${comp.placeCountryCode}`);
368
+ if (comp.placeCountryName)
369
+ pParts.push(`cn=${comp.placeCountryName}`);
370
+ if (comp.placeCityCode)
371
+ pParts.push(`ci=${comp.placeCityCode}`);
372
+ if (comp.placeCityName)
373
+ pParts.push(`ct=${comp.placeCityName}`);
374
+ layers.push(`P:${pParts.join(",")}`);
375
+ }
376
+ const locationStr = layers.join(";");
377
+ // ── 2. Actor (/A:...) ─────────────────────────────────────────────────────
378
+ const actorPart = comp.actor ? `/A:${comp.actor}` : "";
379
+ // ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
295
380
  const timePart = (0, tps_string_1.buildTimePart)(comp);
296
- // 5. Build Extensions
381
+ // ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
297
382
  let extPart = "";
298
383
  if (comp.extensions && Object.keys(comp.extensions).length > 0) {
299
- const extStrings = Object.entries(comp.extensions).map(([k, v]) => `${k}=${v}`);
300
- extPart = `;${extStrings.join(".")}`;
384
+ const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
385
+ // Emit as KEY:val (preferred v0.6.0 style)
386
+ return `${k.toUpperCase()}:${v}`;
387
+ });
388
+ extPart = `;${extStrings.join(";")}`;
389
+ }
390
+ // ── 5. Context (#C:key=val;...) ──────────────────────────────────────────
391
+ let contextPart = "";
392
+ if (comp.context && Object.keys(comp.context).length > 0) {
393
+ const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
394
+ contextPart = `#C:${ctxStrings.join(";")}`;
301
395
  }
302
- // timePart already begins with 'T:'. The new canonical separator is '@'
303
- // instead of '/', so we interpolate it accordingly. Actor anchor (if
304
- // present) still uses a leading slash.
305
- return `tps://${spacePart}${actorPart}@${timePart}${extPart}`;
396
+ return `tps://${locationStr}${actorPart}@${timePart}${extPart}${contextPart}`;
306
397
  }
307
398
  /**
308
399
  * CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
@@ -315,7 +406,7 @@ class TPS {
315
406
  */
316
407
  static fromDate(date = new Date(), calendar = types_1.DefaultCalendars.TPS, opts) {
317
408
  const normalizedCalendar = calendar.toLowerCase();
318
- const driver = this.drivers.get(normalizedCalendar);
409
+ const driver = this.driverManager.get(normalizedCalendar);
319
410
  if (driver) {
320
411
  // when caller requested an explicit order we can bypass the driver's
321
412
  // `fromDate` helper and instead generate components ourselves so that
@@ -382,12 +473,21 @@ class TPS {
382
473
  if (!parsed)
383
474
  return null;
384
475
  const cal = parsed.calendar || types_1.DefaultCalendars.TPS;
385
- const driver = this.drivers.get(cal);
476
+ const driver = this.driverManager.get(cal);
386
477
  if (!driver) {
387
478
  console.error(`Calendar driver '${cal}' not registered.`);
388
479
  return null;
389
480
  }
390
- return driver.getDateFromComponents(parsed);
481
+ const date = driver.getDateFromComponents(parsed);
482
+ // If the URI has a ;tz= extension, the calendar date was expressed in local
483
+ // time. Convert from local → UTC using the timezone utility.
484
+ const tz = parsed.extensions?.["tz"];
485
+ if (tz && date) {
486
+ const localMs = date.getTime();
487
+ const utcMs = (0, timezone_2.localToUtc)(localMs, tz);
488
+ return new Date(utcMs);
489
+ }
490
+ return date;
391
491
  }
392
492
  // --- DRIVER CONVENIENCE METHODS ---
393
493
  /**
@@ -409,7 +509,7 @@ class TPS {
409
509
  * ```
410
510
  */
411
511
  static parseCalendarDate(calendar, dateString, format) {
412
- const driver = this.drivers.get(calendar);
512
+ const driver = this.driverManager.get(calendar);
413
513
  if (!driver) {
414
514
  throw new Error(`Calendar driver '${calendar}' not found. Register a driver first.`);
415
515
  }
@@ -470,130 +570,309 @@ class TPS {
470
570
  * ```
471
571
  */
472
572
  static formatCalendarDate(calendar, components, format) {
473
- const driver = this.drivers.get(calendar);
573
+ const driver = this.driverManager.get(calendar);
474
574
  if (!driver) {
475
575
  throw new Error(`Calendar driver '${calendar}' not found.`);
476
576
  }
477
577
  // format is guaranteed by the interface, so we can call it directly.
478
578
  return driver.format(components, format);
479
579
  }
580
+ // --- CONVENIENCE METHODS ---
581
+ /**
582
+ * Returns a TPS time string for the current moment.
583
+ * Shorthand for `TPS.fromDate(new Date(), calendar, opts)`.
584
+ *
585
+ * @param calendar - Calendar code. Defaults to 'greg'.
586
+ * @param opts - Optional `order` (ASC/DESC) parameter.
587
+ * @returns TPS time string.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * TPS.now(); // "T:greg.m3.c1.y26.m3.d4.h06.m30.s00.m0"
592
+ * TPS.now('hij'); // "T:hij.y1447.m09.d05.h06.m30.s00"
593
+ * ```
594
+ */
595
+ static now(calendar = types_1.DefaultCalendars.GREG, opts) {
596
+ return this.fromDate(new Date(), calendar, opts);
597
+ }
598
+ /**
599
+ * Returns the difference in milliseconds between two TPS strings.
600
+ * The result is `t2 - t1`; negative if t1 is after t2.
601
+ *
602
+ * @param t1 - First TPS string (subtracted from t2).
603
+ * @param t2 - Second TPS string.
604
+ * @returns Milliseconds between the two moments, or NaN on parse failure.
605
+ *
606
+ * @example
607
+ * ```ts
608
+ * const ms = TPS.diff('T:greg.m3.c1.y26.m1.d1.h0.m0.s0.m0',
609
+ * 'T:greg.m3.c1.y26.m1.d2.h0.m0.s0.m0');
610
+ * // 86_400_000 (one day)
611
+ * ```
612
+ */
613
+ static diff(t1, t2) {
614
+ const d1 = this.toDate(t1);
615
+ const d2 = this.toDate(t2);
616
+ if (!d1 || !d2)
617
+ return NaN;
618
+ return d2.getTime() - d1.getTime();
619
+ }
620
+ /**
621
+ * Returns a new TPS string shifted by the given duration.
622
+ * The result is in the same calendar as the original string.
623
+ *
624
+ * @param tpsStr - Source TPS string.
625
+ * @param duration - Object with optional `days`, `hours`, `minutes`, `seconds`, `milliseconds`.
626
+ * @returns Shifted TPS string, or null if the input is invalid.
627
+ *
628
+ * @example
629
+ * ```ts
630
+ * const t = 'T:greg.m3.c1.y26.m1.d9.h14.m30.s25.m0';
631
+ * TPS.add(t, { days: 7 }); // one week later
632
+ * TPS.add(t, { hours: -2 }); // two hours earlier
633
+ * ```
634
+ */
635
+ static add(tpsStr, duration) {
636
+ const date = this.toDate(tpsStr);
637
+ if (!date)
638
+ return null;
639
+ const parsed = this.parse(tpsStr);
640
+ const calendar = parsed?.calendar ?? types_1.DefaultCalendars.GREG;
641
+ const order = parsed?.order;
642
+ const deltaMs = (duration.days ?? 0) * 86400000 +
643
+ (duration.hours ?? 0) * 3600000 +
644
+ (duration.minutes ?? 0) * 60000 +
645
+ (duration.seconds ?? 0) * 1000 +
646
+ (duration.milliseconds ?? 0);
647
+ const shifted = new Date(date.getTime() + deltaMs);
648
+ return this.fromDate(shifted, calendar, order ? { order } : undefined);
649
+ }
480
650
  // --- INTERNAL HELPERS ---
481
651
  static _mapGroupsToComponents(g) {
482
652
  const components = {};
483
653
  components.calendar = g.calendar;
484
- // Signature Mapping
654
+ // ── Signature ────────────────────────────────────────────────────────────
485
655
  if (g.signature) {
486
656
  components.signature = g.signature;
487
657
  }
488
- // Actor Mapping
658
+ // ── Actor (/A:...) ────────────────────────────────────────────────────────
489
659
  if (g.actor) {
490
- components.actor = g.actor;
660
+ components.actor = g.actor.trim();
661
+ }
662
+ // ── Location layers (v0.6.0: multi-layer, ;-separated) ───────────────────
663
+ if (g.location) {
664
+ this._parseLocationLayers(g.location, components);
665
+ }
666
+ // ── Extensions (;KEY:val or ;key=val after T: tokens) ────────────────────
667
+ if (g.extensions) {
668
+ const extObj = {};
669
+ g.extensions.split(";").forEach((part) => {
670
+ part = part.trim();
671
+ if (!part)
672
+ return;
673
+ const colonIdx = part.indexOf(":");
674
+ const eqIdx = part.indexOf("=");
675
+ if (colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx)) {
676
+ // KEY:val form (e.g. TZ:+03:00)
677
+ const key = part.substring(0, colonIdx).toLowerCase();
678
+ const val = part.substring(colonIdx + 1);
679
+ if (key && val !== undefined)
680
+ extObj[key] = val;
681
+ }
682
+ else if (eqIdx > 0) {
683
+ // key=val form (e.g. tz=+03:00)
684
+ const key = part.substring(0, eqIdx).toLowerCase();
685
+ const val = part.substring(eqIdx + 1);
686
+ if (key && val !== undefined)
687
+ extObj[key] = val;
688
+ }
689
+ });
690
+ if (Object.keys(extObj).length > 0)
691
+ components.extensions = extObj;
692
+ }
693
+ // ── Context (#C:key=val;key=val) ─────────────────────────────────────────
694
+ if (g.context) {
695
+ const ctx = {};
696
+ g.context.split(";").forEach((part) => {
697
+ part = part.trim();
698
+ if (!part)
699
+ return;
700
+ const eqIdx = part.indexOf("=");
701
+ if (eqIdx > 0) {
702
+ ctx[part.substring(0, eqIdx)] = part.substring(eqIdx + 1);
703
+ }
704
+ });
705
+ if (Object.keys(ctx).length > 0)
706
+ components.context = ctx;
491
707
  }
492
- // Space Mapping
493
- if (g.space) {
494
- // Privacy markers
495
- if (g.space === "unknown" || g.space === "-") {
708
+ return components;
709
+ }
710
+ /**
711
+ * Parses a multi-layer location string (before @T:) into component fields.
712
+ * Layers are `;`-separated. Each layer is identified by its prefix token.
713
+ *
714
+ * Supported layers:
715
+ * L:lat,lon[,altm] — GPS
716
+ * L:~|L:-|L:redacted — Privacy markers
717
+ * P:cc=JO,ci=AMM,... — Place (country/city codes and names)
718
+ * S2:token — S2 cell
719
+ * H3:token — H3 cell
720
+ * 3W:word.word.word — What3Words
721
+ * plus:token — Plus Code
722
+ * net:ip4:x.x.x.x — IPv4
723
+ * net:ip6:x::x — IPv6
724
+ * node:name — Logical node/host
725
+ * bldg:name — Building
726
+ * floor:x — Floor
727
+ * room:x — Room
728
+ * door:x — Door
729
+ * zone:x — Zone
730
+ */
731
+ static _parseLocationLayers(location, components) {
732
+ const layers = location.trim().split(";");
733
+ for (const layer of layers) {
734
+ const l = layer.trim();
735
+ if (!l)
736
+ continue;
737
+ // Privacy shorthand
738
+ if (l === "L:~" || l === "L:hidden") {
739
+ components.isHiddenLocation = true;
740
+ continue;
741
+ }
742
+ if (l === "L:-" || l === "L:unknown") {
496
743
  components.isUnknownLocation = true;
744
+ continue;
497
745
  }
498
- else if (g.space === "redacted") {
746
+ if (l === "L:redacted") {
499
747
  components.isRedactedLocation = true;
748
+ continue;
500
749
  }
501
- else if (g.space === "hidden" || g.space === "~") {
502
- components.isHiddenLocation = true;
750
+ // P: Place layer P:cc=JO,ci=AMM,cn=Jordan,ct=Amman
751
+ if (l.startsWith("P:")) {
752
+ l.slice(2).split(",").forEach((pair) => {
753
+ const eq = pair.indexOf("=");
754
+ if (eq < 1)
755
+ return;
756
+ const k = pair.substring(0, eq).toLowerCase();
757
+ const v = pair.substring(eq + 1);
758
+ if (k === "cc")
759
+ components.placeCountryCode = v;
760
+ else if (k === "cn")
761
+ components.placeCountryName = v;
762
+ else if (k === "ci")
763
+ components.placeCityCode = v;
764
+ else if (k === "ct")
765
+ components.placeCityName = v;
766
+ });
767
+ continue;
768
+ }
769
+ // GPS coordinates (L:lat,lon[,alt])
770
+ if (l.startsWith("L:")) {
771
+ const coords = l.slice(2);
772
+ const m = coords.match(/^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(-?\d+(?:\.\d+)?)m?)?$/);
773
+ if (m) {
774
+ components.latitude = parseFloat(m[1]);
775
+ components.longitude = parseFloat(m[2]);
776
+ if (m[3])
777
+ components.altitude = parseFloat(m[3]);
778
+ }
779
+ continue;
503
780
  }
504
781
  // Geospatial cells
505
- else if (g.s2) {
506
- components.s2Cell = g.s2;
782
+ if (/^S2:/i.test(l)) {
783
+ components.s2Cell = l.slice(3);
784
+ continue;
507
785
  }
508
- else if (g.h3) {
509
- components.h3Cell = g.h3;
786
+ if (/^H3:/i.test(l)) {
787
+ components.h3Cell = l.slice(3);
788
+ continue;
510
789
  }
511
- else if (g.plus) {
512
- components.plusCode = g.plus;
790
+ if (/^3W:/i.test(l)) {
791
+ components.what3words = l.slice(3);
792
+ continue;
513
793
  }
514
- else if (g.w3w) {
515
- components.what3words = g.w3w;
794
+ if (/^plus:/i.test(l)) {
795
+ components.plusCode = l.slice(5);
796
+ continue;
516
797
  }
517
- // Structural anchors
518
- else if (g.bldg) {
519
- components.building = g.bldg;
520
- if (g.floor)
521
- components.floor = g.floor;
522
- if (g.room)
523
- components.room = g.room;
524
- if (g.zone)
525
- components.zone = g.zone;
798
+ // Network
799
+ if (/^net:ip4:/i.test(l)) {
800
+ components.ipv4 = l.slice(8);
801
+ continue;
526
802
  }
527
- // Generic pre-@ anchor (adm/node/net/planet/etc)
528
- else if (g.generic) {
529
- components.spaceAnchor = g.generic;
803
+ if (/^net:ip6:/i.test(l)) {
804
+ components.ipv6 = l.slice(8);
805
+ continue;
530
806
  }
531
- // GPS coordinates
532
- else {
533
- if (g.lat)
534
- components.latitude = parseFloat(g.lat);
535
- if (g.lon)
536
- components.longitude = parseFloat(g.lon);
537
- if (g.alt)
538
- components.altitude = parseFloat(g.alt);
807
+ if (/^node:/i.test(l)) {
808
+ components.nodeName = l.slice(5);
809
+ continue;
810
+ }
811
+ // Structural
812
+ if (/^bldg:/i.test(l)) {
813
+ components.building = l.slice(5);
814
+ continue;
815
+ }
816
+ if (/^floor:/i.test(l)) {
817
+ components.floor = l.slice(6);
818
+ continue;
819
+ }
820
+ if (/^room:/i.test(l)) {
821
+ components.room = l.slice(5);
822
+ continue;
823
+ }
824
+ if (/^door:/i.test(l)) {
825
+ components.door = l.slice(5);
826
+ continue;
827
+ }
828
+ if (/^zone:/i.test(l)) {
829
+ components.zone = l.slice(5);
830
+ continue;
831
+ }
832
+ // Fallback: generic space anchor (adm:, planet:, legacy strings)
833
+ if (l) {
834
+ components.spaceAnchor = components.spaceAnchor
835
+ ? components.spaceAnchor + ";" + l
836
+ : l;
539
837
  }
540
838
  }
541
- // Extensions Mapping
542
- if (g.extensions) {
543
- const extObj = {};
544
- const parts = g.extensions.split(".");
545
- parts.forEach((p) => {
546
- const eqIdx = p.indexOf("=");
547
- if (eqIdx > 0) {
548
- const key = p.substring(0, eqIdx);
549
- const val = p.substring(eqIdx + 1);
550
- if (key && val)
551
- extObj[key] = val;
552
- }
553
- else {
554
- // Legacy format: first char is key
555
- const key = p.charAt(0);
556
- const val = p.substring(1);
557
- if (key && val)
558
- extObj[key] = val;
559
- }
560
- });
561
- components.extensions = extObj;
562
- }
563
- return components;
564
839
  }
565
840
  }
566
841
  exports.TPS = TPS;
567
842
  // --- PLUGIN REGISTRY ---
568
- TPS.drivers = new Map();
569
- // --- REGEX ---
570
- // Updated for v0.5.0: supports L: anchors, A: actor, ! signature, structural & geospatial anchors
571
- // Tokens may appear in any order; actual semantic parsing happens in
572
- // `parseTimeString()` so these patterns are intentionally permissive.
573
- // regex simply ensures prefix, space, calendar, and token characters;
574
- // token order is not enforced (parseTimeString handles semantics).
843
+ /** Shared DriverManager instance — use TPS.driverManager for direct access. */
844
+ TPS.driverManager = new driver_manager_2.DriverManager();
845
+ // --- REGEX (v0.6.0) ---
846
+ // The URI and time regexes are intentionally permissive in the location &
847
+ // extension sections detailed semantic parsing happens in
848
+ // _mapGroupsToComponents() and the layer parsers below.
849
+ //
850
+ // Structure:
851
+ // tps://[location]/A:[actor]@T:[cal].[tokens];[ext];...#C:[ctx];...
852
+ //
853
+ // The `;` separator is used consistently:
854
+ // - between location layers (before @T:)
855
+ // - between extensions (after T: tokens, before #)
856
+ // - between context key=val pairs (after #C:)
575
857
  TPS.REGEX_URI = new RegExp("^tps://" +
576
- // Location part (preserve named captures for space subfields)
577
- "(?:L:)?(?<space>" +
578
- "~|-|unknown|redacted|hidden|" +
579
- "s2=(?<s2>[a-fA-F0-9]+)|" +
580
- "h3=(?<h3>[a-fA-F0-9]+)|" +
581
- "plus=(?<plus>[A-Z0-9+]+)|" +
582
- "w3w=(?<w3w>[a-z]+\\.[a-z]+\\.[a-z]+)|" +
583
- "bldg=(?<bldg>[\\w-]+)(?:\\.floor=(?<floor>[\\w-]+))?(?:\\.room=(?<room>[\\w-]+))?(?:\\.zone=(?<zone>[\\w-]+))?|" +
584
- "(?<lat>-?\\d+(?:\\.\\d+)?),(?<lon>-?\\d+(?:\\.\\d+)?)(?:,(?<alt>-?\\d+(?:\\.\\d+)?)m?)?|" +
585
- "(?<generic>[^@/?#]+)" +
586
- ")" +
587
- "(?:/A:(?<actor>[^/@]+))?" +
858
+ // Location: everything up to optional /A: actor and then @T:
859
+ "(?<location>[^@]+?)" +
860
+ // Optional actor overlay
861
+ "(?:/A:(?<actor>[^@]+))?" +
862
+ // Time section
588
863
  "@T:(?<calendar>[a-z]{3,4})" +
589
- "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
590
- "(?:![^;?#]+)?" +
591
- "(?:;(?<extensions>[^?#]+))?" +
592
- "(?:\\?[^#]+)?" +
593
- "(?:#.+)?$");
864
+ "(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
865
+ // Optional signature
866
+ "(?:!(?<signature>[^;#]+))?" +
867
+ // Optional extensions (;KEY:val;key=val;...)
868
+ "(?:;(?<extensions>[^#]+))?" +
869
+ // Optional context fragment (#C:key=val;...)
870
+ "(?:#C:(?<context>.+))?$");
594
871
  TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
595
- "(?:\\.(?:m-?\\d+|c\\d+|y\\d+|d\\d{1,2}|h\\d{1,2}|s\\d+(?:\\.\\d+)?))*" +
596
- "(?:![^;?#]+)?$");
872
+ "(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
873
+ "(?:!(?<signature>[^;#]+))?" +
874
+ "(?:;(?<extensions>[^#]+))?" +
875
+ "(?:#C:(?<context>.+))?$");
597
876
  // register built-in drivers and set default
598
877
  // (tps and gregorian provide canonical conversions before unix)
599
878
  TPS.registerDriver(new tps_1.TpsDriver());
@@ -603,6 +882,7 @@ TPS.registerDriver(new persian_1.PersianDriver());
603
882
  TPS.registerDriver(new hijri_1.HijriDriver());
604
883
  TPS.registerDriver(new julian_1.JulianDriver());
605
884
  TPS.registerDriver(new holocene_1.HoloceneDriver());
885
+ TPS.registerDriver(new chinese_1.ChineseDriver());
606
886
  /**
607
887
  * `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
608
888
  *