@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.
- package/CHANGELOG.md +88 -0
- package/README.md +133 -56
- package/dist/driver-manager.d.ts +34 -0
- package/dist/driver-manager.js +53 -0
- package/dist/driver-manager.js.map +1 -0
- package/dist/drivers/chinese.d.ts +25 -0
- package/dist/drivers/chinese.js +485 -0
- package/dist/drivers/chinese.js.map +1 -0
- package/dist/esm/date.js +170 -0
- package/dist/esm/date.js.map +1 -0
- package/dist/esm/driver-manager.js +49 -0
- package/dist/esm/driver-manager.js.map +1 -0
- package/dist/esm/drivers/chinese.js +481 -0
- package/dist/esm/drivers/chinese.js.map +1 -0
- package/dist/esm/drivers/gregorian.js +160 -0
- package/dist/esm/drivers/gregorian.js.map +1 -0
- package/dist/esm/drivers/hijri.js +184 -0
- package/dist/esm/drivers/hijri.js.map +1 -0
- package/dist/esm/drivers/holocene.js +115 -0
- package/dist/esm/drivers/holocene.js.map +1 -0
- package/dist/esm/drivers/julian.js +161 -0
- package/dist/esm/drivers/julian.js.map +1 -0
- package/dist/esm/drivers/persian.js +190 -0
- package/dist/esm/drivers/persian.js.map +1 -0
- package/dist/esm/drivers/tps.js +181 -0
- package/dist/esm/drivers/tps.js.map +1 -0
- package/dist/esm/drivers/unix.js +50 -0
- package/dist/esm/drivers/unix.js.map +1 -0
- package/dist/esm/index.js +873 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.js +28 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/uid.js +221 -0
- package/dist/esm/uid.js.map +1 -0
- package/dist/esm/utils/calendar.js +126 -0
- package/dist/esm/utils/calendar.js.map +1 -0
- package/dist/esm/utils/env.js +76 -0
- package/dist/esm/utils/env.js.map +1 -0
- package/dist/esm/utils/timezone.js +168 -0
- package/dist/esm/utils/timezone.js.map +1 -0
- package/dist/esm/utils/tps-string.js +160 -0
- package/dist/esm/utils/tps-string.js.map +1 -0
- package/dist/index.d.ts +91 -2
- package/dist/index.js +412 -132
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/uid.js +1 -1
- package/dist/uid.js.map +1 -1
- package/dist/utils/timezone.d.ts +32 -0
- package/dist/utils/timezone.js +173 -0
- package/dist/utils/timezone.js.map +1 -0
- package/package.json +20 -5
- package/src/driver-manager.ts +54 -0
- package/src/drivers/chinese.ts +542 -0
- package/src/index.ts +379 -123
- package/src/types.ts +26 -2
- package/src/uid.ts +2 -2
- package/src/utils/timezone.ts +182 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TPS: Temporal Positioning System
|
|
3
|
+
* The Universal Protocol for Space-Time Coordinates.
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
* @version 0.6.0
|
|
6
|
+
* @license Apache-2.0
|
|
7
|
+
* @copyright 2026 TPS Standards Working Group
|
|
8
|
+
*
|
|
9
|
+
* v0.5.35 Changes:
|
|
10
|
+
* - Added TPS.now(), TPS.diff(), TPS.add() convenience methods
|
|
11
|
+
* - Added Chinese Lunisolar (chin) calendar driver
|
|
12
|
+
* - Added DriverManager (driver registry separated from TPS class)
|
|
13
|
+
* - Added timezone utility (src/utils/timezone.ts) with IANA + offset support
|
|
14
|
+
* - TPS.toDate() now respects ;tz= extensions when present
|
|
15
|
+
* - ESM dual-mode exports + browser IIFE bundle
|
|
16
|
+
*
|
|
17
|
+
* v0.5.0 Changes:
|
|
18
|
+
* - Added Actor anchor (A:) for provenance tracking
|
|
19
|
+
* - Added Signature (!) for cryptographic verification
|
|
20
|
+
* - Added structural anchors (bldg, floor, room, zone)
|
|
21
|
+
* - Added geospatial cell systems (S2, H3, Plus Code, what3words)
|
|
22
|
+
*/
|
|
23
|
+
// built-in drivers are registered automatically; importing them here
|
|
24
|
+
// ensures they are included when the library bundler/tree-shaker runs.
|
|
25
|
+
import { GregorianDriver } from "./drivers/gregorian";
|
|
26
|
+
import { UnixDriver } from "./drivers/unix";
|
|
27
|
+
import { TpsDriver } from "./drivers/tps";
|
|
28
|
+
import { PersianDriver } from "./drivers/persian";
|
|
29
|
+
import { HijriDriver } from "./drivers/hijri";
|
|
30
|
+
import { JulianDriver } from "./drivers/julian";
|
|
31
|
+
import { HoloceneDriver } from "./drivers/holocene";
|
|
32
|
+
import { ChineseDriver } from "./drivers/chinese";
|
|
33
|
+
export * from "./types";
|
|
34
|
+
export * from "./uid";
|
|
35
|
+
export * from "./date";
|
|
36
|
+
export { Env } from "./utils/env";
|
|
37
|
+
export { DriverManager } from "./driver-manager";
|
|
38
|
+
export { utcToLocal, localToUtc, getOffsetString } from "./utils/timezone";
|
|
39
|
+
import { DriverManager } from "./driver-manager";
|
|
40
|
+
import { buildTimePart, parseTimeString } from "./utils/tps-string";
|
|
41
|
+
import { localToUtc } from "./utils/timezone";
|
|
42
|
+
import { DefaultCalendars, } from "./types";
|
|
43
|
+
export class TPS {
|
|
44
|
+
/**
|
|
45
|
+
* Registers a calendar driver plugin.
|
|
46
|
+
* @param driver - The driver instance to register.
|
|
47
|
+
*/
|
|
48
|
+
static registerDriver(driver) {
|
|
49
|
+
this.driverManager.register(driver);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets a registered calendar driver.
|
|
53
|
+
* @param code - The calendar code.
|
|
54
|
+
* @returns The driver or undefined.
|
|
55
|
+
*/
|
|
56
|
+
static getDriver(code) {
|
|
57
|
+
return this.driverManager.get(code);
|
|
58
|
+
}
|
|
59
|
+
// --- CORE METHODS ---
|
|
60
|
+
/**
|
|
61
|
+
* SANITIZER: Normalises a raw TPS input string before validation.
|
|
62
|
+
*
|
|
63
|
+
* Pure string-based — no parsing into components, no regex beyond simple
|
|
64
|
+
* character checks, no re-serialisation via buildTimePart / toURI.
|
|
65
|
+
*
|
|
66
|
+
* Token ranks (descending): m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
|
|
67
|
+
*/
|
|
68
|
+
static sanitizeTimeInput(input) {
|
|
69
|
+
// ── 1. Whitespace ────────────────────────────────────────────────────────
|
|
70
|
+
let s = input.trim().replace(/\s+/g, "");
|
|
71
|
+
if (!s)
|
|
72
|
+
return s;
|
|
73
|
+
// ── 1.2 Compact scheme normalization (v0.6.0) ──────────────────────────
|
|
74
|
+
// TPS:... → tps://... (generic compact)
|
|
75
|
+
// NIP4:x → tps://net:ip4:x (IPv4 shorthand)
|
|
76
|
+
// NIP6:x → tps://net:ip6:x (IPv6 shorthand)
|
|
77
|
+
// NODE:x → tps://node:x (logical node shorthand)
|
|
78
|
+
if (/^TPS:/i.test(s) && !s.toLowerCase().startsWith("tps://")) {
|
|
79
|
+
// TPS:L:... or TPS:lat,lon... → tps://...
|
|
80
|
+
s = "tps://" + s.slice(4); // strip 'TPS:'
|
|
81
|
+
}
|
|
82
|
+
else if (/^NIP4:/i.test(s)) {
|
|
83
|
+
s = "tps://net:ip4:" + s.slice(5);
|
|
84
|
+
}
|
|
85
|
+
else if (/^NIP6:/i.test(s)) {
|
|
86
|
+
s = "tps://net:ip6:" + s.slice(5);
|
|
87
|
+
}
|
|
88
|
+
else if (/^NODE:/i.test(s)) {
|
|
89
|
+
s = "tps://node:" + s.slice(5);
|
|
90
|
+
}
|
|
91
|
+
// ── 1.5 Convert legacy "/T:" separators to the new canonical "@T:".
|
|
92
|
+
// The input may contain "/T:" from older versions; we normalise early so
|
|
93
|
+
// subsequent logic can assume only the '@' form.
|
|
94
|
+
if (s.includes("/T:")) {
|
|
95
|
+
s = s.replace(/\/T:/g, "@T:");
|
|
96
|
+
}
|
|
97
|
+
// ── 2. Scheme casing ─────────────────────────────────────────────────────
|
|
98
|
+
if (s.slice(0, 6).toLowerCase() === "tps://") {
|
|
99
|
+
s = "tps://" + s.slice(6);
|
|
100
|
+
}
|
|
101
|
+
// ── 3. T: prefix casing (time-only strings) ──────────────────────────────
|
|
102
|
+
if (!s.startsWith("tps://") && s.slice(0, 2).toLowerCase() === "t:") {
|
|
103
|
+
s = "T:" + s.slice(2);
|
|
104
|
+
}
|
|
105
|
+
// ── 4. Locate T: section ─────────────────────────────────────────────────
|
|
106
|
+
let tStart = -1;
|
|
107
|
+
if (s.startsWith("T:")) {
|
|
108
|
+
tStart = 0;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const atT = s.indexOf("@T:");
|
|
112
|
+
if (atT !== -1)
|
|
113
|
+
tStart = atT + 1;
|
|
114
|
+
}
|
|
115
|
+
if (tStart === -1)
|
|
116
|
+
return s; // no T: section — return as-is
|
|
117
|
+
const beforeT = s.slice(0, tStart); // URI prefix or empty
|
|
118
|
+
const timeAndRest = s.slice(tStart); // T:cal.tok... [!sig][;ext]
|
|
119
|
+
// Isolate token section from any trailing suffix (!sig / ;ext / ?q / #f)
|
|
120
|
+
const suffixIdx = timeAndRest.search(/[!;?#]/);
|
|
121
|
+
const timeSuffix = suffixIdx !== -1 ? timeAndRest.slice(suffixIdx) : "";
|
|
122
|
+
const timePart = suffixIdx !== -1 ? timeAndRest.slice(0, suffixIdx) : timeAndRest;
|
|
123
|
+
// timePart = "T:greg.m3.c1.y26.m01.d07.h13.m20.s45"
|
|
124
|
+
// Split off calendar code
|
|
125
|
+
const afterColon = timePart.slice(timePart.indexOf(":") + 1); // "greg.m3.c1..."
|
|
126
|
+
const firstDot = afterColon.indexOf(".");
|
|
127
|
+
const cal = (firstDot !== -1 ? afterColon.slice(0, firstDot) : afterColon).toLowerCase();
|
|
128
|
+
const tokenStr = firstDot !== -1 ? afterColon.slice(firstDot + 1) : "";
|
|
129
|
+
// If no calendar code was provided at all (e.g. "T:"), bail out early
|
|
130
|
+
// rather than inventing a default calendar. The string will remain
|
|
131
|
+
// unparsable so validation can report it as invalid.
|
|
132
|
+
if (!cal) {
|
|
133
|
+
return s;
|
|
134
|
+
}
|
|
135
|
+
// No tokens at all — fill every slot with 0 and return
|
|
136
|
+
if (!tokenStr) {
|
|
137
|
+
return `${beforeT}T:${cal}.m0.c0.y0.m0.d0.h0.m0.s0.m0${timeSuffix}`;
|
|
138
|
+
}
|
|
139
|
+
const tokens = tokenStr
|
|
140
|
+
.split(".")
|
|
141
|
+
.filter((t) => t.length >= 2 && /^[a-z]/.test(t))
|
|
142
|
+
.map((t) => ({ p: t[0], v: t.slice(1) }));
|
|
143
|
+
// ── 6. Detect order from non-m tokens (c=7, y=6, d=4, h=3, s=1) ─────────
|
|
144
|
+
const nonMRank = { c: 7, y: 6, d: 4, h: 3, s: 1 };
|
|
145
|
+
const nonMSeq = tokens
|
|
146
|
+
.filter((t) => t.p !== "m" && nonMRank[t.p] !== undefined)
|
|
147
|
+
.map((t) => nonMRank[t.p]);
|
|
148
|
+
let isAsc = false;
|
|
149
|
+
if (nonMSeq.length >= 2) {
|
|
150
|
+
// ascending when every consecutive rank-diff is positive
|
|
151
|
+
isAsc = nonMSeq.every((r, i) => i === 0 || r > nonMSeq[i - 1]);
|
|
152
|
+
}
|
|
153
|
+
// ── 7. Reverse tokens if ascending ───────────────────────────────────────
|
|
154
|
+
if (isAsc)
|
|
155
|
+
tokens.reverse();
|
|
156
|
+
// ── 8. Disambiguate 'm' tokens by DESC position ──────────────────────────
|
|
157
|
+
// DESC slot order for m tokens: rank 8 (millennium), 5 (month), 2 (minute), 0 (ms)
|
|
158
|
+
const mDescRanks = [8, 5, 2, 0];
|
|
159
|
+
const byRank = new Map();
|
|
160
|
+
let mIdx = 0;
|
|
161
|
+
for (const tok of tokens) {
|
|
162
|
+
if (tok.p === "m") {
|
|
163
|
+
if (mIdx < mDescRanks.length)
|
|
164
|
+
byRank.set(mDescRanks[mIdx++], tok.v);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const r = nonMRank[tok.p];
|
|
168
|
+
if (r !== undefined)
|
|
169
|
+
byRank.set(r, tok.v);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ── 9. Build complete DESC token string, filling gaps with '0' ───────────
|
|
173
|
+
// Full DESC slot sequence: m(8) c(7) y(6) m(5) d(4) h(3) m(2) s(1) m(0)
|
|
174
|
+
const descSlots = [
|
|
175
|
+
["m", 8],
|
|
176
|
+
["c", 7],
|
|
177
|
+
["y", 6],
|
|
178
|
+
["m", 5],
|
|
179
|
+
["d", 4],
|
|
180
|
+
["h", 3],
|
|
181
|
+
["m", 2],
|
|
182
|
+
["s", 1],
|
|
183
|
+
["m", 0],
|
|
184
|
+
];
|
|
185
|
+
const finalTokenStr = descSlots
|
|
186
|
+
.map(([p, r]) => p + (byRank.get(r) ?? "0"))
|
|
187
|
+
.join(".");
|
|
188
|
+
return `${beforeT}T:${cal}.${finalTokenStr}${timeSuffix}`;
|
|
189
|
+
}
|
|
190
|
+
static validate(input) {
|
|
191
|
+
const sanitized = this.sanitizeTimeInput(input);
|
|
192
|
+
if (sanitized.startsWith("tps://")) {
|
|
193
|
+
return this.REGEX_URI.test(sanitized);
|
|
194
|
+
}
|
|
195
|
+
return this.REGEX_TIME.test(sanitized);
|
|
196
|
+
}
|
|
197
|
+
static parse(input) {
|
|
198
|
+
// Always sanitize first so we operate on the canonical form. This also
|
|
199
|
+
// rewrites any legacy "/T:" separators to "@T:" so the regex below can
|
|
200
|
+
// remain strict.
|
|
201
|
+
input = this.sanitizeTimeInput(input);
|
|
202
|
+
// quick fail via regex to rule out obviously bad strings
|
|
203
|
+
if (input.startsWith("tps://")) {
|
|
204
|
+
const match = this.REGEX_URI.exec(input);
|
|
205
|
+
if (!match || !match.groups)
|
|
206
|
+
return null;
|
|
207
|
+
const comp = this._mapGroupsToComponents(match.groups);
|
|
208
|
+
// extract the raw time portion and parse it separately
|
|
209
|
+
const atIdx = input.indexOf("@T:");
|
|
210
|
+
let timeStr = "";
|
|
211
|
+
let signature;
|
|
212
|
+
if (atIdx !== -1) {
|
|
213
|
+
timeStr = input.slice(atIdx + 1); // include the leading 'T:'
|
|
214
|
+
// if there's a signature, capture it first
|
|
215
|
+
const sigMatch = timeStr.match(/!(?<sig>[^;?#]+)/);
|
|
216
|
+
if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
|
|
217
|
+
signature = sigMatch.groups.sig;
|
|
218
|
+
}
|
|
219
|
+
// cut off signature, extensions, query, or fragment
|
|
220
|
+
timeStr = timeStr.split(/[!;?#]/)[0];
|
|
221
|
+
}
|
|
222
|
+
if (timeStr) {
|
|
223
|
+
const parsed = parseTimeString(timeStr);
|
|
224
|
+
if (!parsed)
|
|
225
|
+
return null;
|
|
226
|
+
Object.assign(comp, parsed.components);
|
|
227
|
+
comp.order = parsed.order;
|
|
228
|
+
}
|
|
229
|
+
if (signature) {
|
|
230
|
+
comp.signature = signature;
|
|
231
|
+
}
|
|
232
|
+
return comp;
|
|
233
|
+
}
|
|
234
|
+
// time-only string
|
|
235
|
+
const match = this.REGEX_TIME.exec(input);
|
|
236
|
+
if (!match || !match.groups)
|
|
237
|
+
return null;
|
|
238
|
+
// isolate signature if present
|
|
239
|
+
let timeOnly = input;
|
|
240
|
+
let signature;
|
|
241
|
+
const sigMatch = input.match(/!(?<sig>[^;?#]+)/);
|
|
242
|
+
if (sigMatch && sigMatch.groups && sigMatch.groups.sig) {
|
|
243
|
+
signature = sigMatch.groups.sig;
|
|
244
|
+
timeOnly = input.split(/[!;?#]/)[0];
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// Strip extension/query/fragment suffix so parseTimeString sees only tokens
|
|
248
|
+
timeOnly = input.split(/[;?#]/)[0];
|
|
249
|
+
}
|
|
250
|
+
const parsed = parseTimeString(timeOnly);
|
|
251
|
+
if (!parsed)
|
|
252
|
+
return null;
|
|
253
|
+
const comp = parsed.components;
|
|
254
|
+
if (signature)
|
|
255
|
+
comp.signature = signature;
|
|
256
|
+
comp.order = parsed.order;
|
|
257
|
+
// Route through the same group mapper used by REGEX_URI for consistency
|
|
258
|
+
// (handles extensions ;KEY:val and context #C:key=val)
|
|
259
|
+
const syntheticGroups = {
|
|
260
|
+
calendar: match.groups.calendar ?? "",
|
|
261
|
+
signature: match.groups.signature ?? "",
|
|
262
|
+
extensions: match.groups.extensions ?? "",
|
|
263
|
+
context: match.groups.context ?? "",
|
|
264
|
+
location: "", // no location in time-only string
|
|
265
|
+
actor: "",
|
|
266
|
+
};
|
|
267
|
+
const mappedComp = this._mapGroupsToComponents(syntheticGroups);
|
|
268
|
+
// Merge temporal components from parseTimeString with mapped metadata
|
|
269
|
+
Object.assign(comp, {
|
|
270
|
+
signature: mappedComp.signature || comp.signature,
|
|
271
|
+
extensions: mappedComp.extensions || comp.extensions,
|
|
272
|
+
context: mappedComp.context,
|
|
273
|
+
});
|
|
274
|
+
return comp;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* SERIALIZER: Converts a components object into a full TPS URI.
|
|
278
|
+
* @param comp - The TPS components.
|
|
279
|
+
* @returns Full URI string (e.g. "tps://...").
|
|
280
|
+
*/
|
|
281
|
+
static toURI(comp) {
|
|
282
|
+
// ── 1. Location layers (v0.6.0) ──────────────────────────────────────────
|
|
283
|
+
// Build an ordered list of location layer strings, then join with ";"
|
|
284
|
+
const layers = [];
|
|
285
|
+
// Privacy shorthand takes priority
|
|
286
|
+
if (comp.isHiddenLocation) {
|
|
287
|
+
layers.push("L:~");
|
|
288
|
+
}
|
|
289
|
+
else if (comp.isRedactedLocation) {
|
|
290
|
+
layers.push("L:redacted");
|
|
291
|
+
}
|
|
292
|
+
else if (comp.isUnknownLocation) {
|
|
293
|
+
layers.push("L:-");
|
|
294
|
+
}
|
|
295
|
+
else if (comp.spaceAnchor) {
|
|
296
|
+
// Generic / legacy anchor (adm:, planet:, etc.)
|
|
297
|
+
layers.push(comp.spaceAnchor);
|
|
298
|
+
}
|
|
299
|
+
else if (comp.ipv4) {
|
|
300
|
+
layers.push(`net:ip4:${comp.ipv4}`);
|
|
301
|
+
}
|
|
302
|
+
else if (comp.ipv6) {
|
|
303
|
+
layers.push(`net:ip6:${comp.ipv6}`);
|
|
304
|
+
}
|
|
305
|
+
else if (comp.nodeName) {
|
|
306
|
+
layers.push(`node:${comp.nodeName}`);
|
|
307
|
+
}
|
|
308
|
+
else if (comp.s2Cell) {
|
|
309
|
+
layers.push(`S2:${comp.s2Cell}`);
|
|
310
|
+
}
|
|
311
|
+
else if (comp.h3Cell) {
|
|
312
|
+
layers.push(`H3:${comp.h3Cell}`);
|
|
313
|
+
}
|
|
314
|
+
else if (comp.what3words) {
|
|
315
|
+
layers.push(`3W:${comp.what3words}`);
|
|
316
|
+
}
|
|
317
|
+
else if (comp.plusCode) {
|
|
318
|
+
layers.push(`plus:${comp.plusCode}`);
|
|
319
|
+
}
|
|
320
|
+
else if (comp.building) {
|
|
321
|
+
layers.push(`bldg:${comp.building}`);
|
|
322
|
+
if (comp.floor)
|
|
323
|
+
layers.push(`floor:${comp.floor}`);
|
|
324
|
+
if (comp.room)
|
|
325
|
+
layers.push(`room:${comp.room}`);
|
|
326
|
+
if (comp.door)
|
|
327
|
+
layers.push(`door:${comp.door}`);
|
|
328
|
+
if (comp.zone)
|
|
329
|
+
layers.push(`zone:${comp.zone}`);
|
|
330
|
+
}
|
|
331
|
+
else if (comp.latitude !== undefined && comp.longitude !== undefined) {
|
|
332
|
+
let gps = `L:${comp.latitude},${comp.longitude}`;
|
|
333
|
+
if (comp.altitude !== undefined)
|
|
334
|
+
gps += `,${comp.altitude}m`;
|
|
335
|
+
layers.push(gps);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
layers.push("L:-"); // unknown fallback
|
|
339
|
+
}
|
|
340
|
+
// Place layer (P:) — appended after primary location
|
|
341
|
+
if (comp.placeCountryCode || comp.placeCountryName ||
|
|
342
|
+
comp.placeCityCode || comp.placeCityName) {
|
|
343
|
+
const pParts = [];
|
|
344
|
+
if (comp.placeCountryCode)
|
|
345
|
+
pParts.push(`cc=${comp.placeCountryCode}`);
|
|
346
|
+
if (comp.placeCountryName)
|
|
347
|
+
pParts.push(`cn=${comp.placeCountryName}`);
|
|
348
|
+
if (comp.placeCityCode)
|
|
349
|
+
pParts.push(`ci=${comp.placeCityCode}`);
|
|
350
|
+
if (comp.placeCityName)
|
|
351
|
+
pParts.push(`ct=${comp.placeCityName}`);
|
|
352
|
+
layers.push(`P:${pParts.join(",")}`);
|
|
353
|
+
}
|
|
354
|
+
const locationStr = layers.join(";");
|
|
355
|
+
// ── 2. Actor (/A:...) ─────────────────────────────────────────────────────
|
|
356
|
+
const actorPart = comp.actor ? `/A:${comp.actor}` : "";
|
|
357
|
+
// ── 3. Time (mandatory 9 tokens) ─────────────────────────────────────────
|
|
358
|
+
const timePart = buildTimePart(comp);
|
|
359
|
+
// ── 4. Extensions (;KEY:val;...) ─────────────────────────────────────────
|
|
360
|
+
let extPart = "";
|
|
361
|
+
if (comp.extensions && Object.keys(comp.extensions).length > 0) {
|
|
362
|
+
const extStrings = Object.entries(comp.extensions).map(([k, v]) => {
|
|
363
|
+
// Emit as KEY:val (preferred v0.6.0 style)
|
|
364
|
+
return `${k.toUpperCase()}:${v}`;
|
|
365
|
+
});
|
|
366
|
+
extPart = `;${extStrings.join(";")}`;
|
|
367
|
+
}
|
|
368
|
+
// ── 5. Context (#C:key=val;...) ──────────────────────────────────────────
|
|
369
|
+
let contextPart = "";
|
|
370
|
+
if (comp.context && Object.keys(comp.context).length > 0) {
|
|
371
|
+
const ctxStrings = Object.entries(comp.context).map(([k, v]) => `${k}=${v}`);
|
|
372
|
+
contextPart = `#C:${ctxStrings.join(";")}`;
|
|
373
|
+
}
|
|
374
|
+
return `tps://${locationStr}${actorPart}@${timePart}${extPart}${contextPart}`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* CONVERTER: Creates a TPS Time Object string from a JavaScript Date.
|
|
378
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
379
|
+
* @param date - The JS Date object (defaults to Now).
|
|
380
|
+
* @param calendar - The target calendar driver (default `"tps"`).
|
|
381
|
+
* @param opts - Optional parameters; for built-in calendars the only
|
|
382
|
+
* supported key is `order` which may be `'ascending'` or `'descending'`.
|
|
383
|
+
* @returns Canonical string (e.g., "T:tps.m3.c1.y26...").
|
|
384
|
+
*/
|
|
385
|
+
static fromDate(date = new Date(), calendar = DefaultCalendars.TPS, opts) {
|
|
386
|
+
const normalizedCalendar = calendar.toLowerCase();
|
|
387
|
+
const driver = this.driverManager.get(normalizedCalendar);
|
|
388
|
+
if (driver) {
|
|
389
|
+
// when caller requested an explicit order we can bypass the driver's
|
|
390
|
+
// `fromDate` helper and instead generate components ourselves so that
|
|
391
|
+
// order is honoured even if the driver doesn't know about it. This
|
|
392
|
+
// keeps behaviour identical to the old built-in implementation.
|
|
393
|
+
if (opts?.order) {
|
|
394
|
+
const comp = driver.getComponentsFromDate(date);
|
|
395
|
+
comp.calendar = normalizedCalendar;
|
|
396
|
+
comp.order = opts.order;
|
|
397
|
+
return buildTimePart(comp);
|
|
398
|
+
}
|
|
399
|
+
return driver.getFromDate(date);
|
|
400
|
+
}
|
|
401
|
+
// Fallback for old built-in calendars (shouldn't happen once drivers are
|
|
402
|
+
// registered, but kept for backwards compatibility).
|
|
403
|
+
const comp = { calendar: normalizedCalendar };
|
|
404
|
+
if (normalizedCalendar === DefaultCalendars.UNIX) {
|
|
405
|
+
const s = (date.getTime() / 1000).toFixed(3);
|
|
406
|
+
comp.unixSeconds = parseFloat(s);
|
|
407
|
+
if (opts?.order)
|
|
408
|
+
comp.order = opts.order;
|
|
409
|
+
return buildTimePart(comp);
|
|
410
|
+
}
|
|
411
|
+
if (normalizedCalendar === DefaultCalendars.GREG) {
|
|
412
|
+
const fullYear = date.getUTCFullYear();
|
|
413
|
+
comp.millennium = Math.floor(fullYear / 1000) + 1;
|
|
414
|
+
comp.century = Math.floor((fullYear % 1000) / 100) + 1;
|
|
415
|
+
comp.year = fullYear % 100;
|
|
416
|
+
comp.month = date.getUTCMonth() + 1;
|
|
417
|
+
comp.day = date.getUTCDate();
|
|
418
|
+
comp.hour = date.getUTCHours();
|
|
419
|
+
comp.minute = date.getUTCMinutes();
|
|
420
|
+
comp.second = date.getUTCSeconds();
|
|
421
|
+
comp.millisecond = date.getUTCMilliseconds();
|
|
422
|
+
if (opts?.order)
|
|
423
|
+
comp.order = opts.order;
|
|
424
|
+
return buildTimePart(comp);
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`Calendar driver '${normalizedCalendar}' not implemented. Register a driver.`);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* CONVERTER: Converts a TPS string to a Date in a target calendar format.
|
|
430
|
+
* Uses plugin drivers for cross-calendar conversion.
|
|
431
|
+
* @param tpsString - The source TPS string (any calendar).
|
|
432
|
+
* @param targetCalendar - The target calendar code (e.g., 'hij').
|
|
433
|
+
* @returns A TPS string in the target calendar, or null if invalid.
|
|
434
|
+
*/
|
|
435
|
+
static to(targetCalendar, tpsString) {
|
|
436
|
+
// 1. Parse to components and convert to Gregorian Date
|
|
437
|
+
const gregDate = this.toDate(tpsString);
|
|
438
|
+
if (!gregDate)
|
|
439
|
+
return null;
|
|
440
|
+
// 2. Convert Gregorian to target calendar using driver
|
|
441
|
+
return this.fromDate(gregDate, targetCalendar);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* CONVERTER: Reconstructs a JavaScript Date object from a TPS string.
|
|
445
|
+
* Supports plugin drivers for non-Gregorian calendars.
|
|
446
|
+
* @param tpsString - The TPS string.
|
|
447
|
+
* @returns JS Date object or `null` if invalid.
|
|
448
|
+
*/
|
|
449
|
+
static toDate(tpsString) {
|
|
450
|
+
const parsed = this.parse(tpsString);
|
|
451
|
+
if (!parsed)
|
|
452
|
+
return null;
|
|
453
|
+
const cal = parsed.calendar || DefaultCalendars.TPS;
|
|
454
|
+
const driver = this.driverManager.get(cal);
|
|
455
|
+
if (!driver) {
|
|
456
|
+
console.error(`Calendar driver '${cal}' not registered.`);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const date = driver.getDateFromComponents(parsed);
|
|
460
|
+
// If the URI has a ;tz= extension, the calendar date was expressed in local
|
|
461
|
+
// time. Convert from local → UTC using the timezone utility.
|
|
462
|
+
const tz = parsed.extensions?.["tz"];
|
|
463
|
+
if (tz && date) {
|
|
464
|
+
const localMs = date.getTime();
|
|
465
|
+
const utcMs = localToUtc(localMs, tz);
|
|
466
|
+
return new Date(utcMs);
|
|
467
|
+
}
|
|
468
|
+
return date;
|
|
469
|
+
}
|
|
470
|
+
// --- DRIVER CONVENIENCE METHODS ---
|
|
471
|
+
/**
|
|
472
|
+
* Parse a calendar-specific date string into TPS components.
|
|
473
|
+
* Requires the driver to implement `parseDate`.
|
|
474
|
+
*
|
|
475
|
+
* @param calendar - The calendar code (e.g., 'hij')
|
|
476
|
+
* @param dateString - Date string in calendar-native format (e.g., '1447-07-21')
|
|
477
|
+
* @param format - Optional format string (driver-specific)
|
|
478
|
+
* @returns TPS components or null if parsing fails
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const components = TPS.parseCalendarDate('hij', '1447-07-21');
|
|
483
|
+
* // { calendar: 'hij', year: 1447, month: 7, day: 21 }
|
|
484
|
+
*
|
|
485
|
+
* const uri = TPS.toURI({ ...components, latitude: 31.95, longitude: 35.91 });
|
|
486
|
+
* // "tps://31.95,35.91@T:hij.y1447.m07.d21"
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
static parseCalendarDate(calendar, dateString, format) {
|
|
490
|
+
const driver = this.driverManager.get(calendar);
|
|
491
|
+
if (!driver) {
|
|
492
|
+
throw new Error(`Calendar driver '${calendar}' not found. Register a driver first.`);
|
|
493
|
+
}
|
|
494
|
+
// parseDate is guaranteed by the interface, so we can call it directly.
|
|
495
|
+
return driver.parseDate(dateString, format);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Convert a calendar-specific date string directly to a TPS URI.
|
|
499
|
+
* This is a convenience method that combines parseDate + toURI.
|
|
500
|
+
*
|
|
501
|
+
* @param calendar - The calendar code (e.g., 'hij')
|
|
502
|
+
* @param dateString - Date string in calendar-native format
|
|
503
|
+
* @param location - Optional location (lat/lon/alt or privacy flag)
|
|
504
|
+
* @returns Full TPS URI string
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```ts
|
|
508
|
+
* // With coordinates
|
|
509
|
+
* TPS.fromCalendarDate('hij', '1447-07-21', { latitude: 31.95, longitude: 35.91 });
|
|
510
|
+
* // "tps://31.95,35.91@T:hij.y1447.m07.d21"
|
|
511
|
+
*
|
|
512
|
+
* // With privacy flag
|
|
513
|
+
* TPS.fromCalendarDate('hij', '1447-07-21', { isHiddenLocation: true });
|
|
514
|
+
* // "tps://hidden@T:hij.y1447.m07.d21"
|
|
515
|
+
*
|
|
516
|
+
* // Without location
|
|
517
|
+
* TPS.fromCalendarDate('hij', '1447-07-21');
|
|
518
|
+
* // "tps://unknown@T:hij.y1447.m07.d21"
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
static fromCalendarDate(calendar, dateString, location) {
|
|
522
|
+
const components = this.parseCalendarDate(calendar, dateString);
|
|
523
|
+
if (!components) {
|
|
524
|
+
throw new Error(`Failed to parse date string: ${dateString}`);
|
|
525
|
+
}
|
|
526
|
+
// Merge with location
|
|
527
|
+
const fullComponents = {
|
|
528
|
+
calendar: calendar,
|
|
529
|
+
...components,
|
|
530
|
+
...location,
|
|
531
|
+
};
|
|
532
|
+
return this.toURI(fullComponents);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Format TPS components to a calendar-specific date string.
|
|
536
|
+
* Requires the driver to implement `format`.
|
|
537
|
+
*
|
|
538
|
+
* @param calendar - The calendar code
|
|
539
|
+
* @param components - TPS components to format
|
|
540
|
+
* @param format - Optional format string (driver-specific)
|
|
541
|
+
* @returns Formatted date string in calendar-native format
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* ```ts
|
|
545
|
+
* const tps = TPS.parse('tps://unknown@T:hij.y1447.m07.d21');
|
|
546
|
+
* const formatted = TPS.formatCalendarDate('hij', tps);
|
|
547
|
+
* // "1447-07-21"
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
static formatCalendarDate(calendar, components, format) {
|
|
551
|
+
const driver = this.driverManager.get(calendar);
|
|
552
|
+
if (!driver) {
|
|
553
|
+
throw new Error(`Calendar driver '${calendar}' not found.`);
|
|
554
|
+
}
|
|
555
|
+
// format is guaranteed by the interface, so we can call it directly.
|
|
556
|
+
return driver.format(components, format);
|
|
557
|
+
}
|
|
558
|
+
// --- CONVENIENCE METHODS ---
|
|
559
|
+
/**
|
|
560
|
+
* Returns a TPS time string for the current moment.
|
|
561
|
+
* Shorthand for `TPS.fromDate(new Date(), calendar, opts)`.
|
|
562
|
+
*
|
|
563
|
+
* @param calendar - Calendar code. Defaults to 'greg'.
|
|
564
|
+
* @param opts - Optional `order` (ASC/DESC) parameter.
|
|
565
|
+
* @returns TPS time string.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* TPS.now(); // "T:greg.m3.c1.y26.m3.d4.h06.m30.s00.m0"
|
|
570
|
+
* TPS.now('hij'); // "T:hij.y1447.m09.d05.h06.m30.s00"
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
static now(calendar = DefaultCalendars.GREG, opts) {
|
|
574
|
+
return this.fromDate(new Date(), calendar, opts);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Returns the difference in milliseconds between two TPS strings.
|
|
578
|
+
* The result is `t2 - t1`; negative if t1 is after t2.
|
|
579
|
+
*
|
|
580
|
+
* @param t1 - First TPS string (subtracted from t2).
|
|
581
|
+
* @param t2 - Second TPS string.
|
|
582
|
+
* @returns Milliseconds between the two moments, or NaN on parse failure.
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```ts
|
|
586
|
+
* const ms = TPS.diff('T:greg.m3.c1.y26.m1.d1.h0.m0.s0.m0',
|
|
587
|
+
* 'T:greg.m3.c1.y26.m1.d2.h0.m0.s0.m0');
|
|
588
|
+
* // 86_400_000 (one day)
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
591
|
+
static diff(t1, t2) {
|
|
592
|
+
const d1 = this.toDate(t1);
|
|
593
|
+
const d2 = this.toDate(t2);
|
|
594
|
+
if (!d1 || !d2)
|
|
595
|
+
return NaN;
|
|
596
|
+
return d2.getTime() - d1.getTime();
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Returns a new TPS string shifted by the given duration.
|
|
600
|
+
* The result is in the same calendar as the original string.
|
|
601
|
+
*
|
|
602
|
+
* @param tpsStr - Source TPS string.
|
|
603
|
+
* @param duration - Object with optional `days`, `hours`, `minutes`, `seconds`, `milliseconds`.
|
|
604
|
+
* @returns Shifted TPS string, or null if the input is invalid.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* const t = 'T:greg.m3.c1.y26.m1.d9.h14.m30.s25.m0';
|
|
609
|
+
* TPS.add(t, { days: 7 }); // one week later
|
|
610
|
+
* TPS.add(t, { hours: -2 }); // two hours earlier
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
static add(tpsStr, duration) {
|
|
614
|
+
const date = this.toDate(tpsStr);
|
|
615
|
+
if (!date)
|
|
616
|
+
return null;
|
|
617
|
+
const parsed = this.parse(tpsStr);
|
|
618
|
+
const calendar = parsed?.calendar ?? DefaultCalendars.GREG;
|
|
619
|
+
const order = parsed?.order;
|
|
620
|
+
const deltaMs = (duration.days ?? 0) * 86400000 +
|
|
621
|
+
(duration.hours ?? 0) * 3600000 +
|
|
622
|
+
(duration.minutes ?? 0) * 60000 +
|
|
623
|
+
(duration.seconds ?? 0) * 1000 +
|
|
624
|
+
(duration.milliseconds ?? 0);
|
|
625
|
+
const shifted = new Date(date.getTime() + deltaMs);
|
|
626
|
+
return this.fromDate(shifted, calendar, order ? { order } : undefined);
|
|
627
|
+
}
|
|
628
|
+
// --- INTERNAL HELPERS ---
|
|
629
|
+
static _mapGroupsToComponents(g) {
|
|
630
|
+
const components = {};
|
|
631
|
+
components.calendar = g.calendar;
|
|
632
|
+
// ── Signature ────────────────────────────────────────────────────────────
|
|
633
|
+
if (g.signature) {
|
|
634
|
+
components.signature = g.signature;
|
|
635
|
+
}
|
|
636
|
+
// ── Actor (/A:...) ────────────────────────────────────────────────────────
|
|
637
|
+
if (g.actor) {
|
|
638
|
+
components.actor = g.actor.trim();
|
|
639
|
+
}
|
|
640
|
+
// ── Location layers (v0.6.0: multi-layer, ;-separated) ───────────────────
|
|
641
|
+
if (g.location) {
|
|
642
|
+
this._parseLocationLayers(g.location, components);
|
|
643
|
+
}
|
|
644
|
+
// ── Extensions (;KEY:val or ;key=val after T: tokens) ────────────────────
|
|
645
|
+
if (g.extensions) {
|
|
646
|
+
const extObj = {};
|
|
647
|
+
g.extensions.split(";").forEach((part) => {
|
|
648
|
+
part = part.trim();
|
|
649
|
+
if (!part)
|
|
650
|
+
return;
|
|
651
|
+
const colonIdx = part.indexOf(":");
|
|
652
|
+
const eqIdx = part.indexOf("=");
|
|
653
|
+
if (colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx)) {
|
|
654
|
+
// KEY:val form (e.g. TZ:+03:00)
|
|
655
|
+
const key = part.substring(0, colonIdx).toLowerCase();
|
|
656
|
+
const val = part.substring(colonIdx + 1);
|
|
657
|
+
if (key && val !== undefined)
|
|
658
|
+
extObj[key] = val;
|
|
659
|
+
}
|
|
660
|
+
else if (eqIdx > 0) {
|
|
661
|
+
// key=val form (e.g. tz=+03:00)
|
|
662
|
+
const key = part.substring(0, eqIdx).toLowerCase();
|
|
663
|
+
const val = part.substring(eqIdx + 1);
|
|
664
|
+
if (key && val !== undefined)
|
|
665
|
+
extObj[key] = val;
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
if (Object.keys(extObj).length > 0)
|
|
669
|
+
components.extensions = extObj;
|
|
670
|
+
}
|
|
671
|
+
// ── Context (#C:key=val;key=val) ─────────────────────────────────────────
|
|
672
|
+
if (g.context) {
|
|
673
|
+
const ctx = {};
|
|
674
|
+
g.context.split(";").forEach((part) => {
|
|
675
|
+
part = part.trim();
|
|
676
|
+
if (!part)
|
|
677
|
+
return;
|
|
678
|
+
const eqIdx = part.indexOf("=");
|
|
679
|
+
if (eqIdx > 0) {
|
|
680
|
+
ctx[part.substring(0, eqIdx)] = part.substring(eqIdx + 1);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
if (Object.keys(ctx).length > 0)
|
|
684
|
+
components.context = ctx;
|
|
685
|
+
}
|
|
686
|
+
return components;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Parses a multi-layer location string (before @T:) into component fields.
|
|
690
|
+
* Layers are `;`-separated. Each layer is identified by its prefix token.
|
|
691
|
+
*
|
|
692
|
+
* Supported layers:
|
|
693
|
+
* L:lat,lon[,altm] — GPS
|
|
694
|
+
* L:~|L:-|L:redacted — Privacy markers
|
|
695
|
+
* P:cc=JO,ci=AMM,... — Place (country/city codes and names)
|
|
696
|
+
* S2:token — S2 cell
|
|
697
|
+
* H3:token — H3 cell
|
|
698
|
+
* 3W:word.word.word — What3Words
|
|
699
|
+
* plus:token — Plus Code
|
|
700
|
+
* net:ip4:x.x.x.x — IPv4
|
|
701
|
+
* net:ip6:x::x — IPv6
|
|
702
|
+
* node:name — Logical node/host
|
|
703
|
+
* bldg:name — Building
|
|
704
|
+
* floor:x — Floor
|
|
705
|
+
* room:x — Room
|
|
706
|
+
* door:x — Door
|
|
707
|
+
* zone:x — Zone
|
|
708
|
+
*/
|
|
709
|
+
static _parseLocationLayers(location, components) {
|
|
710
|
+
const layers = location.trim().split(";");
|
|
711
|
+
for (const layer of layers) {
|
|
712
|
+
const l = layer.trim();
|
|
713
|
+
if (!l)
|
|
714
|
+
continue;
|
|
715
|
+
// Privacy shorthand
|
|
716
|
+
if (l === "L:~" || l === "L:hidden") {
|
|
717
|
+
components.isHiddenLocation = true;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (l === "L:-" || l === "L:unknown") {
|
|
721
|
+
components.isUnknownLocation = true;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (l === "L:redacted") {
|
|
725
|
+
components.isRedactedLocation = true;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
// P: Place layer — P:cc=JO,ci=AMM,cn=Jordan,ct=Amman
|
|
729
|
+
if (l.startsWith("P:")) {
|
|
730
|
+
l.slice(2).split(",").forEach((pair) => {
|
|
731
|
+
const eq = pair.indexOf("=");
|
|
732
|
+
if (eq < 1)
|
|
733
|
+
return;
|
|
734
|
+
const k = pair.substring(0, eq).toLowerCase();
|
|
735
|
+
const v = pair.substring(eq + 1);
|
|
736
|
+
if (k === "cc")
|
|
737
|
+
components.placeCountryCode = v;
|
|
738
|
+
else if (k === "cn")
|
|
739
|
+
components.placeCountryName = v;
|
|
740
|
+
else if (k === "ci")
|
|
741
|
+
components.placeCityCode = v;
|
|
742
|
+
else if (k === "ct")
|
|
743
|
+
components.placeCityName = v;
|
|
744
|
+
});
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
// GPS coordinates (L:lat,lon[,alt])
|
|
748
|
+
if (l.startsWith("L:")) {
|
|
749
|
+
const coords = l.slice(2);
|
|
750
|
+
const m = coords.match(/^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(-?\d+(?:\.\d+)?)m?)?$/);
|
|
751
|
+
if (m) {
|
|
752
|
+
components.latitude = parseFloat(m[1]);
|
|
753
|
+
components.longitude = parseFloat(m[2]);
|
|
754
|
+
if (m[3])
|
|
755
|
+
components.altitude = parseFloat(m[3]);
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// Geospatial cells
|
|
760
|
+
if (/^S2:/i.test(l)) {
|
|
761
|
+
components.s2Cell = l.slice(3);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (/^H3:/i.test(l)) {
|
|
765
|
+
components.h3Cell = l.slice(3);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (/^3W:/i.test(l)) {
|
|
769
|
+
components.what3words = l.slice(3);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (/^plus:/i.test(l)) {
|
|
773
|
+
components.plusCode = l.slice(5);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Network
|
|
777
|
+
if (/^net:ip4:/i.test(l)) {
|
|
778
|
+
components.ipv4 = l.slice(8);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (/^net:ip6:/i.test(l)) {
|
|
782
|
+
components.ipv6 = l.slice(8);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (/^node:/i.test(l)) {
|
|
786
|
+
components.nodeName = l.slice(5);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
// Structural
|
|
790
|
+
if (/^bldg:/i.test(l)) {
|
|
791
|
+
components.building = l.slice(5);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (/^floor:/i.test(l)) {
|
|
795
|
+
components.floor = l.slice(6);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (/^room:/i.test(l)) {
|
|
799
|
+
components.room = l.slice(5);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (/^door:/i.test(l)) {
|
|
803
|
+
components.door = l.slice(5);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (/^zone:/i.test(l)) {
|
|
807
|
+
components.zone = l.slice(5);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
// Fallback: generic space anchor (adm:, planet:, legacy strings)
|
|
811
|
+
if (l) {
|
|
812
|
+
components.spaceAnchor = components.spaceAnchor
|
|
813
|
+
? components.spaceAnchor + ";" + l
|
|
814
|
+
: l;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// --- PLUGIN REGISTRY ---
|
|
820
|
+
/** Shared DriverManager instance — use TPS.driverManager for direct access. */
|
|
821
|
+
TPS.driverManager = new DriverManager();
|
|
822
|
+
// --- REGEX (v0.6.0) ---
|
|
823
|
+
// The URI and time regexes are intentionally permissive in the location &
|
|
824
|
+
// extension sections — detailed semantic parsing happens in
|
|
825
|
+
// _mapGroupsToComponents() and the layer parsers below.
|
|
826
|
+
//
|
|
827
|
+
// Structure:
|
|
828
|
+
// tps://[location]/A:[actor]@T:[cal].[tokens];[ext];...#C:[ctx];...
|
|
829
|
+
//
|
|
830
|
+
// The `;` separator is used consistently:
|
|
831
|
+
// - between location layers (before @T:)
|
|
832
|
+
// - between extensions (after T: tokens, before #)
|
|
833
|
+
// - between context key=val pairs (after #C:)
|
|
834
|
+
TPS.REGEX_URI = new RegExp("^tps://" +
|
|
835
|
+
// Location: everything up to optional /A: actor and then @T:
|
|
836
|
+
"(?<location>[^@]+?)" +
|
|
837
|
+
// Optional actor overlay
|
|
838
|
+
"(?:/A:(?<actor>[^@]+))?" +
|
|
839
|
+
// Time section
|
|
840
|
+
"@T:(?<calendar>[a-z]{3,4})" +
|
|
841
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
842
|
+
// Optional signature
|
|
843
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
844
|
+
// Optional extensions (;KEY:val;key=val;...)
|
|
845
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
846
|
+
// Optional context fragment (#C:key=val;...)
|
|
847
|
+
"(?:#C:(?<context>.+))?$");
|
|
848
|
+
TPS.REGEX_TIME = new RegExp("^T:(?<calendar>[a-z]{3,4})" +
|
|
849
|
+
"(?<tokens>(?:\\.[a-z]-?[\\d.]+)*)" +
|
|
850
|
+
"(?:!(?<signature>[^;#]+))?" +
|
|
851
|
+
"(?:;(?<extensions>[^#]+))?" +
|
|
852
|
+
"(?:#C:(?<context>.+))?$");
|
|
853
|
+
// register built-in drivers and set default
|
|
854
|
+
// (tps and gregorian provide canonical conversions before unix)
|
|
855
|
+
TPS.registerDriver(new TpsDriver());
|
|
856
|
+
TPS.registerDriver(new GregorianDriver());
|
|
857
|
+
TPS.registerDriver(new UnixDriver());
|
|
858
|
+
TPS.registerDriver(new PersianDriver());
|
|
859
|
+
TPS.registerDriver(new HijriDriver());
|
|
860
|
+
TPS.registerDriver(new JulianDriver());
|
|
861
|
+
TPS.registerDriver(new HoloceneDriver());
|
|
862
|
+
TPS.registerDriver(new ChineseDriver());
|
|
863
|
+
/**
|
|
864
|
+
* `TpsDate` is a Date-like wrapper with native TPS conversion helpers.
|
|
865
|
+
*
|
|
866
|
+
* It mirrors common JavaScript `Date` construction patterns:
|
|
867
|
+
* - `new TpsDate()`
|
|
868
|
+
* - `new TpsDate(ms)`
|
|
869
|
+
* - `new TpsDate(isoString)`
|
|
870
|
+
* - `new TpsDate(tpsString)`
|
|
871
|
+
* - `new TpsDate(year, monthIndex, day?, hour?, minute?, second?, ms?)`
|
|
872
|
+
*/
|
|
873
|
+
//# sourceMappingURL=index.js.map
|