diff --git a/.agent/workflows/interactive-testing.md b/.agent/workflows/interactive-testing.md
index 3ee4d48d..15da7b3d 100644
--- a/.agent/workflows/interactive-testing.md
+++ b/.agent/workflows/interactive-testing.md
@@ -6,12 +6,12 @@ To use a NodeJS interactive session to test your Tempo library, you can use the
// turbo
```bash
-npx tsx -i --import ./test/repl.ts
+npx tsx --conditions=development -i --harmony-temporal --import ./bin/repl.ts
```
### Purpose
-This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` polyfill into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script.
+This command starts a Node.js REPL (Read-Eval-Print Loop) while pre-loading the `Tempo` class and the `Temporal` support into the global scope. This allows you to try different invocations of `Tempo` directly without writing a script.
### Usage Examples
Once the REPL has started, you can run commands like:
@@ -32,4 +32,4 @@ t1.add({ days: 5 }).format('plain');
### Why this works
- `npx tsx`: Uses the `tsx` runner to handle TypeScript files on the fly.
- `-i`: Explicitly requests an interactive session.
-- `--import ./test/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`.
\ No newline at end of file
+- `--import ./bin/repl.ts`: Loads the helper script before starting the REPL, which attaches `Tempo` to `globalThis`.
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 30d121bb..f851e788 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,39 +34,4 @@ jobs:
run: npm test
working-directory: packages/tempo
- test-parse-prefilter:
- name: Test with parse.preFilter enabled
- runs-on: ubuntu-latest
- timeout-minutes: 30
- if: (github.event_name == 'push' || github.event_name == 'pull_request') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release/D' || github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'release/D')
- steps:
- - uses: actions/checkout@v4
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '22'
- cache: 'npm'
- - name: Install monorepo dependencies
- run: npm ci
- working-directory: ${{ github.workspace }}
- - name: Run all tests with parse.preFilter
- run: npm test
- working-directory: packages/tempo
- env:
- TEMPO_PREFILTER_CI: 'true'
- - name: Run end-to-end benchmark
- run: npx tsx --conditions=development bench/bench.parse.prefilter.e2e.ts > bench-output.json 2> bench-error.log
- working-directory: packages/tempo
- - name: Upload benchmark output
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: bench-parse-prefilter-e2e
- path: |
- packages/tempo/bench-output.json
- packages/tempo/bench-error.log
- - name: Validate benchmark output
- run: |
- node -e "const r=require('./packages/tempo/bench-output.json');if(!r.success){console.error('Benchmark failed:',r.errors);process.exit(1)}else{console.log('Benchmark passed.')}"
- working-directory: ${{ github.workspace }}
diff --git a/doc/main_branch_protection.md b/doc/main_branch_protection.md
new file mode 100644
index 00000000..98949e2c
--- /dev/null
+++ b/doc/main_branch_protection.md
@@ -0,0 +1,147 @@
+# Preventing Writes to the Main Branch Locally
+
+To prevent accidental writes (commits and pushes) to the `main` branch on your local workstation, you can use **Git Hooks**.
+
+
+## Example: Local Hooks for Main Branch Protection
+Set up local hooks to:
+1. **`pre-commit`**: Prevent direct commits to the `main` branch.
+2. **`pre-push`**: Prevent pushing changes to the remote `main` branch.
+
+### How to Override
+If you genuinely need to write to `main` (e.g., for an urgent fix), you have two options:
+- **Environment Variable**: Prepend the command with the override flag:
+ - `ALLOW_MAIN_COMMIT=true git commit -m "Urgent fix"`
+ - `ALLOW_MAIN_PUSH=true git push`
+- **Skip Hooks**: Use the standard Git flag:
+ - `git commit --no-verify`
+ - `git push --no-verify`
+
+---
+
+## Applying This Globally (Recommended)
+If you want this protection to apply to **every repository** on your workstation, you can configure a global hooks directory.
+
+### 1. Create a Global Hooks Directory
+Choose a location (e.g., `~/.git-hooks`) and move the hook scripts there:
+
+```bash
+mkdir -p ~/.git-hooks
+# Copy the hooks from your repository (replace /path/to/repo with your repo root or use the command below)
+# Example using git to find the repo root:
+cp $(git rev-parse --show-toplevel)/.git/hooks/pre-commit ~/.git-hooks/
+cp $(git rev-parse --show-toplevel)/.git/hooks/pre-push ~/.git-hooks/
+chmod +x ~/.git-hooks/*
+```
+
+
+### 2. Configure Git Globally (with Important Warning)
+Run this command to tell Git to use your new global hooks directory:
+
+```bash
+git config --global core.hooksPath ~/.git-hooks
+```
+
+**⚠️ Warning:** Setting `core.hooksPath` with the `--global` flag disables all per-repository `.git/hooks/*` scripts. This will break tools that rely on per-project hooks, such as Husky, lefthook, lint-staged, and others. Any hooks defined in individual repositories will be ignored as long as the global `core.hooksPath` is set.
+
+#### Recommended Alternatives
+
+- **Chain per-repo hooks from your global hook scripts:**
+ - Manually update your global hook scripts (in `~/.git-hooks/`) to call any existing hooks in each repository’s `.git/hooks/` directory, so you don’t lose project-specific logic.
+- **Scope the setting to individual repositories:**
+ - Instead of using `--global`, set the hooks path only for the current repository:
+ ```bash
+ git config core.hooksPath ~/.git-hooks
+ ```
+ - This way, only the current repo is affected, and other repos keep their own `.git/hooks/*` scripts.
+
+For more details, see the [Git documentation on `core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehookspath).
+
+---
+
+## Hook Implementation Details
+
+### pre-commit
+This script checks the current branch before every commit.
+
+```bash
+#!/bin/bash
+CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+if [ "$CURRENT_BRANCH" = "main" ]; then
+ if [ "$ALLOW_MAIN_COMMIT" != "true" ]; then
+ echo "❌ ERROR: Direct commit to 'main' branch is prohibited."
+ exit 1
+ fi
+fi
+```
+
+### pre-push
+This script checks the remote branch being pushed to.
+
+```bash
+#!/bin/bash
+while read local_ref local_sha remote_ref remote_sha
+do
+ if [ "$remote_ref" = "refs/heads/main" ]; then
+ if [ "$ALLOW_MAIN_PUSH" != "true" ]; then
+ echo "❌ ERROR: Pushing to 'main' branch is prohibited."
+ exit 1
+ fi
+ fi
+done
+```
+
+---
+
+## 🆘 I'm on 'main' and have changes, what do I do?
+
+If you've already made changes on `main` and the hook blocks your commit, **don't panic and don't drop your stash!** You can easily move your work to a new branch.
+
+### The "Magic" Command: Just Create a New Branch
+Git allows you to create and switch to a new branch while keeping your uncommitted changes.
+
+```bash
+# 1. Create and switch to a new branch
+git checkout -b feature/my-cool-feature
+# OR (modern syntax)
+git switch -c feature/my-cool-feature
+
+# 2. Now you can commit normally
+git add .
+git commit -m "My feature changes"
+```
+
+### If you want to be extra safe (The Stash Method)
+If you have a lot of complex changes and want to ensure `main` stays clean:
+
+```bash
+# 1. Save your work temporarily
+git stash
+
+# 2. Create and switch to the new branch
+git checkout -b feature/my-cool-feature
+
+# 3. Bring your changes back
+git stash pop
+
+# 4. Commit
+git commit -am "My feature changes"
+```
+
+### "I accidentally committed before I added the hook!"
+If you have local commits on `main` that you haven't pushed yet, you can move them to a new branch:
+
+```bash
+# 1. Create a new branch at your current (accidental) commit
+git branch feature/my-feature
+
+# 2. Make sure your local reference to 'origin/main' is up-to-date
+git fetch origin
+# 3. Reset your local 'main' back to where it should be (the remote version)
+# (Fetching first ensures you don't accidentally reset to a stale origin/main reference)
+git reset --hard origin/main
+
+# 4. Switch to your new branch to continue working
+git checkout feature/my-feature
+```
+
diff --git a/packages/tempo/doc/installation.md b/packages/tempo/doc/installation.md
index 4c6c4982..05b4d7b9 100644
--- a/packages/tempo/doc/installation.md
+++ b/packages/tempo/doc/installation.md
@@ -8,7 +8,11 @@
`Temporal` is now at Stage 4 and is expected to land broadly in runtimes soon. To avoid needlessly inflating package size with a dependency that will increasingly become unnecessary, `Tempo` does not bundle a `Temporal` polyfill by default.
-As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support, while Node.js still does not provide built-in `Temporal` globally. Please verify support in your actual target runtime(s) and add a polyfill only when needed.
+As of 13 January 2026, Chrome 144 has shipped `Temporal`, and Firefox 139 also includes native `Temporal` support.
+
+While Node.js does not yet enable `Temporal` by default, recent versions (Node 20+) support it via the `--harmony-temporal` flag. This allows you to use `Tempo` without an external polyfill package.
+
+Please verify support in your actual target runtime(s) and add a polyfill only when needed.
You can check at runtime with a simple guard:
@@ -39,7 +43,15 @@ import { Tempo } from '@magmacomputing/tempo';
const t = new Tempo('next Friday');
```
-### Node.js quick-start (if Temporal is not available)
+### Node.js (with Native Temporal)
+
+If you are using Node.js 20+, you can enable native `Temporal` support without installing a polyfill:
+
+```bash
+node --harmony-temporal my-app.js
+```
+
+### Node.js (with Polyfill)
The polyfill import shown here is conditional guidance, not required for all environments.
diff --git a/packages/tempo/doc/tempo.layout.md b/packages/tempo/doc/tempo.layout.md
index 7a865dfc..d1fecea6 100644
--- a/packages/tempo/doc/tempo.layout.md
+++ b/packages/tempo/doc/tempo.layout.md
@@ -81,6 +81,18 @@ When crafting raw regex, the following capture groups are used by the engine:
- `per`: Period string offset
- `unt`: Relative unit (e.g., `days`, `weeks`)
+## Prototyping with `Tempo.regexp()`
+
+You can use the static `Tempo.regexp()` method to "preview" how a layout string will be compiled by the engine. This is useful for testing custom regex logic before applying it to your configuration.
+
+```typescript
+// Expands {dd}, {sep}, {mm}, etc. into a final anchored RegExp
+const regex = Tempo.regexp('{dd}{sep}{mm}{sep}{yy}');
+
+console.log(regex.source);
+// Output (illustrative): ^((?
...)(?...)(?...)(?...)(?...))$
+```
+
---
## Professional Services
diff --git a/packages/tempo/index.md b/packages/tempo/index.md
index e50cff6e..83be48fb 100644
--- a/packages/tempo/index.md
+++ b/packages/tempo/index.md
@@ -556,11 +556,12 @@ function focusActiveCard() {
}
.tempo-btn-brand {
- background-color: var(--vp-c-brand-1);
+ background-color: #3498db;
color: white;
}
.tempo-btn-brand:hover {
- background-color: var(--vp-c-brand-2);
+ background-color: #2980b9;
+ color: white;
}
.tempo-btn-alt {
diff --git a/packages/tempo/package.json b/packages/tempo/package.json
index bf436dff..5785a3cb 100644
--- a/packages/tempo/package.json
+++ b/packages/tempo/package.json
@@ -30,8 +30,6 @@
"**/plugin/extend/extend.*.ts",
"dist/engine/engine.*.js",
"src/engine/engine.*.ts",
- "dist/parse/parse.*.js",
- "src/parse/parse.*.ts",
"dist/module/module.*.js",
"src/module/module.*.ts",
"dist/discrete/discrete.*.js",
@@ -54,8 +52,8 @@
"default": "./dist/core.index.js"
},
"#tempo/enums": {
- "development": "./src/support/tempo.enum.ts",
- "default": "./dist/support/tempo.enum.js"
+ "development": "./src/support/support.enum.ts",
+ "default": "./dist/support/support.enum.js"
},
"#tempo/duration": {
"development": "./src/module/module.duration.ts",
@@ -109,10 +107,6 @@
"development": "./src/discrete/discrete.parse.ts",
"default": "./dist/discrete/discrete.parse.js"
},
- "#tempo/parse/*.js": {
- "development": "./src/parse/*.ts",
- "default": "./dist/parse/*.js"
- },
"#tempo/format": {
"development": "./src/discrete/discrete.format.ts",
"default": "./dist/discrete/discrete.format.js"
@@ -132,8 +126,8 @@
"import": "./dist/tempo.index.js"
},
"./enums": {
- "types": "./dist/support/tempo.enum.d.ts",
- "import": "./dist/support/tempo.enum.js"
+ "types": "./dist/support/support.enum.d.ts",
+ "import": "./dist/support/support.enum.js"
},
"./extend/*": {
"types": "./dist/plugin/extend/extend.*.d.ts",
diff --git a/packages/tempo/plan/RELEASE-D.md b/packages/tempo/plan/RELEASE-D.md
index f2caf2ec..54e8017a 100644
--- a/packages/tempo/plan/RELEASE-D.md
+++ b/packages/tempo/plan/RELEASE-D.md
@@ -5,20 +5,21 @@ This release focuses on modularizing and refactoring the parsing and pattern-mat
## Task Breakdown & Tracking
+
### Pattern Compiler + Cache Extraction
-- [ ] Extract `compileRegExp`, `setPatterns`, and helpers to new module
-- [ ] Integrate memoization/caching logic as needed
-- [ ] Refactor engine and consumers to use new module
-- [ ] Ensure compatibility with snippet/layout definitions
-- [ ] Add/expand unit tests for pattern logic and cache
-- [ ] Update documentation and references
+- [x] Extract `compileRegExp`, `setPatterns`, and helpers to new module (PatternCompiler)
+- [x] Integrate memoization/caching logic as needed (PatternCompiler cache)
+- [x] Refactor engine and consumers to use new PatternCompiler module
+- [x] Ensure compatibility with snippet/layout definitions
+- [x] Add/expand unit tests for pattern logic and cache
+- [x] Update documentation and references
### Alias Resolution Engine Extraction
-- [ ] Extract alias resolution logic to new module
-- [ ] Define interfaces for registration, lookup, collision
-- [ ] Refactor engine and plugins to use new APIs
-- [ ] Add/expand unit tests for alias/collision
-- [ ] Update documentation and references
+- [x] Extract alias resolution logic to new module
+- [x] Define interfaces for registration, lookup, collision
+- [x] Refactor engine and plugins to use new APIs
+- [x] Add/expand unit tests for alias/collision
+- [x] Update documentation and references
### Guard Builder Extraction (Assessment)
- [ ] Identify all guard-building/token-ingestion logic
diff --git a/packages/tempo/src/discrete/discrete.parse.ts b/packages/tempo/src/discrete/discrete.parse.ts
index 7745ff36..7f8ef9ff 100644
--- a/packages/tempo/src/discrete/discrete.parse.ts
+++ b/packages/tempo/src/discrete/discrete.parse.ts
@@ -16,8 +16,8 @@ import { defineInterpreterModule } from '../plugin/plugin.util.js';
import type { Range, ResolvedRange } from '../plugin/plugin.type.js';
import { sym, isTempo, TermError, getRuntime, Match } from '../support/support.index.js';
import { markConfig, setPatterns, init, extendState } from '../support/support.index.js';
-import { setProperty } from '#tempo/support/tempo.util.js';
-import enums from '../support/tempo.enum.js';
+import { setProperty } from '#tempo/support/support.util.js';
+import enums from '../support/support.enum.js';
import * as t from '../tempo.type.js';
import type { Tempo } from '../tempo.class.js';
diff --git a/packages/tempo/src/engine/engine.alias.ts b/packages/tempo/src/engine/engine.alias.ts
index dab78483..5b1561f3 100644
--- a/packages/tempo/src/engine/engine.alias.ts
+++ b/packages/tempo/src/engine/engine.alias.ts
@@ -86,17 +86,14 @@ export class AliasEngine {
this.#config = options.config;
this.#id = AliasEngine.#idCounter++;
- if (this.#parent) {
- if (!(this.#parent instanceof AliasEngine)) {
- const msg = "Parent engine must be an instance of AliasEngine";
- this.#logger?.error(this.#config, msg);
- throw new TypeError(msg);
- }
-
+ if (this.#parent instanceof AliasEngine) {
this.#depth = this.#parent.#depth + 1;
this.#state = Object.create(this.#parent.#state); // create a new state object that inherits from the parent engine's state
this.#words = Object.create(this.#parent.#words); // create a new words object that inherits from the parent engine's words for collision detection
} else {
+ if (this.#parent)
+ this.#logger?.error(this.#config, "Parent engine must be an instance of AliasEngine");
+
this.#depth = 0;
this.#state = Object.create(null); // initialize an empty state for the root engine (no parent)
this.#words = Object.create(null); // initialize an empty words object for the root engine (no parent)
diff --git a/packages/tempo/src/engine/engine.layout.ts b/packages/tempo/src/engine/engine.layout.ts
index a07293d0..4ca00e4c 100644
--- a/packages/tempo/src/engine/engine.layout.ts
+++ b/packages/tempo/src/engine/engine.layout.ts
@@ -1,9 +1,10 @@
import { ownEntries } from '#library/primitive.library.js';
-import { Token } from '#tempo/support/tempo.symbol.js';
+import { Token } from '#tempo/support/support.symbol.js';
+import { resolveLayoutOrderPure } from './engine.resolver.js';
import type * as t from '../tempo.type.js';
export type LayoutEntry = [symbol, string];
-export type LayoutController = Record;
+export type LayoutController = Record;
const TOKEN_ALIAS = new Map(
(ownEntries(Token, true) as [string, symbol][]).map(([name, key]) => [key, name])
@@ -51,19 +52,27 @@ export function resolveLayoutClassificationOrder(layout: Record,
if (preferred.length === 0) return layout;
const entries = ownEntries(layout) as LayoutEntry[];
- const byName = new Map();
+ const lookup = new Map();
entries.forEach(([key, value]) => {
+ lookup.set(key, [key, value]);
const description = key.description ?? '';
- if (description) byName.set(description, [key, value]);
+ if (description) lookup.set(description, [key, value]);
const alias = TOKEN_ALIAS.get(key);
- if (alias) byName.set(alias, [key, value]);
+ if (alias) lookup.set(alias, [key, value]);
});
const next: LayoutEntry[] = [];
const seen = new Set();
preferred.forEach(name => {
- const resolvedName = TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name;
- const entry = byName.get(resolvedName) ?? byName.get(name);
+ const isSym = typeof name === 'symbol';
+ const description = isSym ? (name.description ?? '') : '';
+ const alias = isSym ? TOKEN_ALIAS.get(name) : undefined;
+
+ const resolvedName = !isSym ? (TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name) : undefined;
+ const entry = isSym
+ ? (lookup.get(name) ?? lookup.get(description) ?? (alias ? lookup.get(alias) : undefined))
+ : (lookup.get(resolvedName!) ?? lookup.get(name));
+
if (!entry) return;
if (seen.has(entry[0])) return;
seen.add(entry[0]);
@@ -94,22 +103,12 @@ export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layout
classification ?? DEFAULT_LAYOUT_CLASS,
);
- const layouts = ownEntries(ordered) as LayoutEntry[];
- let changed = false;
+ const entries = ownEntries(ordered) as LayoutEntry[];
+ const layouts = resolveLayoutOrderPure(ordered, monthDayLayouts, isMonthDay);
- monthDayLayouts.forEach(([dmy, mdy]) => {
- const idx1 = layouts.findIndex(([key]) => key.description === dmy);
- const idx2 = layouts.findIndex(([key]) => key.description === mdy);
+ if (layouts.length !== entries.length) return ordered;
- if (idx1 === -1 || idx2 === -1) return;
-
- const swap1 = idx1 < idx2 && isMonthDay;
- const swap2 = idx1 > idx2 && !isMonthDay;
- if (swap1 || swap2) {
- [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
- changed = true;
- }
- });
+ const changed = layouts.some((entry, idx) => entry[0] !== entries[idx][0]);
if (changed) return Object.fromEntries(layouts) as Record;
return ordered;
diff --git a/packages/tempo/src/engine/engine.pattern.ts b/packages/tempo/src/engine/engine.pattern.ts
new file mode 100644
index 00000000..aca9bb04
--- /dev/null
+++ b/packages/tempo/src/engine/engine.pattern.ts
@@ -0,0 +1,167 @@
+// engine.pattern.ts
+// Pattern Compiler and Cache Engine for Tempo
+// Responsible for snippet/layout expansion, regex compilation, and pattern caching
+
+import { isRegExp, isNullish, isEmpty, isString } from '#library/assertion.library.js';
+import { ownEntries, ownKeys } from '#library/primitive.library.js';
+import { Match, Snippet, Layout } from '../support/support.default.js';
+import { getSymbol, hasOwn, logWarn, logError } from '../support/support.util.js';
+import { Token } from '../support/support.symbol.js';
+import enums from '../support/support.enum.js';
+import type * as t from '../tempo.type.js';
+
+const BRACES_REGEX = new RegExp(Match.braces, 'g');
+
+export interface PatternCompilerOptions {
+ state: t.Internal.State;
+}
+
+export class PatternCompiler {
+ #state: t.Internal.State;
+ #globalCache: Map = new Map();
+ #snippetCache: WeakMap> = new WeakMap();
+
+ constructor(options: PatternCompilerOptions) {
+ this.#state = options.state;
+ }
+
+ /**
+ * Translates {layout} into an anchored, case-insensitive RegExp.
+ * Includes recursive expansion of placeholders using snippet registries.
+ */
+ compileRegExp(layout: string | RegExp, snippet?: Snippet): RegExp {
+ const state = this.#state;
+ const source = isRegExp(layout) ? layout.source : layout;
+ let cache: Map;
+ if (snippet) {
+ if (!this.#snippetCache.has(snippet)) {
+ this.#snippetCache.set(snippet, new Map());
+ }
+ cache = this.#snippetCache.get(snippet)!;
+ } else {
+ cache = this.#globalCache;
+ }
+ if (cache.has(source)) {
+ return cache.get(source)!;
+ }
+
+ const matcher = (source: string, d = 0): string => {
+ if (d > 10) { // Emit a diagnostic if recursion limit is hit (likely circular placeholder)
+ logWarn(this.#state.config, `[PatternCompiler] Recursion limit exceeded in matcher (d > 10) for src:`, source, `depth:`, d);
+ return source;
+ }
+
+ if (source.startsWith('/') && source.endsWith('/'))
+ source = source.substring(1, source.length - 1); // remove the leading/trailing "/"
+ if (source.startsWith('^') && source.endsWith('$'))
+ source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $)
+
+ return source.replace(BRACES_REGEX, (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;
+ const stateLayout = state.parse.layout[token as keyof Layout] ?? state.parse.layout[name as keyof Layout];
+ const defaultLayout = Layout[token as keyof Layout];// get resolution source (layout)
+
+ let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source
+
+ if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback
+ const prefix = name.split('.')[0]; // get the base token name
+ const pToken = getSymbol(prefix);
+ res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source
+ ?? state.parse.snippet[pToken as keyof Snippet]?.source ?? state.parse.snippet[prefix as keyof Snippet]?.source
+ ?? state.parse.layout[pToken as keyof Layout] ?? state.parse.layout[prefix as keyof Layout]
+ ?? Layout[pToken as keyof Layout];
+ }
+
+ if (res && name.includes('.')) { // wrap dotted extensions for identification
+ const safeName = name.replace(/\./g, '_');
+ if (!res.startsWith(`(?<${safeName}>`))
+ res = `(?<${safeName}>${res})`;
+ }
+
+ return (isNullish(res) || res === match) // if no definition found,
+ ? match // return the original match
+ : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs
+ });
+ };
+
+ try {
+ const expanded = matcher(source);
+ const compiled = new RegExp(`^(${expanded})$`, 'i');
+ cache.set(source, compiled);
+ return compiled;
+ } catch (e: any) {
+ // Use the computed source for fallback, do not cache fallback, and log error
+ logError(this.#state.config, { context: 'pattern compile failed', pattern: source }, e);
+ return new RegExp(`^${Match.escape(source)}$`, 'i');
+ }
+ }
+
+ /**
+ * Build RegExp patterns into the state.
+ * Re-evaluates all snippets and layouts.
+ */
+ setPatterns() {
+ this.clearCache();
+ const state = this.#state;
+ // ensure we have our own isolated mutable containers before mutation
+ state.parse.snippet = { ...state.parse.snippet };
+ state.parse.pattern = new Map();
+
+ const snippet = state.parse.snippet;
+
+ // 1. ensure numeric snippets are current
+ if (enums?.NUMBER) {
+ const keys = Object.keys(enums.NUMBER).map(w => Match.escape(w));
+ const nbr = new RegExp(`(?[0-9]+|${keys.sort((a, b) => b.length - a.length).join('|')})`);
+
+ snippet[Token.nbr] = nbr;
+ snippet[Token.mod] = new RegExp(`((?${Match.modifier.source})?${nbr.source}? *)`);
+ snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep]?.source || ''}?`);
+ }
+
+ // 2. build ignore pattern
+ const ignores = ownKeys(state.parse.ignore, true);
+
+ if (!isEmpty(ignores)) {
+ const words = ignores
+ .filter(isString)
+ .map(w => Match.escape(w.toLowerCase()))
+ .join('|');
+
+ state.parse.ignorePattern = new RegExp(`\\b(${words})\\b`, 'gi');
+ } else {
+ delete state.parse.ignorePattern;
+ }
+
+ // 3. build the patterns
+ ownEntries(state.parse.layout, true).forEach(([key, layout]) => {
+ const symbol = getSymbol(key);
+ const compiled = this.compileRegExp(layout, snippet);
+
+ state.parse.pattern.set(symbol, compiled);
+ });
+ }
+
+ /**
+ * Clear the pattern cache.
+ */
+ clearCache() {
+ this.#globalCache.clear();
+ // WeakMap has no clear(), so re-instantiate to drop all snippet-specific caches
+ this.#snippetCache = new WeakMap();
+ }
+}
+
+/**
+ * Functional wrapper for the PatternCompiler.
+ * Handles engine instantiation and pattern building for a given state.
+ */
+export function setPatterns(state: t.Internal.State) {
+ // 🛡️ Critical fix: ensure we use an OWN PatternCompiler for each state to avoid cross-pollution
+ if (!hasOwn(state, 'patternCompiler') || !state.patternCompiler)
+ state.patternCompiler = new PatternCompiler({ state });
+
+ state.patternCompiler.setPatterns();
+}
diff --git a/packages/tempo/src/engine/engine.layout.resolver.ts b/packages/tempo/src/engine/engine.resolver.ts
similarity index 100%
rename from packages/tempo/src/engine/engine.layout.resolver.ts
rename to packages/tempo/src/engine/engine.resolver.ts
diff --git a/packages/tempo/src/parse/parse.layout.ts b/packages/tempo/src/parse/parse.layout.ts
deleted file mode 100644
index ae4da410..00000000
--- a/packages/tempo/src/parse/parse.layout.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { ownEntries } from '#library/primitive.library.js';
-import { Token } from '#tempo/support/tempo.symbol.js';
-import type * as t from '../tempo.type.js';
-
-export type LayoutEntry = [symbol, string];
-export type LayoutController = Record;
-
-const TOKEN_ALIAS = new Map(
- (ownEntries(Token, true) as [string, symbol][]).map(([name, key]) => [key, name])
-);
-
-const TOKEN_DESCRIPTION_BY_NAME = new Map(
- (ownEntries(Token, true) as [string, symbol][])
- .map(([name, key]) => [name, key.description ?? ''] as const)
- .filter(([, description]) => description.length > 0)
-);
-
-export const DEFAULT_LAYOUT_CLASS: unique symbol = Symbol('default');
-
-export interface ResolveLayoutOrderArgs {
- layout: Record;
- monthDayLayouts: t.LayoutPair[] | readonly t.LayoutPair[];
- isMonthDay: boolean;
- layoutController?: LayoutController;
- classification?: PropertyKey;
-}
-
-export function createLayoutController(layout: Record): LayoutController {
- return {
- [DEFAULT_LAYOUT_CLASS]: getLayoutOrder(layout),
- }
-}
-
-export function resolveLayoutClassificationOrder(layout: Record, controller: LayoutController, classification: PropertyKey = DEFAULT_LAYOUT_CLASS): Record {
- const preferred = controller[classification] ?? [];
- if (preferred.length === 0) return layout;
-
- const entries = ownEntries(layout) as LayoutEntry[];
- const byName = new Map();
- entries.forEach(([key, value]) => {
- const description = key.description ?? '';
- if (description) byName.set(description, [key, value]);
- const alias = TOKEN_ALIAS.get(key);
- if (alias) byName.set(alias, [key, value]);
- });
- const next: LayoutEntry[] = [];
- const seen = new Set();
-
- preferred.forEach(name => {
- const isSym = typeof name === 'symbol';
- const description = isSym ? (name.description ?? '') : '';
- const alias = isSym ? TOKEN_ALIAS.get(name) : undefined;
-
- const resolvedName = !isSym ? (TOKEN_DESCRIPTION_BY_NAME.get(name) ?? name) : undefined;
- const entry = isSym
- ? (byName.get(description) ?? (alias ? byName.get(alias) : undefined))
- : (byName.get(resolvedName!) ?? byName.get(name));
-
- if (!entry) return;
- if (seen.has(entry[0])) return;
- seen.add(entry[0]);
- next.push(entry);
- });
-
- entries.forEach(entry => {
- if (!seen.has(entry[0])) next.push(entry);
- });
-
- const changed = next.length === entries.length && next.some((entry, idx) => entry[0] !== entries[idx][0]);
- return changed ? Object.fromEntries(next) as Record : layout;
-}
-
-export function resolveLayoutOrder({ layout, monthDayLayouts, isMonthDay, layoutController, classification }: ResolveLayoutOrderArgs): Record {
- const ordered = resolveLayoutClassificationOrder(
- layout,
- layoutController ?? createLayoutController(layout),
- classification ?? DEFAULT_LAYOUT_CLASS,
- );
-
- const layouts = ownEntries(ordered) as LayoutEntry[];
- let changed = false;
-
- monthDayLayouts.forEach(([dmy, mdy]) => {
- const idx1 = layouts.findIndex(([key]) => key.description === dmy);
- const idx2 = layouts.findIndex(([key]) => key.description === mdy);
-
- if (idx1 === -1 || idx2 === -1) return;
-
- const swap1 = idx1 < idx2 && isMonthDay;
- const swap2 = idx1 > idx2 && !isMonthDay;
- if (swap1 || swap2) {
- [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
- changed = true;
- }
- });
-
- if (changed) return Object.fromEntries(layouts) as Record;
- return ordered;
-}
-
-export function getLayoutOrder(layout: Record): string[] {
- return (ownEntries(layout) as LayoutEntry[])
- .map(([key]) => key.description ?? String(key));
-}
diff --git a/packages/tempo/src/parse/parse.resolver.ts b/packages/tempo/src/parse/parse.resolver.ts
deleted file mode 100644
index a03bcebe..00000000
--- a/packages/tempo/src/parse/parse.resolver.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ownEntries } from '#library/primitive.library.js';
-import type * as t from '../tempo.type.js';
-
-export type LayoutEntry = [symbol, string];
-
-export function resolveLayoutOrderPure(
- layout: Record,
- monthDayLayouts: t.LayoutPair[] | readonly t.LayoutPair[],
- isMonthDay: boolean
-): LayoutEntry[] {
- const layouts = ownEntries(layout) as LayoutEntry[];
- let changed = false;
-
- monthDayLayouts.forEach(([dmy, mdy]) => {
- const idx1 = layouts.findIndex(([key]) => key.description === dmy);
- const idx2 = layouts.findIndex(([key]) => key.description === mdy);
- if (idx1 === -1 || idx2 === -1) return;
- const swap1 = idx1 < idx2 && isMonthDay;
- const swap2 = idx1 > idx2 && !isMonthDay;
- if (swap1 || swap2) {
- [layouts[idx1], layouts[idx2]] = [layouts[idx2], layouts[idx1]];
- changed = true;
- }
- });
-
- return layouts;
-}
diff --git a/packages/tempo/src/plugin/plugin.util.ts b/packages/tempo/src/plugin/plugin.util.ts
index 83023935..d974a060 100644
--- a/packages/tempo/src/plugin/plugin.util.ts
+++ b/packages/tempo/src/plugin/plugin.util.ts
@@ -2,7 +2,7 @@ import { isFunction, isString, isUndefined, isClass, isObject, isDefined } from
import { secureRef } from '#library/proxy.library.js';
import { sym, getRuntime, isTempo } from '#tempo/support';
-import { hasOwn } from '#tempo/support/tempo.util.js';
+import { hasOwn } from '#tempo/support/support.util.js';
import type { Tempo } from '../tempo.class.js';
import type { Plugin } from './plugin.type.js';
diff --git a/packages/tempo/src/plugin/term/term.quarter.ts b/packages/tempo/src/plugin/term/term.quarter.ts
index 51a48571..b1fec7c5 100644
--- a/packages/tempo/src/plugin/term/term.quarter.ts
+++ b/packages/tempo/src/plugin/term/term.quarter.ts
@@ -1,5 +1,5 @@
import { defineTerm, getTermRange, defineRange, resolveCycleWindow } from '../term.util.js';
-import { COMPASS } from '../../support/tempo.enum.js';
+import { COMPASS } from '../../support/support.enum.js';
import { isNumber } from '#library/assertion.library.js';
import { asArray } from '#library';
import type { Tempo } from '../../tempo.class.js';
diff --git a/packages/tempo/src/plugin/term/term.season.ts b/packages/tempo/src/plugin/term/term.season.ts
index 49024ce1..02321db6 100644
--- a/packages/tempo/src/plugin/term/term.season.ts
+++ b/packages/tempo/src/plugin/term/term.season.ts
@@ -1,5 +1,5 @@
import { getTermRange, defineTerm, defineRange, resolveCycleWindow } from '../term.util.js';
-import { COMPASS } from '../../support/tempo.enum.js';
+import { COMPASS } from '../../support/support.enum.js';
import type { Tempo } from '../../tempo.class.js';
/** definition of meteorological season ranges */
diff --git a/packages/tempo/src/support/tempo.default.ts b/packages/tempo/src/support/support.default.ts
similarity index 98%
rename from packages/tempo/src/support/tempo.default.ts
rename to packages/tempo/src/support/support.default.ts
index e76aa748..ce55d52e 100644
--- a/packages/tempo/src/support/tempo.default.ts
+++ b/packages/tempo/src/support/support.default.ts
@@ -2,9 +2,9 @@ import { looseIndex } from '#library/object.library.js';
import { secure, proxify } from '#library/proxy.library.js';
import { getDateTimeFormat } from '#library/international.library.js';
-import { NUMBER, MODE, MONTH_DAY } from './tempo.enum.js';
-import { Token } from './tempo.symbol.js';
-import { IntlDefault } from './tempo.intl.js';
+import { NUMBER, MODE, MONTH_DAY } from './support.enum.js';
+import { Token } from './support.symbol.js';
+import { IntlDefault } from './support.intl.js';
import type { Options } from '../tempo.type.js';
import type { Tempo } from '../tempo.class.js';
diff --git a/packages/tempo/src/support/tempo.enum.ts b/packages/tempo/src/support/support.enum.ts
similarity index 99%
rename from packages/tempo/src/support/tempo.enum.ts
rename to packages/tempo/src/support/support.enum.ts
index d91e5776..3f43a220 100644
--- a/packages/tempo/src/support/tempo.enum.ts
+++ b/packages/tempo/src/support/support.enum.ts
@@ -1,4 +1,4 @@
-import { sym } from './tempo.symbol.js';
+import { sym } from './support.symbol.js';
import { enumify, Enum } from '#library/enumerate.library.js';
import { proxify } from '#library/proxy.library.js';
import { allDescriptors } from '#library/reflection.library.js';
diff --git a/packages/tempo/src/support/support.index.ts b/packages/tempo/src/support/support.index.ts
index 71d06f0c..0b68965e 100644
--- a/packages/tempo/src/support/support.index.ts
+++ b/packages/tempo/src/support/support.index.ts
@@ -24,13 +24,14 @@ export {
PARSE,
MONTH_DAY,
NumericPattern
-} from './tempo.enum.js';
+} from './support.enum.js';
export { markConfig } from '#library/symbol.library.js';
-export { sym, isTempo, Token, TermError, type TempoBrand } from './tempo.symbol.js';
-export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $Logify, $Discover, $ImmutableSkip } from './tempo.symbol.js';
-export { registryUpdate, registryReset, onRegistryReset } from './tempo.register.js';
-export { getRuntime, TempoRuntime } from './tempo.runtime.js';
-export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './tempo.default.js';
-export { SCHEMA, getLargestUnit, setPatterns, logError, logWarn, logDebug } from './tempo.util.js';
-export { init, extendState } from './tempo.init.js';
\ No newline at end of file
+export { sym, isTempo, Token, TermError, type TempoBrand } from './support.symbol.js';
+export { $Tempo, $Register, $Interpreter, $logError, $logDebug, $dbg, $guard, $errored, $Internal, $Bridge, $RuntimeBrand, $Descriptor, $setConfig, $setDiscovery, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Identity, $Logify, $Discover, $ImmutableSkip } from './support.symbol.js';
+export { registryUpdate, registryReset, onRegistryReset } from './support.register.js';
+export { getRuntime, TempoRuntime } from './support.runtime.js';
+export { Match, Snippet, Layout, Event, Period, Ignore, Guard, Default } from './support.default.js';
+export { SCHEMA, getLargestUnit, logError, logWarn, logDebug } from './support.util.js';
+export { setPatterns } from '../engine/engine.pattern.js';
+export { init, extendState } from './support.init.js';
\ No newline at end of file
diff --git a/packages/tempo/src/support/tempo.init.ts b/packages/tempo/src/support/support.init.ts
similarity index 93%
rename from packages/tempo/src/support/tempo.init.ts
rename to packages/tempo/src/support/support.init.ts
index 9241a92e..206cee97 100644
--- a/packages/tempo/src/support/tempo.init.ts
+++ b/packages/tempo/src/support/support.init.ts
@@ -8,11 +8,11 @@ import { asType } from '#library/type.library.js';
import { isString, isObject, isUndefined, isDefined, isRegExp } from '#library/assertion.library.js';
import { ownEntries } from '#library/primitive.library.js';
-import { getRuntime } from './tempo.runtime.js';
-import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay } from './tempo.util.js';
-import { sym, Token } from './tempo.symbol.js';
-import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './tempo.default.js';
-import enums, { STATE } from './tempo.enum.js';
+import { getRuntime } from './support.runtime.js';
+import { setProperty, setProperties, hasOwn, create, collect, normalizeLayoutOrder, resolveMonthDay, logError } from './support.util.js';
+import { sym, Token } from './support.symbol.js';
+import { Match, Snippet, Layout, Event, Period, Ignore, Default } from './support.default.js';
+import enums, { STATE } from './support.enum.js';
import * as t from '../tempo.type.js';
/** @internal Initialise a Tempo state */
@@ -141,8 +141,14 @@ export function extendState(state: t.Internal.State, options: t.Options) {
if (optKey === 'snippet') {
const pattern = isRegExp(v) ? v.source : String(v);
// 🛡️ Security Check: Prevent catastrophic backtracking and malicious patterns
- if (pattern.length > 500) throw new Error(`[Tempo#extend] Snippet pattern too long (max 500 chars).`);
- if (Match.backtrack.test(pattern)) throw new Error(`[Tempo#extend] Snippet contains suspicious nested quantifiers.`);
+ if (pattern.length > 500) {
+ logError(state.config, `[Tempo#extend] Snippet pattern too long (max 500 chars).`);
+ return new RegExp(Match.escape(pattern));
+ }
+ if (Match.backtrack.test(pattern)) {
+ logError(state.config, `[Tempo#extend] Snippet contains suspicious nested quantifiers.`);
+ return new RegExp(Match.escape(pattern));
+ }
return new RegExp(pattern);
}
return isRegExp(v) ? v.source : v;
diff --git a/packages/tempo/src/support/tempo.intl.ts b/packages/tempo/src/support/support.intl.ts
similarity index 100%
rename from packages/tempo/src/support/tempo.intl.ts
rename to packages/tempo/src/support/support.intl.ts
diff --git a/packages/tempo/src/support/tempo.register.ts b/packages/tempo/src/support/support.register.ts
similarity index 94%
rename from packages/tempo/src/support/tempo.register.ts
rename to packages/tempo/src/support/support.register.ts
index 1bf410d8..2859f9e6 100644
--- a/packages/tempo/src/support/tempo.register.ts
+++ b/packages/tempo/src/support/support.register.ts
@@ -3,14 +3,14 @@ import { isEqual } from '#library/object.library.js';
import { isDefined, isObject, isSymbol, isUndefined } from '#library/assertion.library.js';
import { ownKeys } from '#library/primitive.library.js';
import { unwrap } from '#library/primitive.library.js';
-import { sym } from './tempo.symbol.js';
+import { sym } from './support.symbol.js';
import type { Property } from '#library/type.library.js';
-import { getRuntime } from './tempo.runtime.js';
-import { hasOwn, setProperty } from './tempo.util.js';
+import { getRuntime } from './support.runtime.js';
+import { hasOwn, setProperty } from './support.util.js';
// Import the live enums and their mutable state from the enum module
-import { STATE, REGISTRIES, DEFAULTS } from './tempo.enum.js';
+import { STATE, REGISTRIES, DEFAULTS } from './support.enum.js';
const rt = getRuntime();
diff --git a/packages/tempo/src/support/tempo.runtime.ts b/packages/tempo/src/support/support.runtime.ts
similarity index 99%
rename from packages/tempo/src/support/tempo.runtime.ts
rename to packages/tempo/src/support/support.runtime.ts
index 3e439c65..df2fbcc7 100644
--- a/packages/tempo/src/support/tempo.runtime.ts
+++ b/packages/tempo/src/support/support.runtime.ts
@@ -1,4 +1,4 @@
-import { sym } from './tempo.symbol.js';
+import { sym } from './support.symbol.js';
import type { TermPlugin, Extension, Plugin } from '../plugin/plugin.type.js';
import type { Internal } from '../tempo.type.js';
diff --git a/packages/tempo/src/support/tempo.symbol.ts b/packages/tempo/src/support/support.symbol.ts
similarity index 59%
rename from packages/tempo/src/support/tempo.symbol.ts
rename to packages/tempo/src/support/support.symbol.ts
index 8ef15d7c..ea0d5222 100644
--- a/packages/tempo/src/support/tempo.symbol.ts
+++ b/packages/tempo/src/support/support.symbol.ts
@@ -13,26 +13,26 @@ export const isTempo = (tempo?: any): tempo is TempoBrand => Boolean(tempo?.[sym
export const TermError: unique symbol = Symbol.for('magmacomputing/tempo/termError') as any;
/** @internal unique symbols for critical internal accessors */
-/** key for Global Discovery of Tempo configuration */ export const $Tempo: unique symbol = Symbol.for('$Tempo') as any;
-/** key for Reactive Plugin Registration */ export const $Register: unique symbol = Symbol.for('magmacomputing/tempo/register') as any;
-/** key for Internal Interpreter Service */ export const $Interpreter: unique symbol = Symbol.for('magmacomputing/tempo/interpreter') as any;
-/** key for contextual Error Logging */ export const $logError: unique symbol = Symbol.for('magmacomputing/tempo/logError') as any;
-/** key for contextual Debug Logging */ export const $logDebug: unique symbol = Symbol.for('magmacomputing/tempo/logDebug') as any;
-/** key for contextual Debugger */ export const $dbg: unique symbol = Symbol.for('magmacomputing/tempo/dbg') as any;
-/** key for Master Guard */ export const $guard: unique symbol = Symbol.for('magmacomputing/tempo/guard') as any;
-/** internal key for signaling pre-errored state */ export const $errored: unique symbol = Symbol.for('magmacomputing/tempo/errored') as any;
-/** internal key for accessing private instance state */ export const $Internal: unique symbol = Symbol.for('magmacomputing/tempo/internal') as any;
-/** hardened globalThis bridge key for the TempoRuntime */export const $Bridge: unique symbol = Symbol.for('magmacomputing/tempo/runtime') as any;
-/** cross-bundle brand check for TempoRuntime */ export const $RuntimeBrand: unique symbol = Symbol.for('magmacomputing/tempo/runtime/brand') as any;
-/** branding for explicit PropertyDescriptors */ export const $Descriptor: unique symbol = Symbol.for('magmacomputing/tempo/descriptor') as any;
+/** key for Global Discovery of Tempo configuration */ export const $Tempo: unique symbol = Symbol.for('$Tempo') as any;
+/** key for Reactive Plugin Registration */ export const $Register: unique symbol = Symbol.for('magmacomputing/tempo/register') as any;
+/** key for Internal Interpreter Service */ export const $Interpreter: unique symbol = Symbol.for('magmacomputing/tempo/interpreter') as any;
+/** key for contextual Error Logging */ export const $logError: unique symbol = Symbol.for('magmacomputing/tempo/logError') as any;
+/** key for contextual Debug Logging */ export const $logDebug: unique symbol = Symbol.for('magmacomputing/tempo/logDebug') as any;
+/** key for contextual Debugger */ export const $dbg: unique symbol = Symbol.for('magmacomputing/tempo/dbg') as any;
+/** key for Master Guard */ export const $guard: unique symbol = Symbol.for('magmacomputing/tempo/guard') as any;
+/** internal key for signaling pre-errored state */ export const $errored: unique symbol = Symbol.for('magmacomputing/tempo/errored') as any;
+/** internal key for accessing private instance state */ export const $Internal: unique symbol = Symbol.for('magmacomputing/tempo/internal') as any;
+/** hardened globalThis bridge key for the TempoRuntime */ export const $Bridge: unique symbol = Symbol.for('magmacomputing/tempo/runtime') as any;
+/** cross-bundle brand check for TempoRuntime */ export const $RuntimeBrand: unique symbol = Symbol.for('magmacomputing/tempo/runtime/brand') as any;
+/** branding for explicit PropertyDescriptors */ export const $Descriptor: unique symbol = Symbol.for('magmacomputing/tempo/descriptor') as any;
-/** internal static config helper */ export const $setConfig: unique symbol = Symbol.for('magmacomputing/tempo/setConfig') as any;
-/** internal static discovery helper */ export const $setDiscovery: unique symbol = Symbol.for('magmacomputing/tempo/setDiscovery') as any;
-/** internal static event builder */ export const $setEvents: unique symbol = Symbol.for('magmacomputing/tempo/setEvents') as any;
-/** internal static period builder */ export const $setPeriods: unique symbol = Symbol.for('magmacomputing/tempo/setPeriods') as any;
-/** internal static alias builder */ export const $setAliases: unique symbol = Symbol.for('magmacomputing/tempo/setAliases') as any;
-/** internal static guard builder */ export const $buildGuard: unique symbol = Symbol.for('magmacomputing/tempo/buildGuard') as any;
-/** internal static base class marker */ export const $IsBase: unique symbol = Symbol.for('magmacomputing/tempo/isBase') as any;
+/** internal static config helper */ export const $setConfig: unique symbol = Symbol.for('magmacomputing/tempo/setConfig') as any;
+/** internal static discovery helper */ export const $setDiscovery: unique symbol = Symbol.for('magmacomputing/tempo/setDiscovery') as any;
+/** internal static event builder */ export const $setEvents: unique symbol = Symbol.for('magmacomputing/tempo/setEvents') as any;
+/** internal static period builder */ export const $setPeriods: unique symbol = Symbol.for('magmacomputing/tempo/setPeriods') as any;
+/** internal static alias builder */ export const $setAliases: unique symbol = Symbol.for('magmacomputing/tempo/setAliases') as any;
+/** internal static guard builder */ export const $buildGuard: unique symbol = Symbol.for('magmacomputing/tempo/buildGuard') as any;
+/** internal static base class marker */ export const $IsBase: unique symbol = Symbol.for('magmacomputing/tempo/isBase') as any;
/** @internal Tempo Symbol Registry (Local Keys) */
const local = {
diff --git a/packages/tempo/src/support/tempo.util.ts b/packages/tempo/src/support/support.util.ts
similarity index 52%
rename from packages/tempo/src/support/tempo.util.ts
rename to packages/tempo/src/support/support.util.ts
index c43beb24..859bfc30 100644
--- a/packages/tempo/src/support/tempo.util.ts
+++ b/packages/tempo/src/support/support.util.ts
@@ -1,13 +1,13 @@
import { isBoolean } from '#library/assertion.library.js';
-import { sym, Token } from './tempo.symbol.js';
+import { sym, Token } from './support.symbol.js';
import { asType } from '#library/type.library.js';
import { asArray } from '#library/coercion.library.js';
import { isSymbol, isUndefined, isDefined, isString, isRegExp, isNullish, isObject, isEmpty } from '#library/assertion.library.js';
-import { ownEntries, ownKeys } from '#library/primitive.library.js';
-import { getRuntime } from './tempo.runtime.js';
-import { Match, Snippet, Layout } from './tempo.default.js';
-import enums from './tempo.enum.js';
+import { ownEntries, ownKeys, unwrap } from '#library/primitive.library.js';
+import { getRuntime } from './support.runtime.js';
+import { Match, Snippet, Layout } from './support.default.js';
+import enums from './support.enum.js';
import type * as t from '../tempo.type.js';
/** @internal normalize layout-order options into a clean string array */
@@ -49,17 +49,30 @@ export function logDebug(config: any, ...msg: any[]) {
rt.logger?.debug(config ?? rt.state?.config, ...msg);
}
-/** @internal return the Prototype parent of an object */
-export const proto = (obj: object) => Object.getPrototypeOf(obj);
+/** @internal check if an object is a proxy */
+export const isProxy = (obj: any): boolean => !!obj && !!(obj as any)[sym.$Target];
-/** @internal test object has own property with the given key */
-export const hasOwn = (obj: object, key: PropertyKey) => Object.hasOwn(obj, key);
+/** @internal check if an object has an own property (respects Proxy/Shadowing) */
+export const hasOwn = (obj: any, key: PropertyKey): boolean => {
+ return Object.hasOwn(unwrap(obj), key);
+}
+
+/** @internal get the prototype of an object */
+export const proto = (obj: any): any => Object.getPrototypeOf(unwrap(obj));
+
+/** @internal create a new shadowed object from a prototype */
+export function create(obj: any, name: string): T {
+ const prototype = proto(obj);
+ if (!isObject(prototype)) {
+ logError(null, `[Tempo#create] Failed to create shadowed object for '${name}'. Proto(obj) is null or not an object.`);
+ return {} as T;
+ }
-/** @internal create an object based on a prototype */
-export const create = (obj: object, name: string): T => {
- const entry = proto(obj)[name];
- if (!isObject(entry))
- 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}).`);
+ const entry = prototype[name];
+ if (!isObject(entry)) {
+ logError(null, `[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 {} as T;
+ }
return { ...entry } as T;
}
@@ -115,97 +128,6 @@ export function getLargestUnit(list: any[]): string {
return 'nanosecond';
}
-/** @internal translates {layout} into an anchored, case-insensitive RegExp. */
-export function compileRegExp(layout: string | RegExp, state: t.Internal.State, snippet?: Snippet) {
- // helper function to replace {name} placeholders with their corresponding snippets
- const matcher = (source: string, d = 0): string => {
- if (d > 10) return source; // prevent infinite recursion
-
- if (source.startsWith('/') && source.endsWith('/'))
- source = source.substring(1, source.length - 1); // remove the leading/trailing "/"
- if (source.startsWith('^') && source.endsWith('$'))
- source = source.substring(1, source.length - 1); // remove the leading/trailing anchors (^ $)
-
- 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;
- const stateLayout = state.parse.layout[token as keyof Layout] ?? state.parse.layout[name as keyof Layout];
- const defaultLayout = Layout[token as keyof Layout]; // get resolution source (layout)
-
- let res = customs ?? globals ?? stateLayout ?? defaultLayout; // get the snippet/layout source
-
- if (isNullish(res) && name.includes('.')) { // if no definition found, try fallback
- const prefix = name.split('.')[0]; // get the base token name
- const pToken = getSymbol(prefix);
- res = snippet?.[pToken as keyof Snippet]?.source ?? snippet?.[prefix as keyof Snippet]?.source
- ?? state.parse.snippet[pToken as keyof Snippet]?.source ?? state.parse.snippet[prefix as keyof Snippet]?.source
- ?? state.parse.layout[pToken as keyof Layout] ?? state.parse.layout[prefix as keyof Layout]
- ?? Layout[pToken as keyof Layout];
- }
-
- if (res && name.includes('.')) { // wrap dotted extensions for identification
- const safeName = name.replace(/\./g, '_');
- if (!res.startsWith(`(?<${safeName}>`))
- res = `(?<${safeName}>${res})`;
- }
-
- return (isNullish(res) || res === match) // if no definition found,
- ? match // return the original match
- : matcher(res, d + 1); // else recurse to see if snippet contains embedded "{}" pairs
- });
- };
-
- try {
- const source = isRegExp(layout) ? layout.source : layout;
- const expanded = matcher(source);
- return new RegExp(`^(${expanded})$`, 'i');
- } catch (e: any) {
- return new RegExp(`^${Match.escape(layout as string)}$`, 'i');
- }
-}
-
-/** @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();
-
- const snippet = state.parse.snippet;
-
- // 1. ensure numeric snippets are current
- if (enums?.NUMBER) {
- const keys = Object.keys(enums.NUMBER).map(w => Match.escape(w)); // escape each key
- const nbr = new RegExp(`(?[0-9]+|${keys.sort((a, b) => b.length - a.length).join('|')})`);
-
- snippet[Token.nbr] = nbr;
- snippet[Token.mod] = new RegExp(`((?${Match.modifier.source})?${nbr.source}? *)`);
- snippet[Token.afx] = new RegExp(`((s)? (?${Match.affix.source}))?${snippet[Token.sep].source}?`);
- }
-
- // 2. build ignore pattern
- const ignores = ownKeys(state.parse.ignore, true);
-
- if (!isEmpty(ignores)) {
- const words = ignores
- .filter(isString)
- .map(w => Match.escape(w.toLowerCase()))
- .join('|');
-
- state.parse.ignorePattern = new RegExp(`\\b(${words})\\b`, 'gi');
- } else {
- delete state.parse.ignorePattern;
- }
-
- // 3. build the patterns
- ownEntries(state.parse.layout).forEach(([key, layout]) => {
- const symbol = getSymbol(key);
- const compiled = compileRegExp(layout, state, snippet);
-
- state.parse.pattern.set(symbol, compiled);
- });
-}
-
/**
* @internal Normalize a MonthDay configuration value against a base.
* @param value The user-supplied value to normalize
diff --git a/packages/tempo/src/tempo.class.ts b/packages/tempo/src/tempo.class.ts
index 18705c63..3004eb38 100644
--- a/packages/tempo/src/tempo.class.ts
+++ b/packages/tempo/src/tempo.class.ts
@@ -22,11 +22,11 @@ import { registerTerm, getTermRange } from './plugin/term.util.js';
import type { TermPlugin, Plugin } from './plugin/plugin.type.js';
import { AliasEngine } from './engine/engine.alias.js';
-import { resolveMonthDay } from './support/tempo.util.js';
-import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './parse/parse.layout.js';
-import { datePattern } from './support/tempo.default.js';
-import { setProperty, proto, hasOwn, compileRegExp, setPatterns, normalizeLayoutOrder } from './support/tempo.util.js';
-import { sym, markConfig, TermError, getRuntime, init, extendState, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support';
+import { PatternCompiler } from './engine/engine.pattern.js';
+import { resolveMonthDay, setProperty, proto, hasOwn, normalizeLayoutOrder } from './support/support.util.js';
+import { DEFAULT_LAYOUT_CLASS, resolveLayoutOrder, getLayoutOrder } from './engine/engine.layout.js';
+import { datePattern } from './support/support.default.js';
+import { sym, markConfig, TermError, getRuntime, init, extendState, setPatterns, isTempo, registryUpdate, registryReset, onRegistryReset, Match, Token, Snippet, Layout, Event, Period, Ignore, Default, Guard, enums, STATE, DISCOVERY, $Internal, $setConfig, $logError, $logDebug, $Identity, $setEvents, $setPeriods, $setAliases, $buildGuard, $IsBase, $Tempo, $Register, $Logify, $errored, $dbg, $guard, $Discover, $setDiscovery } from '#tempo/support';
import * as t from './tempo.type.js'; // namespaced types (Tempo.*)
declare module '#library/type.library.js' {
@@ -871,7 +871,7 @@ export class Tempo {
/** @internal lookup or registers a new `Symbol` for a given key. */
static getSymbol(key?: string | symbol) {
if (isUndefined(key)) {
- const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key
+ const usr = `usr.${++Tempo.#usrCount}`; // allocate a prefixed 'user' key
return Token[usr] = Symbol(usr); // add to Symbol register
}
@@ -885,9 +885,13 @@ export class Tempo {
return Token[key as keyof typeof Token] ?? Symbol.for(`$Tempo.${key}`);
}
- /** @internal translates {layout} into an anchored, case-insensitive RegExp. */
+ /** translates {layout} into an anchored, case-insensitive RegExp. */
static regexp(layout: string | RegExp, snippet?: Snippet) {
- return compileRegExp(layout, (this as any)[$Internal](), snippet as any);
+ const state = (this as any)[$Internal]();
+
+ state.patternCompiler ??= new PatternCompiler({ state });
+
+ return state.patternCompiler.compileRegExp(layout, snippet as any);
}
/** Compares two `Tempo` instances or date-time values. */
diff --git a/packages/tempo/src/tempo.type.ts b/packages/tempo/src/tempo.type.ts
index 441552fe..c8a7ef89 100644
--- a/packages/tempo/src/tempo.type.ts
+++ b/packages/tempo/src/tempo.type.ts
@@ -7,15 +7,16 @@
* Inside `tempo.class.ts` these are accessed via `import * as t`.
*/
-import { sym, type TempoBrand } from '#tempo/support/tempo.symbol.js';
-import * as enums from '#tempo/support/tempo.enum.js';
+import { sym, type TempoBrand } from '#tempo/support/support.symbol.js';
+import * as enums from '#tempo/support/support.enum.js';
import type { Logify } from '#library/logify.class.js';
-import type { Snippet, Layout, Event, Period, Ignore } from '#tempo/support/tempo.default.js';
+import type { Snippet, Layout, Event, Period, Ignore } from '#tempo/support/support.default.js';
import type { IntRange, NonOptional, Property, Plural, Prettify, TemporalObject, TypeValue, RegistryOption } from '#library/type.library.js';
import type { TermPlugin, Plugin } from '#tempo/plugin/plugin.type.js';
-import type { Token } from '#tempo/support/tempo.symbol.js';
+import type { Token } from '#tempo/support/support.symbol.js';
import type { Tempo } from '#tempo/tempo.class.js';
import { AliasEngine } from './engine/engine.alias.js';
+import type { PatternCompiler } from './engine/engine.pattern.js';
declare global {
interface globalThis {
@@ -225,6 +226,7 @@ export namespace Internal {
/** @internal current ZonedDateTime during parsing */ zdt?: Temporal.ZonedDateTime;
/** @internal has the parse operation errored? */ errored?: boolean;
/** @internal Alias engine for this Tempo instance */ aliasEngine?: AliasEngine;
+ /** @internal Pattern compiler for this Tempo instance */ patternCompiler?: PatternCompiler;
}
/** debug a Tempo instantiation */
diff --git a/packages/tempo/src/tsconfig.json b/packages/tempo/src/tsconfig.json
index 24f4d1b6..cdef1a92 100644
--- a/packages/tempo/src/tsconfig.json
+++ b/packages/tempo/src/tsconfig.json
@@ -22,7 +22,6 @@
"#tempo/ticker": ["./plugin/extend/extend.ticker.ts"],
"#tempo/engine/*.js": ["./engine/*.ts"],
"#tempo/module/*.js": ["./module/*.ts"],
- "#tempo/parse/*.js": ["./parse/*.ts"],
"#tempo/plugin/extend/*.js": ["./plugin/extend/*.ts"],
"#tempo/plugin/term/*.js": ["./plugin/term/*.ts"],
"#tempo/term/*": ["./plugin/term/term.*.ts"],
diff --git a/packages/tempo/test/engine/parse.layout.test.ts b/packages/tempo/test/engine/parse.layout.test.ts
index a9a8779d..8af58ab8 100644
--- a/packages/tempo/test/engine/parse.layout.test.ts
+++ b/packages/tempo/test/engine/parse.layout.test.ts
@@ -3,7 +3,7 @@ import {
createLayoutController,
resolveLayoutClassificationOrder,
resolveLayoutOrder,
-} from '#tempo/parse/parse.layout.js';
+} from '#tempo/engine/engine.layout.js';
const makeLayout = (names: string[]) =>
Object.fromEntries(names.map(name => [Symbol(name), name])) as Record;
@@ -11,7 +11,7 @@ const makeLayout = (names: string[]) =>
const orderOf = (layout: Record) =>
Reflect.ownKeys(layout).map(key => (key as symbol).description);
-describe('engine.layout resolver', () => {
+describe('engine.resolver', () => {
test('no-op when no swap pair matches', () => {
const layout = makeLayout(['x', 'y', 'z']);
const resolved = resolveLayoutOrder({
diff --git a/packages/tempo/test/engine/pattern_compiler_optimization.test.ts b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts
new file mode 100644
index 00000000..72de435a
--- /dev/null
+++ b/packages/tempo/test/engine/pattern_compiler_optimization.test.ts
@@ -0,0 +1,43 @@
+import { $Internal, TempoRuntime, init } from '#tempo/support';
+import { PatternCompiler } from '../../src/engine/engine.pattern.js';
+import { getSymbol } from '../../src/support/support.util.js';
+
+describe('PatternCompiler Optimization and Safety', () => {
+ let state: any;
+ let compiler: PatternCompiler;
+
+ beforeEach(() => {
+ const runtime = TempoRuntime.createScoped();
+ state = init({}, false);
+ runtime.state = state;
+ compiler = new PatternCompiler({ state });
+ });
+
+ test('should expand nested placeholders correctly using the hoisted BRACES_REGEX', () => {
+ // Define some snippets in the state
+ state.parse.snippet[getSymbol('a')] = { source: 'A{b}' };
+ state.parse.snippet[getSymbol('b')] = { source: 'B{c}' };
+ state.parse.snippet[getSymbol('c')] = { source: 'C' };
+
+ const result = compiler.compileRegExp('{a}');
+ expect(result.source).toBe('^(ABC)$');
+ });
+
+ test('should handle circular references by hitting recursion limit', () => {
+ // Circular reference: a -> b -> a
+ state.parse.snippet[getSymbol('a')] = { source: 'A{b}' };
+ state.parse.snippet[getSymbol('b')] = { source: 'B{a}' };
+
+ // This should hit the recursion limit (d > 10) and return the partially expanded string
+ const result = compiler.compileRegExp('{a}');
+ // Expanding {a} -> A{b} -> AB{a} -> ABA{b} ...
+ expect(result.source).toContain('ABABAB');
+ });
+
+ test('should handle multiple occurrences of placeholders in a single string', () => {
+ state.parse.snippet[getSymbol('x')] = { source: 'X' };
+
+ const result = compiler.compileRegExp('{x}-{x}-{x}');
+ expect(result.source).toBe('^(X-X-X)$');
+ });
+});
diff --git a/packages/tempo/test/plugins/plugin_registration.test.ts b/packages/tempo/test/plugins/plugin_registration.test.ts
index 631c81cf..b2314bb7 100644
--- a/packages/tempo/test/plugins/plugin_registration.test.ts
+++ b/packages/tempo/test/plugins/plugin_registration.test.ts
@@ -1,5 +1,5 @@
import { Tempo } from '#tempo';
-import { getRuntime } from '#tempo/support/tempo.runtime.js';
+import { getRuntime } from '#tempo/support/support.runtime.js';
import { TickerModule } from '#tempo/ticker';
describe('Ticker Registration / Initialization', () => {
diff --git a/packages/tempo/vitest.config.ts b/packages/tempo/vitest.config.ts
index 3605aec3..ee931ee1 100644
--- a/packages/tempo/vitest.config.ts
+++ b/packages/tempo/vitest.config.ts
@@ -44,7 +44,6 @@ export default defineConfig({
{ find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/plugin.$1.js') },
{ find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/extend/$1.js') },
{ find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './dist/engine/$1.js') },
- { find: /^#tempo\/parse\/(.*)\.js$/, replacement: resolve(__dirname, './dist/parse/$1.js') },
{ find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './dist/module/$1.js') },
{ find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './dist/plugin/term/$1.js') },
{ find: /^#tempo\/support$/, replacement: resolve(__dirname, './dist/support/support.index.js') },
@@ -65,7 +64,6 @@ export default defineConfig({
{ find: /^#tempo\/plugin\/plugin\.(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/plugin.$1.ts') },
{ find: /^#tempo\/plugin\/extend\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/extend/$1.ts') },
{ find: /^#tempo\/engine\/(.*)\.js$/, replacement: resolve(__dirname, './src/engine/$1.ts') },
- { find: /^#tempo\/parse\/(.*)\.js$/, replacement: resolve(__dirname, './src/parse/$1.ts') },
{ find: /^#tempo\/module\/(.*)\.js$/, replacement: resolve(__dirname, './src/module/$1.ts') },
{ find: /^#tempo\/plugin\/term\/(.*)\.js$/, replacement: resolve(__dirname, './src/plugin/term/$1.ts') },
{ find: /^#tempo\/support$/, replacement: resolve(__dirname, './src/support/support.index.ts') },