From 48ab812d053021e2ed49f338ca684f812590a0db Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 17:24:45 +1000 Subject: [PATCH 1/7] standalone format --- package.json | 2 +- packages/library/package.json | 2 +- packages/tempo/.vitepress/config.ts | 13 ++ packages/tempo/README.md | 8 +- packages/tempo/bin/tsconfig.json | 80 +++++++--- packages/tempo/doc/migration-guide.md | 24 +++ packages/tempo/doc/tempo.format.md | 114 +++++++++++++++ packages/tempo/doc/tempo.parse.md | 3 + packages/tempo/importmap.json | 9 +- packages/tempo/package.json | 48 ++++-- packages/tempo/src/core.index.ts | 6 +- .../tempo/src/discrete/discrete.format.ts | 138 ++++++++++++++++++ packages/tempo/src/discrete/discrete.index.ts | 2 + .../discrete.parse.ts} | 67 +++++++-- packages/tempo/src/library.index.ts | 4 +- .../tempo/src/plugin/module/module.format.ts | 83 ----------- packages/tempo/src/support/tempo.runtime.ts | 5 +- packages/tempo/src/tempo.index.ts | 6 +- packages/tempo/src/tempo.parse.ts | 40 ----- packages/tempo/src/tsconfig.json | 4 +- packages/tempo/test/standalone.test.ts | 83 +++++++++++ packages/tempo/test/tsconfig.json | 11 +- packages/tempo/vitest.config.ts | 8 +- vitest.config.ts | 2 + 24 files changed, 565 insertions(+), 197 deletions(-) create mode 100644 packages/tempo/doc/tempo.format.md create mode 100644 packages/tempo/src/discrete/discrete.format.ts create mode 100644 packages/tempo/src/discrete/discrete.index.ts rename packages/tempo/src/{plugin/module/module.parse.ts => discrete/discrete.parse.ts} (88%) delete mode 100644 packages/tempo/src/plugin/module/module.format.ts delete mode 100644 packages/tempo/src/tempo.parse.ts create mode 100644 packages/tempo/test/standalone.test.ts diff --git a/package.json b/package.json index a1eeb23d..785719dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tempo-monorepo", - "version": "2.3.0", + "version": "2.4.0", "private": true, "description": "Magma Computing Monorepo", "repository": { diff --git a/packages/library/package.json b/packages/library/package.json index 73526c11..05be87d4 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/library", - "version": "2.3.0", + "version": "2.4.0", "description": "Shared utility library for Tempo", "author": "Magma Computing Solutions", "license": "MIT", diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index c6cc0981..0c9f85ac 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -36,6 +36,7 @@ export default defineConfig({ items: [ { text: 'Configuration', link: '/doc/tempo.config' }, { text: 'Smart Parsing', link: '/doc/tempo.parse' }, + { text: 'Smart Formatting', link: '/doc/tempo.format' }, { text: 'Modularity', link: '/doc/tempo.modularity' }, { text: 'Layout Patterns', link: '/doc/tempo.layout' }, { text: 'Terms System', link: '/doc/tempo.term' }, @@ -114,6 +115,18 @@ export default defineConfig({ find: /^@magmacomputing\/tempo\/ticker$/, replacement: fileURLToPath(new URL('../dist/plugin/extend/extend.ticker.js', import.meta.url)) }, + { + find: /^@magmacomputing\/tempo\/parse$/, + replacement: fileURLToPath(new URL('../dist/discrete/discrete.parse.js', import.meta.url)) + }, + { + find: /^@magmacomputing\/tempo\/format$/, + replacement: fileURLToPath(new URL('../dist/discrete/discrete.format.js', import.meta.url)) + }, + { + find: /^@magmacomputing\/tempo\/discrete$/, + replacement: fileURLToPath(new URL('../dist/discrete/discrete.index.js', import.meta.url)) + }, { find: /^@magmacomputing\/tempo$/, replacement: fileURLToPath(new URL('../dist/tempo.index.js', import.meta.url)) diff --git a/packages/tempo/README.md b/packages/tempo/README.md index aee8c3bd..1a309f25 100644 --- a/packages/tempo/README.md +++ b/packages/tempo/README.md @@ -44,11 +44,11 @@ const event = new Tempo('next Friday 3pm'); const reminder = event.add({ hours: 2 }).set({ minute: 0 }); // โณ Comparative Durations -const diff = event.until('next month'); -console.log(diff.toString()); // e.g. P3W2D +const diff = event.until('xmas'); +console.log(diff.iso); // e.g. P3W2D // ๐Ÿ“ Beautiful Formatting -console.log(event.format('{mon} {day}, {year}')); // e.g. Oct 24, 2026 +console.log(event.format('{mon} {day}, {yyyy}')); // e.g. Oct 24, 2026 ``` --- @@ -89,7 +89,7 @@ For granular "Lite" builds, see the [Full Installation Guide](https://magmacompu ## โœจ Why Tempo? * **๐Ÿ—๏ธ Future Standard**: Built natively on the TC39 `Temporal` proposal. Inherit the reliability of the future standard. -* **๐Ÿ—ฃ๏ธ Natural Language**: Resolve complex terms like `#friday.last` or "two days ago" with zero configuration. +* **๐Ÿ—ฃ๏ธ Natural Language**: Resolve complex terms like `#quarter.last` or "two days ago" with zero configuration. * **๐Ÿ”„ Cycle Persistence**: Shift by semantic terms (Quarters, Seasons) while preserving your relative day-of-period offset. * **โšก Zero-Cost Parsing**: Lazy evaluation and smart matching ensure instantiation overhead is near-zero. * **๐Ÿ›ก๏ธ Monorepo Resilient**: Built for stability in complex environments with proxy-protected registries. diff --git a/packages/tempo/bin/tsconfig.json b/packages/tempo/bin/tsconfig.json index 3235f8ef..44cc5e82 100644 --- a/packages/tempo/bin/tsconfig.json +++ b/packages/tempo/bin/tsconfig.json @@ -4,30 +4,72 @@ "rootDir": "..", "noEmit": true, "composite": true, - "types": ["node"], + "types": [ + "node" + ], "paths": { - "#library": ["../../library/src/common.index.ts"], - "#library/*": ["../../library/src/common/*"], - "#browser/*": ["../../library/src/browser/*"], - "#server/*": ["../../library/src/server/*"], - "#tempo": ["../src/tempo.index.ts"], - "#tempo/core": ["../src/core.index.ts"], - "#tempo/duration": ["../src/plugin/module/module.duration.ts"], - "#tempo/format": ["../src/plugin/module/module.format.ts"], - "#tempo/ticker": ["../src/plugin/extend/extend.ticker.ts"], - "#tempo/term/*": ["../src/plugin/term/term.*.ts"], - "#tempo/plugin/plugin.*.js": ["../src/plugin/plugin.*.ts"], - "#tempo/plugin/extend.*.js": ["../src/plugin/extend/extend.*.ts"], - "#tempo/plugin/module.*.js": ["../src/plugin/module/module.*.ts"], - "#tempo/plugin/term.*.js": ["../src/plugin/term/term.*.ts"], - "#tempo/*.js": ["../src/*.ts"], - "#tempo/*": ["../src/*"] + "#library": [ + "../../library/src/common.index.ts" + ], + "#library/*": [ + "../../library/src/common/*" + ], + "#browser/*": [ + "../../library/src/browser/*" + ], + "#server/*": [ + "../../library/src/server/*" + ], + "#tempo": [ + "../src/tempo.index.ts" + ], + "#tempo/core": [ + "../src/core.index.ts" + ], + "#tempo/format": [ + "../src/discrete/discrete.format.ts" + ], + "#tempo/parse": [ + "../src/discrete/discrete.parse.ts" + ], + "#tempo/discrete": [ + "../src/discrete/discrete.index.ts" + ], + "#tempo/ticker": [ + "../src/plugin/extend/extend.ticker.ts" + ], + "#tempo/term/*": [ + "../src/plugin/term/term.*.ts" + ], + "#tempo/duration": [ + "../src/plugin/module/module.duration.ts" + ], + "#tempo/plugin/plugin.*.js": [ + "../src/plugin/plugin.*.ts" + ], + "#tempo/plugin/extend.*.js": [ + "../src/plugin/extend/extend.*.ts" + ], + "#tempo/plugin/module.*.js": [ + "../src/plugin/module/module.*.ts" + ], + "#tempo/plugin/term.*.js": [ + "../src/plugin/term/term.*.ts" + ], + "#tempo/*.js": [ + "../src/*.ts" + ], + "#tempo/*": [ + "../src/*" + ] } }, "include": [ "**/*.ts" ], "references": [ - { "path": "../src/tsconfig.json" } + { + "path": "../src/tsconfig.json" + } ] -} +} \ No newline at end of file diff --git a/packages/tempo/doc/migration-guide.md b/packages/tempo/doc/migration-guide.md index 94eb9e15..a0970edd 100644 --- a/packages/tempo/doc/migration-guide.md +++ b/packages/tempo/doc/migration-guide.md @@ -43,5 +43,29 @@ t.set({ start: '#quarter' }); t.add({ '#quarter': 2 }); ``` +## ๐Ÿš€ Tempo v2.4.0: Standalone Utilities & Path Deprecation + +Tempo v2.4.0 introduces a new `discrete/` folder for standalone utilities (`parse` and `format`). + +### ๐Ÿ› ๏ธ Standalone Imports +You can now import lightweight, tree-shakable versions of our parsing and formatting engines without the `Tempo` class: +```javascript +import { parse } from '@magmacomputing/tempo/parse'; +import { format } from '@magmacomputing/tempo/format'; +``` + +### โš ๏ธ Deprecated Paths +We have reorganized the internal file structure to optimize for standalone usage. The following internal paths are now **deprecated** and will be removed in a future release: + +* โŒ `@magmacomputing/tempo/module/parse` +* โŒ `@magmacomputing/tempo/module/format` + +**Action Required**: +1. **Do not use `dist/` paths** in your imports. These are unstable and may change. +2. **Use package subpath maps**: Update your imports to use the official entry points: + * โœ… `@magmacomputing/tempo/parse` + * โœ… `@magmacomputing/tempo/format` +3. **Check your Import Maps**: If you use browser-side import maps, ensure they point to the new package-mapped locations rather than internal `plugin/module/` paths. + ## ๐Ÿงช Testing and Stability v2.x has been hardened with a 100% pass rate on our regression suite. If you were relying on undocumented "quirks" or bugs in v1.x parsing, you may find that v2.x is more strict and deterministic. diff --git a/packages/tempo/doc/tempo.format.md b/packages/tempo/doc/tempo.format.md new file mode 100644 index 00000000..4c34b6bd --- /dev/null +++ b/packages/tempo/doc/tempo.format.md @@ -0,0 +1,114 @@ +# Smart Formatting Guide + +Tempo provides a powerful token-based formatting engine that goes beyond the standard ISO strings of native `Temporal`. + +## ๐Ÿš€ Standalone Formatting (Zero-Overhead) + +If you have a native `Temporal.ZonedDateTime` and want to format it using Tempo's readable tokens, you can use the standalone `format` function. This allows you to use Tempo's formatting logic without importing the full `Tempo` class. + +```typescript +import { format } from '@magmacomputing/tempo/format'; + +const zdt = Temporal.Now.zonedDateTimeISO(); +const str = format(zdt, '{mon} {day}, {yyyy}'); + +console.log(str); // e.g., "October 24, 2026" +``` + +> [!IMPORTANT] +> **Terms and Standalone Formatting**: When using `format()` with native `Temporal` objects, **Terms** (tokens starting with `#`) are not resolved. To use Terms resolution in your format strings, you must either pass a `Tempo` instance to the `format()` utility or use the class-based `.format()` method. + +### Supported Input Types +The engine can interpret: +* **Temporal Objects**: `ZonedDateTime`, `Instant` (auto-projected to ZDT), `PlainDate`, `PlainDateTime`. +* **Tempo Instances**: Any instance of the `Tempo` class. +* **ISO Strings**: Valid Temporal ISO-8601 strings. +* **Defaults**: If no object is provided, it defaults to **Now** in the configured timezone. + +--- + +## ๐Ÿ—๏ธ Class-Based Formatting + +When using the `Tempo` class, the `.format()` method is available on every instance. + +```typescript +import { Tempo } from '@magmacomputing/tempo'; + +const t = new Tempo('2026-10-24T15:30:00'); +console.log(t.format('display')); // Sat, 24 Oct 2026 (using a named format alias) +``` + +### Named Formats +Tempo comes with several pre-configured format aliases. You can also define your own globally during initialization. + +```typescript +Tempo.init({ + formats: { + 'fancy': '{mon} the {dd}th day of {yyyy}' + } +}); + +const t = new Tempo('2026-10-24'); +console.log(t.format('fancy')); // October the 24th day of 2026 +``` + +--- + +## ๐Ÿงฉ Modularity: Core vs. Full + +Like the parsing engine, the formatting engine is modular: + +| Version | Formatting Status | +| :--- | :--- | +| **Tempo Full** | **Built-in**. Works out of the box. | +| **Tempo Core** | **Opt-in**. You must call `Tempo.extend(FormatModule)` to enable `.format()`. | + +### Enabling Formatting in Core +If you are using `@magmacomputing/tempo/core`, you must explicitly register the formatting engine: + +```typescript +import { Tempo } from '@magmacomputing/tempo/core'; +import { FormatModule } from '@magmacomputing/tempo/format'; + +Tempo.extend(FormatModule); +``` + +--- + +## ๐Ÿ”  Supported Tokens + +| Token | Description | Example | +| :--- | :--- | :--- | +| `{yyyy}` | 4-digit Year | `2026` | +| `{yy}` | 2-digit Year | `26` | +| `{mon}` | Full Month Name | `October` | +| `{mmm}` | Short Month Name | `Oct` | +| `{mm}` | 2-digit Month | `10` | +| `{dd}` | 2-digit Day | `24` | +| `{wkd}` | Full Weekday Name | `Saturday` | +| `{www}` | Short Weekday Name | `Sat` | +| `{hh}` | 2-digit Hour (24h) | `15` | +| `{HH}` | 12-hour clock | `3` | +| `{mer}` | am/pm marker | `pm` | +| `{MER}` | AM/PM marker | `PM` | +| `{mi}` | Minutes | `30` | +| `{ss}` | Seconds | `45` | +| `{ff}` | Fractional Seconds | `123456789` | +| `{ts}` | Unix Timestamp | `1792843200000` | +| `{tz}` | Time Zone ID | `Australia/Sydney` | + +### ๐Ÿ”„ Automatic Meridiem +If your format string contains `{HH}` (12-hour clock) but lacks a `{mer}` or `{MER}` token, Tempo will automatically append `{mer}` to the end of the last time component to ensure the time remains unambiguous. + +```typescript +t.format('{HH}:{mi}'); // "3:30pm" (auto-appended pm) +``` + +### ๐Ÿ”ข Numeric Resolution +If your format string consists *only* of numeric tokens (e.g., `{yyyy}{mm}{dd}`), the `format()` function will return a **Number** instead of a string. This is useful for generating sortable keys or IDs. + +```typescript +const key = t.format('{yyyy}{mm}{dd}'); +console.log(typeof key); // "number" +console.log(key); // 20261024 +``` diff --git a/packages/tempo/doc/tempo.parse.md b/packages/tempo/doc/tempo.parse.md index e1b744aa..ba03e554 100644 --- a/packages/tempo/doc/tempo.parse.md +++ b/packages/tempo/doc/tempo.parse.md @@ -20,6 +20,9 @@ console.log(zdt.toString()); // 2026-04-28T15:00:00+10:00[Australia/Sydney] * **Temporal Native**: Perfect for projects that already use native `Temporal` objects but need a friendlier input layer for users. * **Strict by Default**: The standalone function defaults to `mode: 'strict'`, ensuring that it won't "guess" if the input is ambiguous. +> [!IMPORTANT] +> **Terms and Standalone Parsing**: Standalone `parse()` supports all standard natural language patterns but does not resolve **Global Terms** or aliases unless they are part of the core pattern registry. For full term-dispatch and instance-aware resolution, use the `Tempo` class. + ### โš ๏ธ Standalone vs. Class-Based It is important to understand the trade-offs when using the lightweight `parse()` function: diff --git a/packages/tempo/importmap.json b/packages/tempo/importmap.json index 9d4b7ae0..6e494193 100644 --- a/packages/tempo/importmap.json +++ b/packages/tempo/importmap.json @@ -6,11 +6,12 @@ "@magmacomputing/tempo/core": "./dist/core.index.js", "@magmacomputing/tempo/ticker": "./dist/plugin/extend/extend.ticker.js", "@magmacomputing/tempo/duration": "./dist/plugin/module/module.duration.js", - "@magmacomputing/tempo/format": "./dist/plugin/module/module.format.js", "@magmacomputing/tempo/mutate": "./dist/plugin/module/module.mutate.js", - "@magmacomputing/tempo/parse": "./dist/plugin/module/module.parse.js", "@magmacomputing/tempo/plugin": "./dist/plugin/plugin.index.js", - "@magmacomputing/tempo/enums": "./dist/tempo.enum.js", + "@magmacomputing/tempo/format": "./dist/discrete/discrete.format.js", + "@magmacomputing/tempo/parse": "./dist/discrete/discrete.parse.js", + "@magmacomputing/tempo/discrete": "./dist/discrete/discrete.index.js", + "@magmacomputing/tempo/enums": "./dist/support/tempo.enum.js", "@magmacomputing/tempo/library": "./dist/library.index.js" } -} +} \ No newline at end of file diff --git a/packages/tempo/package.json b/packages/tempo/package.json index ebb8d442..9c6cd0e3 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -1,6 +1,6 @@ { "name": "@magmacomputing/tempo", - "version": "2.3.0", + "version": "2.4.0", "description": "The Tempo core library", "author": "Magma Computing Solutions", "license": "MIT", @@ -50,14 +50,6 @@ "development": "./src/plugin/module/module.duration.ts", "default": "./dist/plugin/module/module.duration.js" }, - "#tempo/format": { - "development": "./src/plugin/module/module.format.ts", - "default": "./dist/plugin/module/module.format.js" - }, - "#tempo/parse": { - "development": "./src/tempo.parse.ts", - "default": "./dist/tempo.parse.js" - }, "#tempo/mutate": { "development": "./src/plugin/module/module.mutate.ts", "default": "./dist/plugin/module/module.mutate.js" @@ -102,6 +94,18 @@ "development": "./src/support/*.ts", "default": "./dist/support/*.js" }, + "#tempo/parse": { + "development": "./src/discrete/discrete.parse.ts", + "default": "./dist/discrete/discrete.parse.js" + }, + "#tempo/format": { + "development": "./src/discrete/discrete.format.ts", + "default": "./dist/discrete/discrete.format.js" + }, + "#tempo/discrete": { + "development": "./src/discrete/discrete.index.ts", + "default": "./dist/discrete/discrete.index.js" + }, "#tempo/*.js": { "development": "./src/*.ts", "default": "./dist/*.js" @@ -120,6 +124,14 @@ "types": "./dist/plugin/extend/extend.*.d.ts", "import": "./dist/plugin/extend/extend.*.js" }, + "./module/parse": { + "types": "./dist/discrete/discrete.parse.d.ts", + "import": "./dist/discrete/discrete.parse.js" + }, + "./module/format": { + "types": "./dist/discrete/discrete.format.d.ts", + "import": "./dist/discrete/discrete.format.js" + }, "./module/*": { "types": "./dist/plugin/module/module.*.d.ts", "import": "./dist/plugin/module/module.*.js" @@ -148,17 +160,21 @@ "types": "./dist/plugin/module/module.duration.d.ts", "import": "./dist/plugin/module/module.duration.js" }, - "./format": { - "types": "./dist/plugin/module/module.format.d.ts", - "import": "./dist/plugin/module/module.format.js" - }, "./mutate": { "types": "./dist/plugin/module/module.mutate.d.ts", "import": "./dist/plugin/module/module.mutate.js" }, + "./discrete": { + "types": "./dist/discrete/discrete.index.d.ts", + "import": "./dist/discrete/discrete.index.js" + }, + "./format": { + "types": "./dist/discrete/discrete.format.d.ts", + "import": "./dist/discrete/discrete.format.js" + }, "./parse": { - "types": "./dist/tempo.parse.d.ts", - "import": "./dist/tempo.parse.js" + "types": "./dist/discrete/discrete.parse.d.ts", + "import": "./dist/discrete/discrete.parse.js" }, "./library": { "types": "./dist/library.index.d.ts", @@ -211,7 +227,7 @@ }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.3.0", + "@magmacomputing/library": "2.4.0", "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", diff --git a/packages/tempo/src/core.index.ts b/packages/tempo/src/core.index.ts index 8f0a00e5..b127d1d7 100644 --- a/packages/tempo/src/core.index.ts +++ b/packages/tempo/src/core.index.ts @@ -1,9 +1,9 @@ import { Tempo } from './tempo.class.js'; - -export * from './tempo.class.js'; import { getRuntime } from '#tempo/support'; -export { enums, Token, Snippet, Match, Default, Guard } from '#tempo/support'; getRuntime().modules['Tempo'] = Tempo; +export { enums, Token, Snippet, Match, Default, Guard } from '#tempo/support'; + +export * from './tempo.class.js'; export default Tempo; diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts new file mode 100644 index 00000000..369e4974 --- /dev/null +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -0,0 +1,138 @@ +import { pad } from '#library/string.library.js'; +import { ifNumeric } from '#library/coercion.library.js'; +import { isString, isObject, isZonedDateTime, isInstant, isUndefined, isDefined } from '#library/type.library.js'; + +import { isTempo, enums, Match, getRuntime } from '#tempo/support'; +import { defineInterpreterModule } from '../plugin/plugin.util.js'; +import { NumericPattern } from '../support/tempo.enum.js'; +import type { Tempo } from '../tempo.class.js'; + +declare module '../tempo.class.js' { + interface Tempo { + /** applies a format to the instance. */ format(fmt: any): any; + } +} + +/** + * Standalone Formatter + * Returns a formatted string or number from a Temporal.ZonedDateTime or Tempo instance. + * + * @example + * import { format } from '@magmacomputing/tempo/format'; + * const str = format(Temporal.Now.zonedDateTimeISO(), '{yyyy}-{mm}-{dd}'); + * + * // Proxy usage (zero or one argument) + * const weekDate = format(zdt).weekDate; + * const stamp = format().logStamp; // defaults to 'Now' + */ +export function format(obj?: Temporal.ZonedDateTime | any): any; +export function format(obj: Temporal.ZonedDateTime | any, fmt: NumericPattern): number; +export function format(obj: Temporal.ZonedDateTime | any, fmt: string | symbol): string; +export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol): string | number | any { + const state = getRuntime().state; + const config = isTempo(obj) ? obj.config : state?.config; + const tz = config?.timeZone ?? 'UTC'; + + const zdt = isTempo(obj) + ? obj.toDateTime() + : (isZonedDateTime(obj) + ? obj + : (isInstant(obj) + ? (obj as Temporal.Instant).toZonedDateTimeISO(tz) + : (isUndefined(obj) + ? Temporal.Now.zonedDateTimeISO(tz) + : obj))); + + if (isUndefined(fmt)) { + const formats = config?.formats ?? enums.FORMAT; + return new Proxy({} as any, { + get(_, prop: string) { + if (!isString(prop) || prop === 'constructor' || prop === 'then') return undefined; + return format(zdt, prop); + }, + ownKeys() { + return Object.keys(formats); + }, + getOwnPropertyDescriptor(t, prop: string) { + if (isString(prop) && isDefined(formats[prop])) { + return { + enumerable: true, + configurable: true + }; + } + return undefined; + } + }); + } + + if (!isZonedDateTime(zdt)) return ''; + + const formats = config?.formats ?? enums.FORMAT; + + let template = (isString(fmt) && formats && (formats as any).has(fmt as string)) + ? (formats as Record)[fmt as string] + : String(fmt); + + // auto-meridiem: if {HH} is present and {mer} is absent, append it after the last time component + if (template.includes('{HH}') && !template.includes('{mer}') && !template.includes('{MER}')) { + const index = Math.max(template.lastIndexOf('{HH}'), template.lastIndexOf('{mi}'), template.lastIndexOf('{ss}')); + if (index !== -1) { + const end = template.indexOf('}', index) + 1; + template = template.slice(0, end) + '{mer}' + template.slice(end); + } + } + + const result = template.replaceAll(new RegExp(Match.braces), (_match: string, token: string) => { + switch (token) { + case 'yyyy': return pad(zdt.year, 4); + case 'yy': return pad(zdt.year % 100); + case 'yw': return pad(zdt.yearOfWeek, 4); + case 'yyww': return pad(zdt.yearOfWeek, 4) + pad(zdt.weekOfYear); + case 'mm': return pad(zdt.month); + case 'mon': return enums.MONTHS.keyOf(zdt.month as any); + case 'mmm': return enums.MONTH.keyOf(zdt.month as any); + case 'dd': return pad(zdt.day); + case 'day': return zdt.day.toString(); + case 'dow': return zdt.dayOfWeek.toString(); + case 'wkd': return enums.WEEKDAYS.keyOf(zdt.dayOfWeek as any); + case 'www': return enums.WEEKDAY.keyOf(zdt.dayOfWeek as any); + case 'ww': return pad(zdt.weekOfYear); + case 'hh': return pad(zdt.hour); + case 'HH': return pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); + case 'mer': return zdt.hour >= 12 ? 'pm' : 'am'; + case 'MER': return zdt.hour >= 12 ? 'PM' : 'AM'; + case 'mi': return pad(zdt.minute); + case 'ss': return pad(zdt.second); + case 'ms': return pad(zdt.millisecond, 3); + case 'us': return pad(zdt.microsecond, 3); + case 'ns': return pad(zdt.nanosecond, 3); + case 'ff': return `${pad(zdt.millisecond, 3)}${pad(zdt.microsecond, 3)}${pad(zdt.nanosecond, 3)}`; + case 'hhmiss': return `${pad(zdt.hour)}${pad(zdt.minute)}${pad(zdt.second)}`; + case 'ts': return ((config?.timeStamp ?? 'ms') === 'ss') + ? Math.trunc(zdt.epochMilliseconds / 1000).toString() + : zdt.epochMilliseconds.toString(); + case 'nano': return zdt.epochNanoseconds.toString(); + case 'tz': return zdt.timeZoneId; + default: { + if (token.startsWith('#') && isTempo(obj)) { + const res = (obj as Tempo).term[token.slice(1)]; + if (isObject(res)) return res.label ?? res.key ?? `{${token}}`; + return res ?? `{${token}}`; + } + return `{${token}}`; + } + } + }); + + const isExplicitlyNumeric = (NumericPattern as readonly string[]).includes(template as any); + return (isExplicitlyNumeric ? ifNumeric(result) : result) as any; +} + +/** + * Format Module Plugin + */ +// @ts-ignore +export const FormatModule: Tempo.Module = defineInterpreterModule('FormatModule', function (this: Tempo, fmt: any) { + if (!this.isValid) return '' as unknown as any; + return format(this, fmt); +}); diff --git a/packages/tempo/src/discrete/discrete.index.ts b/packages/tempo/src/discrete/discrete.index.ts new file mode 100644 index 00000000..797dbe5d --- /dev/null +++ b/packages/tempo/src/discrete/discrete.index.ts @@ -0,0 +1,2 @@ +export { parse } from './discrete.parse.js'; +export { format } from './discrete.format.js'; diff --git a/packages/tempo/src/plugin/module/module.parse.ts b/packages/tempo/src/discrete/discrete.parse.ts similarity index 88% rename from packages/tempo/src/plugin/module/module.parse.ts rename to packages/tempo/src/discrete/discrete.parse.ts index 71833c26..ca9441b7 100644 --- a/packages/tempo/src/plugin/module/module.parse.ts +++ b/packages/tempo/src/discrete/discrete.parse.ts @@ -1,19 +1,23 @@ import '#library/temporal.polyfill.js'; -import { asType, isNull, isString, isObject, isZonedDateTime, isDefined, isUndefined, isIntegerLike, isEmpty, type TypeValue } from '#library/type.library.js'; +import { asType, isNull, isString, isObject, isZonedDateTime, isInstant, isDefined, isUndefined, isIntegerLike, isEmpty, type TypeValue } from '#library/type.library.js'; import { asArray, asInteger, isNumeric } from '#library/coercion.library.js'; import { instant } from '#library/temporal.library.js'; import { ownKeys, ownEntries } from '#library/primitive.library.js'; +import { markConfig } from '#library/symbol.library.js'; -import { sym, enums, isTempo, Match, getRuntime } from '../../support/support.index.js'; -import { prefix, parseWeekday, parseDate, parseTime, parseZone } from './module.lexer.js'; -import { resolveTermMutation, resolveTermValue } from './module.term.js'; -import { compose } from './module.composer.js'; +import { sym, enums, isTempo, Match, getRuntime } from '../support/support.index.js'; +import { init, extendState } from '../support/tempo.init.js'; +import { setPatterns } from '../support/tempo.util.js'; -import { getRange, getTermRange } from '../term.util.js'; -import { defineInterpreterModule } from '../plugin.util.js'; -import type { Range, ResolvedRange } from '../plugin.type.js'; -import type { Tempo } from '../../tempo.class.js'; -import * as t from '../../tempo.type.js'; +import { prefix, parseWeekday, parseDate, parseTime, parseZone } from '../plugin/module/module.lexer.js'; +import { resolveTermMutation, resolveTermValue } from '../plugin/module/module.term.js'; +import { compose } from '../plugin/module/module.composer.js'; + +import { getRange, getTermRange } from '../plugin/term.util.js'; +import { defineInterpreterModule } from '../plugin/plugin.util.js'; +import type { Range, ResolvedRange } from '../plugin/plugin.type.js'; +import type { Tempo } from '../tempo.class.js'; +import * as t from '../tempo.type.js'; /** * Internal helper to resolve state from 'this' context or first argument @@ -39,6 +43,14 @@ const _ParseEngine = { return undefined as any; } + if (!term && (isZonedDateTime(tempo) || isInstant(tempo))) { + const { config } = state; + const tz = isString(config.timeZone) ? config.timeZone : (config.timeZone as any).id ?? (config.timeZone as any).timeZoneId; + const cal = isString(config.calendar) ? config.calendar : (config.calendar as any).id ?? (config.calendar as any).calendarId; + const dt = isZonedDateTime(tempo) ? tempo : (tempo as Temporal.Instant).toZonedDateTimeISO(tz); + return dt.withTimeZone(tz).withCalendar(cal); + } + state.parseDepth = (state.parseDepth ?? 0) + 1; const isRoot = state.parseDepth === 1; if (isRoot) state.matches = []; @@ -46,7 +58,7 @@ const _ParseEngine = { try { const { config } = state; - const val = dateTime ?? state.anchor ?? (isTempo(tempo) ? (tempo as any).toDateTime() : (isZonedDateTime(tempo) ? tempo : undefined)); + const val = dateTime ?? state.anchor ?? (isTempo(tempo) ? (tempo as any).toDateTime() : (isZonedDateTime(tempo) ? tempo : (isInstant(tempo) ? tempo.toZonedDateTimeISO(config.timeZone) : undefined))); const basis = isDefined(val) ? val : instant().toZonedDateTimeISO(config.timeZone); const tz = isTempo(basis) ? (basis as any).tz : (isZonedDateTime(basis) ? basis.timeZoneId : config.timeZone); @@ -357,7 +369,6 @@ const _ParseEngine = { const isGlobal = key.startsWith('g'); const isLocal = key.startsWith('l'); const idx = +key.substring((isGlobal || isLocal) ? 4 : 3); - // const src = isGlobal ? (isEvent ? (getRuntime().modules['Tempo'] as any)[sym.$Internal]().parse.event : (getRuntime().modules['Tempo'] as any)[sym.$Internal]().parse.period) : (isEvent ? state.parse.event : state.parse.period); const globalParse = isGlobal ? (TempoClass as any)?.[sym.$Internal]?.().parse : undefined; const src = isGlobal @@ -515,3 +526,35 @@ const isFunction = (v: any): v is Function => typeof v === 'function'; * Decouples date-string interpretation from the core class. */ export const ParseModule = defineInterpreterModule('ParseModule', ParseEngine); + +/** + * Standalone Parser + * Returns a Temporal.ZonedDateTime from a variety of inputs. + * + * @example + * import { parse } from '@magmacomputing/tempo/parse'; + * const zdt = parse('2026-04-22'); + */ +export function parse(value: t.DateTime, options: t.Options = {}): Temporal.ZonedDateTime { + const runtime = getRuntime(); + const globalState = runtime.state ?? init(); + + // Create a local state shadowed from the global state + const state: t.Internal.State = { + config: markConfig(Object.create(globalState.config)), + parse: markConfig(Object.create(globalState.parse)) + } as t.Internal.State; + + // Standalone parsing defaults to 'strict' mode + const localOptions = { ...options }; + localOptions.mode ??= 'strict'; + + // Apply options + extendState(state, localOptions); + + // Compile RegEx patterns + setPatterns(state); + + // Execute the parse + return ParseEngine.parse(state, value); +} diff --git a/packages/tempo/src/library.index.ts b/packages/tempo/src/library.index.ts index e369d3f9..9331f94b 100644 --- a/packages/tempo/src/library.index.ts +++ b/packages/tempo/src/library.index.ts @@ -1,6 +1,6 @@ /** - * # Tempo Library - * This is a secondary entry point for the "Tempo Utility Stack". + * # Magma Library + * This is a secondary entry point for the "Magma Utility Stack". * It provides curated access to the specific utilities Tempo uses under the hood. */ diff --git a/packages/tempo/src/plugin/module/module.format.ts b/packages/tempo/src/plugin/module/module.format.ts deleted file mode 100644 index f71386b6..00000000 --- a/packages/tempo/src/plugin/module/module.format.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isString, isObject } from '#library/type.library.js'; -import { pad } from '#library/string.library.js'; -import { ifNumeric } from '#library/coercion.library.js'; - -import { defineInterpreterModule } from '../plugin.util.js'; -import { Match } from '../../support/tempo.default.js'; -import { NumericPattern } from '../../support/tempo.enum.js'; -import type { Tempo } from '../../tempo.class.js'; - -declare module '../../tempo.class.js' { - interface Tempo { - /** applies a format to the instance. */ format(fmt: any): any; - } -} - -/** - * Externalized implementation of `Tempo.format()` - * (moved out of tempo.class.ts to reduce core bundle size) - */ -function format(this: Tempo, fmt: any) { - if (!this.isValid) - return '' as unknown as any; - - const obj = this.config.formats; - let template = (isString(fmt) && obj.has(fmt)) - ? (obj as Record)[fmt] - : String(fmt); - - // auto-meridiem: if {HH} is present and {mer} is absent, append it after the last time component - if (template.includes('{HH}') && !template.includes('{mer}') && !template.includes('{MER}')) { - const index = Math.max(template.lastIndexOf('{HH}'), template.lastIndexOf('{mi}'), template.lastIndexOf('{ss}')); - if (index !== -1) { - const end = template.indexOf('}', index) + 1; - template = template.slice(0, end) + '{mer}' + template.slice(end); - } - } - - const result = template.replaceAll(new RegExp(Match.braces), (_match: string, token: string) => { - switch (token) { - case 'yw': return pad(this.yw, 4); - case 'yyww': return pad(this.yw, 4) + pad(this.ww); - case 'yyyy': return pad(this.yy, 4); - case 'yy': return pad(this.yy % 100); - case 'mon': return this.mon; - case 'mmm': return this.mmm; - case 'mm': return pad(this.mm); - case 'dd': return pad(this.dd); - case 'day': return this.day.toString(); - case 'dow': return this.dow.toString(); - case 'wkd': return this.wkd; - case 'www': return this.www; - case 'ww': return pad(this.ww); - case 'hh': return pad(this.hh); - case 'HH': return pad(this.hh > 12 ? this.hh % 12 : this.hh || 12); - case 'mer': return this.hh >= 12 ? 'pm' : 'am'; - case 'MER': return this.hh >= 12 ? 'PM' : 'AM'; - case 'mi': return pad(this.mi); - case 'ss': return pad(this.ss); - case 'ms': return pad(this.ms, 3); - case 'us': return pad(this.us, 3); - case 'ns': return pad(this.ns, 3); - case 'ff': return `${pad(this.ms, 3)}${pad(this.us, 3)}${pad(this.ns, 3)}`; - case 'hhmiss': return `${pad(this.hh)}${pad(this.mi)}${pad(this.ss)}`; - case 'ts': return this.ts.toString(); - case 'nano': return this.nano.toString(); - case 'tz': return this.tz; - default: { - if (token.startsWith('#')) { - const res = this.term[token.slice(1)]; - if (isObject(res)) return res.label ?? res.key ?? `{${token}}`; - return res ?? `{${token}}`; - } - return `{${token}}`; - } - } - }); - - const isExplicitlyNumeric = (NumericPattern as readonly string[]).includes(template as any); - return (isExplicitlyNumeric ? ifNumeric(result) : result) as any; -} - -// @ts-ignore -export const FormatModule: Tempo.Module = defineInterpreterModule('FormatModule', format); diff --git a/packages/tempo/src/support/tempo.runtime.ts b/packages/tempo/src/support/tempo.runtime.ts index c769fd91..752848be 100644 --- a/packages/tempo/src/support/tempo.runtime.ts +++ b/packages/tempo/src/support/tempo.runtime.ts @@ -147,9 +147,8 @@ export function getRuntime(): TempoRuntime { configurable: false, writable: false, }); - } else { - // Cannot overwrite a non-configurable global, use local fallback - localFallbackRuntime = rt; + } else { // Cannot overwrite a non-configurable global + localFallbackRuntime = rt; // use local fallback } } diff --git a/packages/tempo/src/tempo.index.ts b/packages/tempo/src/tempo.index.ts index 66d0b8b0..5b6d2929 100644 --- a/packages/tempo/src/tempo.index.ts +++ b/packages/tempo/src/tempo.index.ts @@ -19,7 +19,9 @@ onRegistryReset(() => { Tempo.extend(core); -export * from './tempo.class.js'; -export { parse } from './tempo.parse.js'; +export { parse } from './discrete/discrete.parse.js'; +export { format } from './discrete/discrete.format.js'; export { enums }; + +export * from './tempo.class.js'; export default Tempo; diff --git a/packages/tempo/src/tempo.parse.ts b/packages/tempo/src/tempo.parse.ts deleted file mode 100644 index d3a41637..00000000 --- a/packages/tempo/src/tempo.parse.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { markConfig } from '#library/symbol.library.js'; -import { getRuntime } from './support/tempo.runtime.js'; -import { init, extendState } from './support/tempo.init.js'; -import { ParseEngine } from './plugin/module/module.parse.js'; -import { setPatterns } from './support/tempo.util.js'; -import type { DateTime, Options, Internal } from './tempo.type.js'; - -export * from './plugin/module/module.parse.js'; - -/** - * Standalone Smart Parser - * Returns a native Temporal.ZonedDateTime without requiring the full Tempo class. - * - * @example - * import { parse } from '@magmacomputing/tempo/parse'; - * const zdt = parse('tomorrow', { timeZone: 'Europe/Paris' }); - */ -export function parse(value: DateTime, options: Options = {}): Temporal.ZonedDateTime { - const runtime = getRuntime(); - const globalState = runtime.state ?? init(); - - // Create a local state shadowed from the global state - const state: Internal.State = { - config: markConfig(Object.create(globalState.config)), - parse: markConfig(Object.create(globalState.parse)) - } as Internal.State; - - // Standalone parsing defaults to 'strict' mode - const localOptions = { ...options }; - localOptions.mode ??= 'strict'; - - // Apply options - extendState(state, localOptions); - - // Compile RegEx patterns - setPatterns(state); - - // Execute the parse - return ParseEngine.parse(state, value); -} diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json index 4e06f9dd..a2846ca4 100644 --- a/packages/tempo/src/tsconfig.json +++ b/packages/tempo/src/tsconfig.json @@ -14,8 +14,10 @@ "#server/*": ["../../library/src/server/*"], "#tempo": ["./tempo.index.ts"], "#tempo/core": ["./core.index.ts"], + "#tempo/parse": ["./discrete/discrete.parse.ts"], + "#tempo/format": ["./discrete/discrete.format.ts"], + "#tempo/discrete": ["./discrete/discrete.index.ts"], "#tempo/duration": ["./plugin/module/module.duration.ts"], - "#tempo/format": ["./plugin/module/module.format.ts"], "#tempo/ticker": ["./plugin/extend/extend.ticker.ts"], "#tempo/term/*": ["./plugin/term/term.*.ts"], "#tempo/support": ["./support/support.index.ts"], diff --git a/packages/tempo/test/standalone.test.ts b/packages/tempo/test/standalone.test.ts new file mode 100644 index 00000000..451728ec --- /dev/null +++ b/packages/tempo/test/standalone.test.ts @@ -0,0 +1,83 @@ +import { format } from '#tempo/format'; +import { parse } from '#tempo/parse'; +import { init } from '#tempo/support'; + + +describe('Tempo: Standalone Utilities', () => { + + beforeAll(() => { + // Initialize the standard patterns and layouts for the standalone parser + // Force UTC for deterministic testing of Instant projection + init({ timeZone: 'UTC' }); + }); + + describe('format()', () => { + const zdt = Temporal.ZonedDateTime.from('2026-04-22T15:00:00+10:00[Australia/Sydney]'); + + it('should format a ZonedDateTime with a string template', () => { + const res = format(zdt, '{yyyy}-{mm}-{dd}'); + expect(res).toBe('2026-04-22'); + }); + + it('should return a number for numeric-only patterns', () => { + const res = format(zdt, '{yyyy}{mm}{dd}'); + expect(typeof res).toBe('number'); + expect(res).toBe(20260422); + }); + + it('should return a Queriable Proxy when called without a template', () => { + const res = format(zdt); + expect(typeof res).toBe('object'); + // 'weekDate' uses {www}, {yyyy}-{mmm}-{dd} + expect(res.weekDate).toBe('Wed, 2026-Apr-22'); + }); + + it('should be enumerable when called without a template', () => { + const res = format(zdt); + const keys = Object.keys(res); + expect(keys).toContain('display'); + expect(keys).toContain('weekDate'); + expect(keys).toContain('logStamp'); + }); + + it('should support Temporal.Instant as input', () => { + const instant = zdt.toInstant(); + // {hh} is 24-hour in Tempo + const res = format(instant, '{yyyy}-{mm}-{dd} {hh}:{mi}'); + + // Check against the same instant projected to the resolved timezone + // (since init() might adopt the system timezone in some environments) + const expected = format(instant.toZonedDateTimeISO(Temporal.Now.timeZoneId()), '{yyyy}-{mm}-{dd} {hh}:{mi}'); + expect(res).toBe(expected); + }); + + it('should default to "Now" when called with no arguments', () => { + const res = format(); + // Numeric return for yearMonthDay + expect(typeof res.yearMonthDay).toBe('number'); + expect(String(res.yearMonthDay)).toMatch(/^\d{8}$/); + }); + }); + + describe('parse()', () => { + it('should parse a string to ZonedDateTime', () => { + const res = parse('2026-04-22'); + expect(res).toBeInstanceOf(Temporal.ZonedDateTime); + expect(res.toString()).toContain('2026-04-22'); + }); + + it('should support Temporal.Instant as input', () => { + const zdt = Temporal.ZonedDateTime.from('2026-04-22T15:00:00+10:00[Australia/Sydney]'); + const instant = zdt.toInstant(); + const res = parse(instant); + expect(res).toBeInstanceOf(Temporal.ZonedDateTime); + expect(res.epochMilliseconds).toBe(instant.epochMilliseconds); + }); + + it('should parse strict ISO strings with offsets', () => { + const str = '2026-04-22T15:00:00+10:00'; + const res = parse(str); + expect(res.toString()).toContain('2026-04-22T15:00:00+10:00'); + }); + }); +}); diff --git a/packages/tempo/test/tsconfig.json b/packages/tempo/test/tsconfig.json index 850176af..f527c384 100644 --- a/packages/tempo/test/tsconfig.json +++ b/packages/tempo/test/tsconfig.json @@ -6,7 +6,8 @@ "composite": true, "types": [ "vitest/globals", - "node" + "node", + "@js-temporal/polyfill" ], "paths": { "#library": [ @@ -31,7 +32,13 @@ "../src/plugin/module/module.duration.ts" ], "#tempo/format": [ - "../src/plugin/module/module.format.ts" + "../src/discrete/discrete.format.ts" + ], + "#tempo/parse": [ + "../src/discrete/discrete.parse.ts" + ], + "#tempo/discrete": [ + "../src/discrete/discrete.index.ts" ], "#tempo/ticker": [ "../src/plugin/extend/extend.ticker.ts" diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts index 3333257e..f1b5c44a 100644 --- a/packages/tempo/vitest.config.ts +++ b/packages/tempo/vitest.config.ts @@ -25,8 +25,8 @@ export default defineConfig({ { find: /^#tempo\/term$/, replacement: resolve(__dirname, './dist/plugin/term/term.index.js') }, { find: /^#tempo\/term\/standard$/, replacement: resolve(__dirname, './dist/plugin/term/standard.index.js') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './dist/plugin/module/module.duration.js') }, - { find: /^#tempo\/format$/, replacement: resolve(__dirname, './dist/plugin/module/module.format.js') }, - { find: /^#tempo\/parse$/, replacement: resolve(__dirname, './dist/tempo.parse.js') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './dist/discrete/discrete.$1.js') }, + { find: /^#tempo\/discrete$/, replacement: resolve(__dirname, './dist/discrete/discrete.index.js') }, { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './dist/plugin/module/module.mutate.js') }, { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './dist/plugin/extend/extend.ticker.js') }, { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.js') }, @@ -46,8 +46,8 @@ export default defineConfig({ { find: /^#tempo\/term\/(.*)$/, replacement: resolve(__dirname, './src/plugin/term/$1') }, { find: /^#tempo\/ticker$/, replacement: resolve(__dirname, './src/plugin/extend/extend.ticker.ts') }, { find: /^#tempo\/duration$/, replacement: resolve(__dirname, './src/plugin/module/module.duration.ts') }, - { find: /^#tempo\/format$/, replacement: resolve(__dirname, './src/plugin/module/module.format.ts') }, - { find: /^#tempo\/parse$/, replacement: resolve(__dirname, './src/tempo.parse.ts') }, + { find: /^#tempo\/(parse|format)$/, replacement: resolve(__dirname, './src/discrete/discrete.$1.ts') }, + { find: /^#tempo\/discrete$/, replacement: resolve(__dirname, './src/discrete/discrete.index.ts') }, { find: /^#tempo\/mutate$/, replacement: resolve(__dirname, './src/plugin/module/module.mutate.ts') }, { find: /^#tempo\/scripts\/(.*)\.js$/, replacement: resolve(__dirname, './scripts/$1.ts') }, { find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') }, diff --git a/vitest.config.ts b/vitest.config.ts index 33985237..e7ce1f6c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,6 +15,8 @@ export default defineConfig({ { find: /^#tempo\/plugins\/plugin\.type\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/plugin.type.ts') }, { find: /^#tempo\/plugins\/plugin\.(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/plugins/extend/plugin.$1.ts') }, { find: /^#tempo\/core$/, replacement: path.resolve(__dirname, './packages/tempo/src/core.index.ts') }, + { find: /^#tempo\/(parse|format)$/, replacement: path.resolve(__dirname, './packages/tempo/src/discrete/discrete.$1.ts') }, + { find: /^#tempo\/discrete$/, replacement: path.resolve(__dirname, './packages/tempo/src/discrete/discrete.index.ts') }, { find: /^#tempo\/tempo\.class\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/tempo.index.ts') }, { find: /^#tempo\/support$/, replacement: path.resolve(__dirname, './packages/tempo/src/support/support.index.ts') }, { find: /^#tempo\/(.*)\.js$/, replacement: path.resolve(__dirname, './packages/tempo/src/$1.ts') }, From b385705446ed094e021a628cb34303cea5f58f10 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 18:33:42 +1000 Subject: [PATCH 2/7] 1st review --- package-lock.json | 506 +++++++++++++++++- packages/tempo/.vitepress/config.ts | 3 + packages/tempo/doc/lazy-evaluation-pattern.md | 2 - packages/tempo/doc/tempo.benchmarks.md | 2 +- packages/tempo/doc/tempo.config.md | 3 +- packages/tempo/package.json | 6 +- 6 files changed, 511 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93f887eb..bcbe3cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tempo-monorepo", - "version": "2.2.6", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tempo-monorepo", - "version": "2.2.6", + "version": "2.4.0", "workspaces": [ "packages/*" ], @@ -2381,6 +2381,17 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.9.tgz", + "integrity": "sha512-qycIHAucxy/LXAYIjmLmtQ8q9GPnMbnjG1KXhWm9o5sCr6pOYDATkMPiTNa6/v8eELyqOQ2FsEqeoFYmgv/gJg==", + "deprecated": "this version has critical issues, please update to the latest version", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2427,6 +2438,16 @@ "string-width": "^4.1.0" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2663,6 +2684,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/boxen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", @@ -3054,6 +3082,45 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3160,6 +3227,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3311,6 +3388,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -3620,6 +3727,75 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -3960,6 +4136,16 @@ "source-map": "~0.6.1" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4726,6 +4912,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -5783,6 +5999,26 @@ "dev": true, "license": "MIT" }, + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5983,6 +6219,17 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-mathjax3": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz", + "integrity": "sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "juice": "^8.0.0", + "mathjax-full": "^3.2.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5993,6 +6240,19 @@ "node": ">= 0.4" } }, + "node_modules/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", @@ -6022,6 +6282,13 @@ "dev": true, "license": "MIT" }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6039,6 +6306,13 @@ "node": ">= 8" } }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -6160,6 +6434,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -6246,6 +6533,13 @@ "dev": true, "license": "MIT" }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6413,6 +6707,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6857,6 +7164,23 @@ "parse-path": "^7.0.0" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7976,6 +8300,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "dev": true, + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -8059,6 +8393,31 @@ "node": ">=0.10.0" } }, + "node_modules/speech-rule-engine": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.3.tgz", + "integrity": "sha512-SBMgkuJYvP4F62daRfBNwYC2nXTEhNXAfsBZ/BB7Ly85/KnbnjmKM7/45ZrFbH6jIMiAliDUDPSZFUuXDvcg6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.9", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -8385,6 +8744,13 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -9266,6 +9632,16 @@ "dev": true, "license": "MIT" }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -9530,6 +9906,100 @@ "defaults": "^1.0.3" } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -9540,6 +10010,24 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9662,6 +10150,13 @@ "node": ">=8" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "dev": true, + "license": "MIT" + }, "node_modules/widest-line": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", @@ -9972,7 +10467,7 @@ }, "packages/library": { "name": "@magmacomputing/library", - "version": "2.2.6", + "version": "2.4.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -9991,17 +10486,18 @@ }, "packages/tempo": { "name": "@magmacomputing/tempo", - "version": "2.2.6", + "version": "2.4.0", "license": "MIT", "dependencies": { "tslib": "^2.8.1" }, "devDependencies": { "@js-temporal/polyfill": "^0.5.1", - "@magmacomputing/library": "2.2.6", + "@magmacomputing/library": "2.4.0", "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", + "markdown-it-mathjax3": "^4.3.2", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "vitepress": "^1.6.4" diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts index 0c9f85ac..72e42426 100644 --- a/packages/tempo/.vitepress/config.ts +++ b/packages/tempo/.vitepress/config.ts @@ -10,6 +10,9 @@ export default defineConfig({ base: '/magma/', title: "Tempo", description: "The Professional Date-Time Library for Temporal", + markdown: { + math: true + }, themeConfig: { logo: '/logo.svg', search: { diff --git a/packages/tempo/doc/lazy-evaluation-pattern.md b/packages/tempo/doc/lazy-evaluation-pattern.md index 75a93560..f09b2aff 100644 --- a/packages/tempo/doc/lazy-evaluation-pattern.md +++ b/packages/tempo/doc/lazy-evaluation-pattern.md @@ -17,8 +17,6 @@ To get around freezing, you might try taking all property descriptors, wiping th Tempo achieves lazy evaluation in $O(1)$ time using a **Delegator Proxy** that memoizes results back onto the target object. ```javascript -// The O(1) approach - Extremely fast, zero overhead - #setLazy(target, name, defineFunction) { const get = () => { const value = defineFunction.call(this); // Evaluate the value diff --git a/packages/tempo/doc/tempo.benchmarks.md b/packages/tempo/doc/tempo.benchmarks.md index 5a748f51..84b5146e 100644 --- a/packages/tempo/doc/tempo.benchmarks.md +++ b/packages/tempo/doc/tempo.benchmarks.md @@ -38,7 +38,7 @@ The static `#guard` regex acts as a rapid "Sync Point." The benchmark script used `performance.now()` within a Vitest environment to ensure accurate module resolution and internal alias support (`#library`). 1. **Lazy Creation**: Creates a `new Tempo('2024-05-20')` without accessing any properties. -2. **Eager Simulation**: Creates a `new Tempo()` and manually triggers discovery on 5 core properties to simulate O(N) initialization. +2. **Eager Simulation**: Creates a `new Tempo()` and manually triggers discovery on 5 core properties to simulate $O(N)$ initialization. 3. **Invalid Parse**: Passes a string that fails the Master Guard (e.g., includes emojis or exotic symbols) to measure rejection speed. > [!NOTE] diff --git a/packages/tempo/doc/tempo.config.md b/packages/tempo/doc/tempo.config.md index 7d8d6db6..bf1010a2 100644 --- a/packages/tempo/doc/tempo.config.md +++ b/packages/tempo/doc/tempo.config.md @@ -151,11 +151,12 @@ When `mode: 'defer'` is set, the registry-discovery logic is deferred until the ```javascript // Optimized for mass-creation const t = new Tempo('now', { mode: 'defer' }); -// No registries are built yet. The constructor returns in O(1) time. console.log(t.format('{yyyy}')); // Discovery triggers NOW, only once. ``` +When initialized this way, no registries are built upfront. The constructor returns in $O(1)$ time. + > [!TIP] > **Zero-Cost Constructor**: Combining the **Master Guard** (automatic) and the **`defer`** mode allows Tempo to satisfy the "Zero-Cost Constructor" requirement for mass-processing applications. diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 9c6cd0e3..29ec4eb9 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -28,7 +28,8 @@ "**/module.*.js", "**/module.*.ts", "**/tempo.index.js", - "**/tempo.parse.js" + "./dist/discrete/discrete.parse.js", + "./dist/discrete/discrete.format.js" ], "main": "dist/tempo.index.js", "types": "dist/tempo.index.d.ts", @@ -231,6 +232,7 @@ "@rollup/plugin-alias": "^6.0.0", "cross-env": "^7.0.3", "magic-string": "^0.30.21", + "markdown-it-mathjax3": "^4.3.2", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", "vitepress": "^1.6.4" @@ -239,4 +241,4 @@ "doc": "doc", "test": "test" } -} \ No newline at end of file +} From 51f60eeb95d0c1cff87b399aea28aa092b3f81e0 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 18:53:17 +1000 Subject: [PATCH 3/7] general review --- packages/tempo/README.md | 10 +++--- packages/tempo/doc/migration-guide.md | 4 +-- packages/tempo/doc/tempo.format.md | 9 +++++ packages/tempo/package.json | 16 +++------ .../tempo/src/discrete/discrete.format.ts | 7 ++-- packages/tempo/src/discrete/discrete.parse.ts | 33 ++++++++++++++----- packages/tempo/src/library.index.ts | 2 +- packages/tempo/src/support/tempo.util.ts | 9 ++--- packages/tempo/test/standalone.test.ts | 10 +++++- 9 files changed, 63 insertions(+), 37 deletions(-) diff --git a/packages/tempo/README.md b/packages/tempo/README.md index 1a309f25..0ab63a16 100644 --- a/packages/tempo/README.md +++ b/packages/tempo/README.md @@ -37,18 +37,18 @@ ```javascript import { Tempo } from '@magmacomputing/tempo'; -// ๐ŸŽฏ Natural Language Parsing -const event = new Tempo('next Friday 3pm'); +// ๐ŸŽฏ Natural Language Parsing (Deterministic anchor) +const event = new Tempo('next Friday 3pm', { anchor: '2026-10-15' }); // ๐Ÿ”„ Fluent Mutations (Immutable) const reminder = event.add({ hours: 2 }).set({ minute: 0 }); // โณ Comparative Durations -const diff = event.until('xmas'); -console.log(diff.iso); // e.g. P3W2D +const diff = event.until('2026-12-25'); +console.log(diff.iso); // P2M2D // ๐Ÿ“ Beautiful Formatting -console.log(event.format('{mon} {day}, {yyyy}')); // e.g. Oct 24, 2026 +console.log(event.format('{mon} {day}, {yyyy}')); // October 23, 2026 ``` --- diff --git a/packages/tempo/doc/migration-guide.md b/packages/tempo/doc/migration-guide.md index a0970edd..ef210261 100644 --- a/packages/tempo/doc/migration-guide.md +++ b/packages/tempo/doc/migration-guide.md @@ -54,8 +54,8 @@ import { parse } from '@magmacomputing/tempo/parse'; import { format } from '@magmacomputing/tempo/format'; ``` -### โš ๏ธ Deprecated Paths -We have reorganized the internal file structure to optimize for standalone usage. The following internal paths are now **deprecated** and will be removed in a future release: +### โš ๏ธ Removed Paths +We have reorganized the internal file structure to optimize for standalone usage. The following internal paths have been **removed** from the public export map in v2.4.0: * โŒ `@magmacomputing/tempo/module/parse` * โŒ `@magmacomputing/tempo/module/format` diff --git a/packages/tempo/doc/tempo.format.md b/packages/tempo/doc/tempo.format.md index 4c34b6bd..981cb79e 100644 --- a/packages/tempo/doc/tempo.format.md +++ b/packages/tempo/doc/tempo.format.md @@ -81,20 +81,29 @@ Tempo.extend(FormatModule); | :--- | :--- | :--- | | `{yyyy}` | 4-digit Year | `2026` | | `{yy}` | 2-digit Year | `26` | +| `{yw}` | Year of Week (ISO) | `2026` | +| `{yyww}` | Year & Week (ISO) | `202617` | | `{mon}` | Full Month Name | `October` | | `{mmm}` | Short Month Name | `Oct` | | `{mm}` | 2-digit Month | `10` | | `{dd}` | 2-digit Day | `24` | +| `{day}` | Unpadded Day | `24` (or `9`) | | `{wkd}` | Full Weekday Name | `Saturday` | | `{www}` | Short Weekday Name | `Sat` | +| `{dow}` | Day of Week (1-7) | `6` | | `{hh}` | 2-digit Hour (24h) | `15` | | `{HH}` | 12-hour clock | `3` | | `{mer}` | am/pm marker | `pm` | | `{MER}` | AM/PM marker | `PM` | | `{mi}` | Minutes | `30` | | `{ss}` | Seconds | `45` | +| `{hhmiss}` | Compact Time (24h) | `153045` | +| `{ms}` | 3-digit Milliseconds | `123` | +| `{us}` | 3-digit Microseconds | `456` | +| `{ns}` | 3-digit Nanoseconds | `789` | | `{ff}` | Fractional Seconds | `123456789` | | `{ts}` | Unix Timestamp | `1792843200000` | +| `{nano}` | Nanosecond Timestamp | `1792843200000000000` | | `{tz}` | Time Zone ID | `Australia/Sydney` | ### ๐Ÿ”„ Automatic Meridiem diff --git a/packages/tempo/package.json b/packages/tempo/package.json index 29ec4eb9..d0fb35c8 100644 --- a/packages/tempo/package.json +++ b/packages/tempo/package.json @@ -25,11 +25,11 @@ "sideEffects": [ "**/temporal.polyfill.js", "**/*-polyfill.ts", + "**/tempo.index.js", "**/module.*.js", "**/module.*.ts", - "**/tempo.index.js", - "./dist/discrete/discrete.parse.js", - "./dist/discrete/discrete.format.js" + "**/discrete.*.js", + "**/discrete.*.ts" ], "main": "dist/tempo.index.js", "types": "dist/tempo.index.d.ts", @@ -125,14 +125,6 @@ "types": "./dist/plugin/extend/extend.*.d.ts", "import": "./dist/plugin/extend/extend.*.js" }, - "./module/parse": { - "types": "./dist/discrete/discrete.parse.d.ts", - "import": "./dist/discrete/discrete.parse.js" - }, - "./module/format": { - "types": "./dist/discrete/discrete.format.d.ts", - "import": "./dist/discrete/discrete.format.js" - }, "./module/*": { "types": "./dist/plugin/module/module.*.d.ts", "import": "./dist/plugin/module/module.*.js" @@ -241,4 +233,4 @@ "doc": "doc", "test": "test" } -} +} \ No newline at end of file diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 369e4974..53ecf94f 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -1,3 +1,4 @@ +import '#library/temporal.polyfill.js'; import { pad } from '#library/string.library.js'; import { ifNumeric } from '#library/coercion.library.js'; import { isString, isObject, isZonedDateTime, isInstant, isUndefined, isDefined } from '#library/type.library.js'; @@ -69,7 +70,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol const formats = config?.formats ?? enums.FORMAT; - let template = (isString(fmt) && formats && (formats as any).has(fmt as string)) + let template = (isString(fmt) && formats && (typeof (formats as any).has === 'function' ? (formats as any).has(fmt as string) : Object.prototype.hasOwnProperty.call(formats, fmt as string))) ? (formats as Record)[fmt as string] : String(fmt); @@ -82,7 +83,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol } } - const result = template.replaceAll(new RegExp(Match.braces), (_match: string, token: string) => { + const result = template.replace(new RegExp(Match.braces, 'g'), (_match: string, token: string) => { switch (token) { case 'yyyy': return pad(zdt.year, 4); case 'yy': return pad(zdt.year % 100); @@ -98,7 +99,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol case 'www': return enums.WEEKDAY.keyOf(zdt.dayOfWeek as any); case 'ww': return pad(zdt.weekOfYear); case 'hh': return pad(zdt.hour); - case 'HH': return pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); + case 'HH': return (zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12).toString(); case 'mer': return zdt.hour >= 12 ? 'pm' : 'am'; case 'MER': return zdt.hour >= 12 ? 'PM' : 'AM'; case 'mi': return pad(zdt.minute); diff --git a/packages/tempo/src/discrete/discrete.parse.ts b/packages/tempo/src/discrete/discrete.parse.ts index ca9441b7..f828f658 100644 --- a/packages/tempo/src/discrete/discrete.parse.ts +++ b/packages/tempo/src/discrete/discrete.parse.ts @@ -19,6 +19,12 @@ import type { Range, ResolvedRange } from '../plugin/plugin.type.js'; import type { Tempo } from '../tempo.class.js'; import * as t from '../tempo.type.js'; +/** + * Internal helpers to normalize TimeZone and Calendar IDs + */ +const tzId = (v: any): string => typeof v === 'string' ? v : (v?.id ?? v?.timeZoneId); +const calId = (v: any): string => typeof v === 'string' ? v : (v?.id ?? v?.calendarId); + /** * Internal helper to resolve state from 'this' context or first argument */ @@ -45,8 +51,8 @@ const _ParseEngine = { if (!term && (isZonedDateTime(tempo) || isInstant(tempo))) { const { config } = state; - const tz = isString(config.timeZone) ? config.timeZone : (config.timeZone as any).id ?? (config.timeZone as any).timeZoneId; - const cal = isString(config.calendar) ? config.calendar : (config.calendar as any).id ?? (config.calendar as any).calendarId; + const tz = tzId(config.timeZone); + const cal = calId(config.calendar); const dt = isZonedDateTime(tempo) ? tempo : (tempo as Temporal.Instant).toZonedDateTimeISO(tz); return dt.withTimeZone(tz).withCalendar(cal); } @@ -61,8 +67,8 @@ const _ParseEngine = { const val = dateTime ?? state.anchor ?? (isTempo(tempo) ? (tempo as any).toDateTime() : (isZonedDateTime(tempo) ? tempo : (isInstant(tempo) ? tempo.toZonedDateTimeISO(config.timeZone) : undefined))); const basis = isDefined(val) ? val : instant().toZonedDateTimeISO(config.timeZone); - const tz = isTempo(basis) ? (basis as any).tz : (isZonedDateTime(basis) ? basis.timeZoneId : config.timeZone); - const cal = isTempo(basis) ? (basis as any).cal : (isZonedDateTime(basis) ? basis.calendarId : config.calendar); + const tz = isTempo(basis) ? (basis as any).tz : tzId(basis ?? config.timeZone); + const cal = isTempo(basis) ? (basis as any).cal : calId(basis ?? config.calendar); today = isZonedDateTime(basis) ? basis : (isTempo(basis) ? (basis as any).toDateTime() : instant().toZonedDateTimeISO(tz).withCalendar(cal)); @@ -134,8 +140,8 @@ const _ParseEngine = { const res = _ParseEngine.conform(state, tempo, today, isAnchored, resolvingKeys); const { timeZone: tz2, calendar: cal2 } = state.config; - const targetTz = isString(tz2) ? tz2 : (tz2 as any).id ?? (tz2 as any).timeZoneId; - const targetCal = isString(cal2) ? cal2 : (cal2 as any).id ?? (cal2 as any).calendarId; + const targetTz = tzId(tz2); + const targetCal = calId(cal2); const { dateTime: dt, timeZone } = compose(res, today, tz, targetTz, targetCal); @@ -539,12 +545,21 @@ export function parse(value: t.DateTime, options: t.Options = {}): Temporal.Zone const runtime = getRuntime(); const globalState = runtime.state ?? init(); - // Create a local state shadowed from the global state + // Create a local state isolated from the global state const state: t.Internal.State = { - config: markConfig(Object.create(globalState.config)), - parse: markConfig(Object.create(globalState.parse)) + ...globalState, + config: markConfig({ ...globalState.config }), + parse: markConfig({ ...globalState.parse }) } as t.Internal.State; + // Deep-clone nested mutable objects to prevent global leakage + state.parse.snippet = { ...globalState.parse.snippet }; + state.parse.layout = { ...globalState.parse.layout }; + state.parse.event = { ...globalState.parse.event }; + state.parse.period = { ...globalState.parse.period }; + state.parse.ignore = { ...globalState.parse.ignore }; + state.parse.pattern = new Map(globalState.parse.pattern); + // Standalone parsing defaults to 'strict' mode const localOptions = { ...options }; localOptions.mode ??= 'strict'; diff --git a/packages/tempo/src/library.index.ts b/packages/tempo/src/library.index.ts index 9331f94b..df3469c7 100644 --- a/packages/tempo/src/library.index.ts +++ b/packages/tempo/src/library.index.ts @@ -1,7 +1,7 @@ /** * # Magma Library * This is a secondary entry point for the "Magma Utility Stack". - * It provides curated access to the specific utilities Tempo uses under the hood. + * It provides curated access to the specific utilities Magma uses under the hood. */ export { Pledge } from '#library/pledge.class.js'; diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/tempo.util.ts index 2d80fc94..e9a7e307 100644 --- a/packages/tempo/src/support/tempo.util.ts +++ b/packages/tempo/src/support/tempo.util.ts @@ -87,7 +87,7 @@ export function compileRegExp(layout: string | RegExp, state: t.Internal.State, if (source.startsWith('^') && source.endsWith('$')) source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $) - return source.replaceAll(new RegExp(Match.braces), (match, name) => { // iterate over "{}" pairs in the source string + return source.replace(new RegExp(Match.braces, 'g'), (match, name) => { // iterate over "{}" pairs in the source string const token = getSymbol(name); // get the symbol for this {name} const customs = snippet?.[token as keyof Snippet]?.source ?? snippet?.[name as keyof Snippet]?.source; const globals = state.parse.snippet[token as keyof Snippet]?.source ?? state.parse.snippet[name as keyof Snippet]?.source; @@ -128,6 +128,10 @@ const isEmpty = (v: any) => !v || (Array.isArray(v) && v.length === 0) || (typeo /** @internal build RegExp patterns into the state */ export function setPatterns(state: t.Internal.State) { + // ensure we have our own isolated mutable containers before mutation + state.parse.snippet = { ...state.parse.snippet }; + state.parse.pattern = new Map(state.parse.pattern); + const snippet = state.parse.snippet; // 1. ensure numeric snippets are current @@ -151,9 +155,6 @@ export function setPatterns(state: t.Internal.State) { delete state.parse.ignorePattern; } - // ensure we have our own Map to mutate - state.parse.pattern = new Map(state.parse.pattern); - // 3. build the patterns ownEntries(state.parse.layout).forEach(([key, layout]) => { const symbol = getSymbol(key); diff --git a/packages/tempo/test/standalone.test.ts b/packages/tempo/test/standalone.test.ts index 451728ec..bafad82b 100644 --- a/packages/tempo/test/standalone.test.ts +++ b/packages/tempo/test/standalone.test.ts @@ -1,16 +1,24 @@ import { format } from '#tempo/format'; import { parse } from '#tempo/parse'; -import { init } from '#tempo/support'; +import { init, getRuntime } from '#tempo/support'; describe('Tempo: Standalone Utilities', () => { + let prevState: any; beforeAll(() => { + // Save current state to restore later + prevState = { ...getRuntime().state?.config }; // Initialize the standard patterns and layouts for the standalone parser // Force UTC for deterministic testing of Instant projection init({ timeZone: 'UTC' }); }); + afterAll(() => { + // Restore previous state + init(prevState); + }); + describe('format()', () => { const zdt = Temporal.ZonedDateTime.from('2026-04-22T15:00:00+10:00[Australia/Sydney]'); From ce5fe073093c4b994089f8d9048235c592e264d4 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 19:00:13 +1000 Subject: [PATCH 4/7] wrap up 1st review --- packages/tempo/src/discrete/discrete.format.ts | 4 ++-- packages/tempo/test/standalone.test.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 53ecf94f..72b52556 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -125,8 +125,8 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol } }); - const isExplicitlyNumeric = (NumericPattern as readonly string[]).includes(template as any); - return (isExplicitlyNumeric ? ifNumeric(result) : result) as any; + const isNumericOutput = (NumericPattern as readonly string[]).includes(template as any) || (result.length > 0 && /^[0-9]+$/.test(result)); + return (isNumericOutput ? ifNumeric(result, true) : result) as any; } /** diff --git a/packages/tempo/test/standalone.test.ts b/packages/tempo/test/standalone.test.ts index bafad82b..0172184e 100644 --- a/packages/tempo/test/standalone.test.ts +++ b/packages/tempo/test/standalone.test.ts @@ -33,6 +33,12 @@ describe('Tempo: Standalone Utilities', () => { expect(res).toBe(20260422); }); + it('should return a number for ad-hoc numeric-only patterns (generic detection)', () => { + const res = format(zdt, '{mm}{dd}'); + expect(typeof res).toBe('number'); + expect(res).toBe(422); // 0422 + }); + it('should return a Queriable Proxy when called without a template', () => { const res = format(zdt); expect(typeof res).toBe('object'); From 46783c1fadda996dd7f39d259bfe12da046493c7 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 19:16:15 +1000 Subject: [PATCH 5/7] regression --- packages/tempo/doc/tempo.format.md | 4 ++-- packages/tempo/src/discrete/discrete.format.ts | 5 +++-- packages/tempo/src/support/tempo.init.ts | 2 +- packages/tempo/src/support/tempo.util.ts | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/tempo/doc/tempo.format.md b/packages/tempo/doc/tempo.format.md index 981cb79e..36692e5f 100644 --- a/packages/tempo/doc/tempo.format.md +++ b/packages/tempo/doc/tempo.format.md @@ -92,7 +92,7 @@ Tempo.extend(FormatModule); | `{www}` | Short Weekday Name | `Sat` | | `{dow}` | Day of Week (1-7) | `6` | | `{hh}` | 2-digit Hour (24h) | `15` | -| `{HH}` | 12-hour clock | `3` | +| `{HH}` | 2-digit Hour (12h) | `03` | | `{mer}` | am/pm marker | `pm` | | `{MER}` | AM/PM marker | `PM` | | `{mi}` | Minutes | `30` | @@ -110,7 +110,7 @@ Tempo.extend(FormatModule); If your format string contains `{HH}` (12-hour clock) but lacks a `{mer}` or `{MER}` token, Tempo will automatically append `{mer}` to the end of the last time component to ensure the time remains unambiguous. ```typescript -t.format('{HH}:{mi}'); // "3:30pm" (auto-appended pm) +t.format('{HH}:{mi}'); // "03:30pm" (auto-appended pm) ``` ### ๐Ÿ”ข Numeric Resolution diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 72b52556..337305b4 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -99,7 +99,7 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol case 'www': return enums.WEEKDAY.keyOf(zdt.dayOfWeek as any); case 'ww': return pad(zdt.weekOfYear); case 'hh': return pad(zdt.hour); - case 'HH': return (zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12).toString(); + case 'HH': return pad(zdt.hour > 12 ? zdt.hour % 12 : zdt.hour || 12); case 'mer': return zdt.hour >= 12 ? 'pm' : 'am'; case 'MER': return zdt.hour >= 12 ? 'PM' : 'AM'; case 'mi': return pad(zdt.minute); @@ -125,7 +125,8 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol } }); - const isNumericOutput = (NumericPattern as readonly string[]).includes(template as any) || (result.length > 0 && /^[0-9]+$/.test(result)); + const tokens = template.match(new RegExp(Match.braces, 'g')); + const isNumericOutput = (NumericPattern as readonly string[]).includes(template as any) || (tokens && tokens.length > 1 && /^[0-9]+$/.test(result)); return (isNumericOutput ? ifNumeric(result, true) : result) as any; } diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/tempo.init.ts index d88bcc4d..2764a072 100644 --- a/packages/tempo/src/support/tempo.init.ts +++ b/packages/tempo/src/support/tempo.init.ts @@ -43,7 +43,7 @@ export function init(options: t.Options = {}): t.Internal.State { }); // 2. Establish the base configuration options - state.config = markConfig(Object.create(Default)); + state.config = markConfig({ ...Default }); Object.defineProperties(state.config, { calendar: { value: calendar, enumerable: true, writable: true, configurable: true }, timeZone: { value: timeZone, enumerable: true, writable: true, configurable: true }, diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/tempo.util.ts index e9a7e307..77de5800 100644 --- a/packages/tempo/src/support/tempo.util.ts +++ b/packages/tempo/src/support/tempo.util.ts @@ -22,7 +22,7 @@ export const create = (obj: object, name: string): T => { if (typeof entry !== 'object' || entry === null) { throw new TypeError(`[Tempo#create] Failed to create shadowed object for '${name}'. The prototype entry from proto(obj) is missing or not an object (received: ${typeof entry}).`); } - return Object.create(entry); + return { ...entry } as T; }; /** @internal resolve a key to a symbol from Token or sym registries */ From 681b9887624fc56ce5de9fd61c7c310d578be4f8 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 19:19:23 +1000 Subject: [PATCH 6/7] stderr from pledge.class.test --- packages/library/test/common/pledge.class.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/library/test/common/pledge.class.test.ts b/packages/library/test/common/pledge.class.test.ts index d9475f57..e71b3acf 100644 --- a/packages/library/test/common/pledge.class.test.ts +++ b/packages/library/test/common/pledge.class.test.ts @@ -8,7 +8,7 @@ describe('Pledge', () => { expect(p1.isResolved).toBe(true); expect(await p1.promise).toBe('ok'); - const p2 = new Pledge({ catch: true }); + const p2 = new Pledge({ catch: true, silent: true }); p2.reject(new Error('fail')); expect(p2.isRejected).toBe(true); await expect(p2.promise).rejects.toThrow('fail'); @@ -22,7 +22,7 @@ describe('Pledge', () => { }); test('disposal', async () => { - const p = new Pledge({ catch: true }); + const p = new Pledge({ catch: true, silent: true }); p[Symbol.dispose](); expect(p.isRejected).toBe(true); await expect(p.promise).rejects.toThrow('Pledge disposed'); @@ -37,7 +37,7 @@ describe('Pledge', () => { await p1.promise; expect(onResolve).toHaveBeenCalledWith('data'); - const p2 = new Pledge({ onReject, catch: true }); + const p2 = new Pledge({ onReject, catch: true, silent: true }); await expect(p2.reject(new Error('err'))).rejects.toThrow('err'); expect(onReject).toHaveBeenCalled(); }); From 604cd2c4fdcacacdee06fab403ac51d9f86db910 Mon Sep 17 00:00:00 2001 From: Michael McRae Date: Wed, 22 Apr 2026 19:36:28 +1000 Subject: [PATCH 7/7] 2nd review --- .../tempo/src/discrete/discrete.format.ts | 35 +++++++++++++------ packages/tempo/src/support/tempo.init.ts | 2 +- packages/tempo/test/standalone.test.ts | 16 +++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/tempo/src/discrete/discrete.format.ts b/packages/tempo/src/discrete/discrete.format.ts index 337305b4..3908e63f 100644 --- a/packages/tempo/src/discrete/discrete.format.ts +++ b/packages/tempo/src/discrete/discrete.format.ts @@ -1,7 +1,7 @@ import '#library/temporal.polyfill.js'; import { pad } from '#library/string.library.js'; import { ifNumeric } from '#library/coercion.library.js'; -import { isString, isObject, isZonedDateTime, isInstant, isUndefined, isDefined } from '#library/type.library.js'; +import { isString, isObject, isZonedDateTime, isInstant, isPlainDate, isPlainDateTime, isUndefined, isDefined } from '#library/type.library.js'; import { isTempo, enums, Match, getRuntime } from '#tempo/support'; import { defineInterpreterModule } from '../plugin/plugin.util.js'; @@ -34,15 +34,30 @@ export function format(obj?: Temporal.ZonedDateTime | any, fmt?: string | symbol const config = isTempo(obj) ? obj.config : state?.config; const tz = config?.timeZone ?? 'UTC'; - const zdt = isTempo(obj) - ? obj.toDateTime() - : (isZonedDateTime(obj) - ? obj - : (isInstant(obj) - ? (obj as Temporal.Instant).toZonedDateTimeISO(tz) - : (isUndefined(obj) - ? Temporal.Now.zonedDateTimeISO(tz) - : obj))); + let zdt: any; + switch (true) { + case isTempo(obj): + zdt = (obj as any).toDateTime(); + break; + case isZonedDateTime(obj): + zdt = obj; + break; + case isInstant(obj): + zdt = (obj as any).toZonedDateTimeISO(tz); + break; + case isString(obj): + zdt = (obj as any).includes('[') ? Temporal.ZonedDateTime.from(obj as any) : ((obj as any).includes('T') ? Temporal.PlainDateTime.from(obj as any).toZonedDateTime(tz) : Temporal.PlainDate.from(obj as any).toZonedDateTime(tz)); + break; + case isPlainDateTime(obj): + case isPlainDate(obj): + zdt = (obj as any).toZonedDateTime(tz); + break; + case obj === undefined: + zdt = Temporal.Now.zonedDateTimeISO(tz); + break; + default: + zdt = obj; + } if (isUndefined(fmt)) { const formats = config?.formats ?? enums.FORMAT; diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/tempo.init.ts index 2764a072..a1d28116 100644 --- a/packages/tempo/src/support/tempo.init.ts +++ b/packages/tempo/src/support/tempo.init.ts @@ -43,7 +43,7 @@ export function init(options: t.Options = {}): t.Internal.State { }); // 2. Establish the base configuration options - state.config = markConfig({ ...Default }); + markConfig(Object.assign(state.config, Default)); Object.defineProperties(state.config, { calendar: { value: calendar, enumerable: true, writable: true, configurable: true }, timeZone: { value: timeZone, enumerable: true, writable: true, configurable: true }, diff --git a/packages/tempo/test/standalone.test.ts b/packages/tempo/test/standalone.test.ts index 0172184e..137928ce 100644 --- a/packages/tempo/test/standalone.test.ts +++ b/packages/tempo/test/standalone.test.ts @@ -65,6 +65,22 @@ describe('Tempo: Standalone Utilities', () => { expect(res).toBe(expected); }); + it('should support ISO strings as input', () => { + expect(format('2026-04-22T15:00:00+10:00[Australia/Sydney]', '{yyyy}')).toBe('2026'); + expect(format('2026-04-22T15:00:00', '{yyyy}')).toBe('2026'); // PlainDateTime -> ZDT + expect(format('2026-04-22', '{yyyy}')).toBe('2026'); // PlainDate -> ZDT + }); + + it('should support Temporal.PlainDate as input', () => { + const pd = Temporal.PlainDate.from('2026-04-22'); + expect(format(pd, '{yyyy}-{mm}-{dd}')).toBe('2026-04-22'); + }); + + it('should support Temporal.PlainDateTime as input', () => { + const pdt = Temporal.PlainDateTime.from('2026-04-22T15:00:00'); + expect(format(pdt, '{yyyy}-{mm}-{dd}')).toBe('2026-04-22'); + }); + it('should default to "Now" when called with no arguments', () => { const res = format(); // Numeric return for yearMonthDay