docs/specs/2026-05-18-211-template-engine-dry.md
- Module:
src/journal/template-engine.ts(journal domain layer, not util) ${week}gap: fix in scope — add totoMomentFormatas well- Thread-safe locales:
moment(date).locale(loc)— no globalmoment.locale()calls - Single-pass:
template.replace(regex, callback)— no forEach + multiple replaces - Registry pattern: each entry holds both
resolvefn andmomentFormatstring
Replace two parallel switch/case blocks with a single TEMPLATE_VARIABLE_MAP registry. Both resolveDate and toMomentFormat use one regex callback pass over the registry. Structural impossibility of drift: adding a variable requires one map entry with both fields.
Trade-off: moving functions from util/ to journal/ means callers in vscode/ and features/ import cross-layer. Acceptable because util/ was a generic dumping ground; the domain layer is the right home. The barrel (src/journal/index.ts) re-exports the new functions, keeping import paths short.
New file. Contains:
TEMPLATE_VARIABLE_MAP(module-private) — 7 named entries each withmomentFormatandresolve(m: Moment): stringTEMPLATE_VAR_REGEX(module-private) — same pattern as currentregExpDateFormatsindates.tsresolveDate(template, date, locale?)— single-pass regex replace usingmoment(date).locale(locale), not global localetoMomentFormat(template)— single-pass regex replace returning moment format tokens;${d:fmt}→fmtpassthrough;${week}→'w'(fixing the existing gap)
Why first: all subsequent steps depend on this file existing.
Remove:
regExpDateFormatsconstantreplaceDateFormatsfunctionreplaceDateTemplatesWithMomentsFormatsfunction
Keep all other functions (formatDate, getCurrentISOWeek, getISOWeekYear, getDatesOfISOWeek, getDayOfWeekForString, getMonthForString).
Why: eliminates the source of duplication. Must happen after step 1.
Remove replaceDateFormats and replaceDateTemplatesWithMomentsFormats from the export { ... } from './dates' block.
Why: barrel must not re-export deleted symbols.
Add exports:
export { resolveDate, toMomentFormat } from './template-engine';Why: makes the new API reachable as J.Journal.resolveDate and via named imports from '../journal' for vscode/features layers.
Line 26: change import { replaceDateTemplatesWithMomentsFormats } from '../util/dates' to import { toMomentFormat } from './template-engine'.
Lines 78–79: replace replaceDateTemplatesWithMomentsFormats(...) → toMomentFormat(...).
Why: closest caller; direct sibling import.
Line 26: change import { replaceDateFormats, replaceVariableValue } from '../util' — remove replaceDateFormats, add import { resolveDate } from '../journal/template-engine'.
Lines 342, 368: replace replaceDateFormats(...) → resolveDate(...).
Why: conf.ts is in the vscode layer; import from journal domain is fine.
Line 4: change import { getDatesOfISOWeek, replaceDateFormats, replaceVariableValue } from '../../util' — remove replaceDateFormats, add import { resolveDate } from '../../journal/template-engine'.
Line 80: replace replaceDateFormats(...) → resolveDate(...).
Why: same locale-aware signature; drop-in replacement.
Remove local duplicate replaceDateTemplatesWithMomentsFormats function (lines 124+).
Replace its two call sites (lines 94–95) with toMomentFormat imported from ../../journal/template-engine.
Why: this is the third copy of the function — the most egregious duplication site.
Update import: import { replaceDateFormats } from "../../util/dates" → import { resolveDate } from "../../journal/template-engine".
Update call sites: replaceDateFormats(...) → resolveDate(...).
Why: test file must import from the new location; function signature is identical.
Unit (new file src/test/suite/template-engine.test.ts):
resolveDate-named-vars:${year},${month},${day},${localTime},${localDate},${weekday},${week}each replaced with correct formatted value for a known dateresolveDate-custom-format:${d:YY}→ two-digit year;${d:dddd}→ weekday nameresolveDate-no-vars: template without${...}returns unchangedresolveDate-locale-isolation: callingresolveDatetwice with different locales returns locale-specific results without affecting a subsequent call (proves no global locale mutation)toMomentFormat-named-vars: all 7 variables replaced with correct moment tokens, including${week}→'w'toMomentFormat-custom-format:${d:YY}→'YY'(passthrough)toMomentFormat-week-fixed:${week}→'w'(explicit test for the closed gap)
Integration (existing tests, unchanged assertions):
replace-variables-in-string.ts— same outputs as before (behavior parity)path-parse-with-date.ts— path construction still produces same paths after removing local duplicate
- feat-210 (#210) must land on
developbefore this PR is merged (avoid double-churn onsrc/model/). Implementation can proceed on a feature branch in parallel; merge order matters.
- Locale regression: old code mutated global
moment.locale(locale)before formatting. New code usesmoment(date).locale(loc). Behavior is equivalent for sequential calls but safe for concurrent calls. TheresolveDate-locale-isolationtest scenario covers this. ${week}toMomentFormat gap fixed:toMomentFormatpreviously silently dropped${week}. After this PR it maps to'w'. Any caller that depended on the silent-drop behavior (passing output tomoment.format) would change. Audit: only caller issrc/journal/paths.tslines 78–79. Those paths do not use${week}, so no behavior change in practice.
Revert the feature branch. No schema migrations, no config changes, no data touched.
feat/211-template-engine-dry off develop (or off the feat-210 branch if sequencing is needed).