Skip to content

Commit de3ec50

Browse files
pajomaclaude
andcommitted
refactor(arch): extract TemplateEngine with single-pass registry (#211)
Replace parallel switch/case blocks in util/dates.ts with a central TEMPLATE_VARIABLE_MAP in src/journal/template-engine.ts. Each entry holds both a resolve fn and a momentFormat string, making it structurally impossible for the two resolution modes to drift. - resolveDate: thread-safe via moment(date).locale(loc), single-pass regex callback - toMomentFormat: same registry, also fixes the ${week} → 'w' gap - Deletes third duplicate from src/test/direct/path-parse-with-date.ts - Updates all callers: paths.ts, conf.ts, sync-daily-links.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9ea46a6 commit de3ec50

10 files changed

Lines changed: 183 additions & 154 deletions

File tree

src/features/sync/sync-daily-links.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as vscode from 'vscode';
22
import * as Path from 'path';
33
import * as J from '../..';
4-
import { getDatesOfISOWeek, replaceDateFormats, replaceVariableValue } from '../../util';
4+
import { getDatesOfISOWeek, replaceVariableValue } from '../../util';
5+
import { resolveDate } from '../../journal/template-engine';
56
import { fileExists } from '../../util/fs-exists';
67

78
export class SyncDailyLinks {
@@ -77,7 +78,7 @@ export class SyncDailyLinks {
7778
const relativePath = Path.relative(weeklyDir, uri.fsPath).replace(/\\/g, '/');
7879

7980
let line = tpl.template;
80-
line = replaceDateFormats(line, date, locale);
81+
line = resolveDate(line, date, locale);
8182
line = replaceVariableValue("link", relativePath, line);
8283
// ${title} substitution: filename without extension
8384
const title = Path.parse(uri.fsPath).name.replace(/_/g, ' ');

src/journal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ export {
3535
inferType,
3636
resolvePath,
3737
} from './paths';
38+
export { resolveDate, toMomentFormat } from './template-engine';

src/journal/paths.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import * as vscode from 'vscode';
2323
import * as J from '..';
2424
import { getDayAsString, prefixZero } from '../util/strings';
2525
import { isNullOrUndefined } from '../util/util';
26-
import { replaceDateTemplatesWithMomentsFormats } from '../util/dates';
26+
import { toMomentFormat } from './template-engine';
2727
import moment = require('moment');
2828

2929
/**
@@ -75,8 +75,8 @@ export async function getDateFromURI(uri: string, pathTemplate: string, fileTemp
7575
const trimmedFileString = pathParts.length > 0 ? pathParts[pathParts.length - 1].split('.')[0] : "";
7676
const trimmedPathString = pathParts.length > 1 ? pathParts.slice(0, -1).join('/') : "";
7777

78-
const entryDateFormat = replaceDateTemplatesWithMomentsFormats(fileTemplate);
79-
const pathDateFormat = replaceDateTemplatesWithMomentsFormats(pathTemplate);
78+
const entryDateFormat = toMomentFormat(fileTemplate);
79+
const pathDateFormat = toMomentFormat(pathTemplate);
8080

8181
let parsedDateFromFile = moment(trimmedFileString, entryDateFormat);
8282
let parsedDateFromPath = moment(trimmedPathString, pathDateFormat);

src/journal/template-engine.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (C) 2024 Patrick Maué
2+
//
3+
// This file is part of vscode-journal.
4+
//
5+
// vscode-journal is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// vscode-journal is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with vscode-journal. If not, see <http://www.gnu.org/licenses/>.
17+
18+
'use strict';
19+
20+
import moment = require('moment');
21+
22+
interface TemplateVariableEntry {
23+
momentFormat: string;
24+
resolve(m: moment.Moment): string;
25+
}
26+
27+
const TEMPLATE_VARIABLE_MAP: Record<string, TemplateVariableEntry> = {
28+
year: { momentFormat: 'YYYY', resolve: m => m.format('YYYY') },
29+
month: { momentFormat: 'MM', resolve: m => m.format('MM') },
30+
day: { momentFormat: 'DD', resolve: m => m.format('DD') },
31+
localTime: { momentFormat: 'LT', resolve: m => m.format('LT') },
32+
localDate: { momentFormat: 'LL', resolve: m => m.format('LL') },
33+
weekday: { momentFormat: 'dddd', resolve: m => m.format('dddd') },
34+
week: { momentFormat: 'w', resolve: m => String(m.week()) },
35+
};
36+
37+
// https://regex101.com/r/i5MUpx/1/ (extended from util/dates.ts)
38+
const TEMPLATE_VAR_REGEX = /\$\{(?:(year|month|day|localTime|localDate|weekday|week)|(d:[\s\S]+?))\}/g;
39+
40+
export function resolveDate(template: string, date: Date, locale?: string): string {
41+
const mom = locale ? moment(date).locale(locale) : moment(date);
42+
return template.replace(TEMPLATE_VAR_REGEX, (match, named: string | undefined, custom: string | undefined) => {
43+
if (named && TEMPLATE_VARIABLE_MAP[named]) {
44+
return TEMPLATE_VARIABLE_MAP[named].resolve(mom);
45+
}
46+
if (custom) {
47+
return mom.format(custom.slice(2).trim()); // strip 'd:' prefix
48+
}
49+
return match;
50+
});
51+
}
52+
53+
export function toMomentFormat(template: string): string {
54+
return template.replace(TEMPLATE_VAR_REGEX, (_match, named: string | undefined, custom: string | undefined) => {
55+
if (named && TEMPLATE_VARIABLE_MAP[named]) {
56+
return TEMPLATE_VARIABLE_MAP[named].momentFormat;
57+
}
58+
if (custom) {
59+
return custom.slice(2).trim(); // strip 'd:' prefix
60+
}
61+
return _match;
62+
});
63+
}

src/test/direct/path-parse-with-date.ts

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import * as assert from 'assert';
22
import moment = require('moment');
3-
4-
5-
6-
const regExpDateFormats: RegExp = new RegExp(/\$\{(?:(year|month|day|localTime|localDate|weekday)|(d:[\s\S]+?))\}/g);
3+
import { toMomentFormat } from '../../journal/template-engine';
74
let base = "c:\\Users\\user\\Git\\vscode-journal\\test\\workspace\\journal";
85
let pathTpl = "${base}/${year}-${month}";
96
let fileTpl = "${year}${month}${day}.${ext}";
@@ -91,8 +88,8 @@ export async function getDateFromURI(uri: string, pathTemplate: string, fileTemp
9188
})*/
9289
let mom: moment.Moment = moment(fileStr, fileTemplate);
9390

94-
const entryMomentTpl = replaceDateTemplatesWithMomentsFormats(fileTemplate);
95-
const pathMomentTpl = replaceDateTemplatesWithMomentsFormats(pathTemplate);
91+
const entryMomentTpl = toMomentFormat(fileTemplate);
92+
const pathMomentTpl = toMomentFormat(pathTemplate);
9693

9794
// filestr: "20210809"
9895
// path str: "/202108"
@@ -121,42 +118,6 @@ export async function getDateFromURI(uri: string, pathTemplate: string, fileTemp
121118

122119

123120

124-
export function replaceDateTemplatesWithMomentsFormats(template: string): string {
125-
let matches: RegExpMatchArray | null = template.match(regExpDateFormats);
126-
if(matches === null) {
127-
return template;
128-
}
129-
130-
matches.forEach(match => {
131-
switch (match) {
132-
case "${year}":
133-
template = template.replace(match, "YYYY"); break;
134-
case "${month}":
135-
template = template.replace(match, "MM"); break;
136-
case "${day}":
137-
template = template.replace(match, "DD"); break;
138-
case "${localTime}":
139-
template = template.replace(match, "LT"); break;
140-
case "${localDate}":
141-
template = template.replace(match, "LL"); break;
142-
case "${weekday}":
143-
template = template.replace(match, "dddd"); break;
144-
default:
145-
// check if custom format
146-
if (match.startsWith("${d:")) {
147-
148-
let modifier = match.substring(match.indexOf("d:") + 2, match.length - 1); // includes } at the end
149-
// st.template = st.template.replace(match, mom.format(modifier));
150-
// fix for #51
151-
template = template.replace(match, modifier);
152-
break;
153-
}
154-
break;
155-
}
156-
});
157-
return template;
158-
159-
}
160121

161122
function assertCorrectDate(date: Date): void {
162123
let iso = date.toISOString();

src/test/direct/replace-variables-in-string.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { replaceDateFormats } from "../../util/dates";
1+
import { resolveDate as replaceDateFormats } from "../../journal/template-engine";
22

33

44

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import * as assert from 'assert';
2+
import { resolveDate, toMomentFormat } from '../../journal/template-engine';
3+
4+
const KNOWN_DATE = new Date(2024, 2, 5, 14, 30, 0); // 2024-03-05 14:30
5+
6+
suite('TemplateEngine', () => {
7+
8+
suite('resolveDate — named variables', () => {
9+
test('replaces ${year}', () => {
10+
assert.strictEqual(resolveDate('${year}', KNOWN_DATE), '2024');
11+
});
12+
test('replaces ${month}', () => {
13+
assert.strictEqual(resolveDate('${month}', KNOWN_DATE), '03');
14+
});
15+
test('replaces ${day}', () => {
16+
assert.strictEqual(resolveDate('${day}', KNOWN_DATE), '05');
17+
});
18+
test('replaces ${weekday}', () => {
19+
assert.strictEqual(resolveDate('${weekday}', KNOWN_DATE), 'Tuesday');
20+
});
21+
test('replaces ${week}', () => {
22+
assert.strictEqual(resolveDate('${week}', KNOWN_DATE), '10');
23+
});
24+
test('replaces multiple variables in one pass', () => {
25+
assert.strictEqual(resolveDate('${year}/${month}/${day}', KNOWN_DATE), '2024/03/05');
26+
});
27+
});
28+
29+
suite('resolveDate — custom format', () => {
30+
test('replaces ${d:YY} with two-digit year', () => {
31+
assert.strictEqual(resolveDate('year: ${d:YY}', KNOWN_DATE), 'year: 24');
32+
});
33+
test('replaces ${d:dddd} with full weekday name', () => {
34+
assert.strictEqual(resolveDate('${d:dddd}', KNOWN_DATE), 'Tuesday');
35+
});
36+
});
37+
38+
suite('resolveDate — edge cases', () => {
39+
test('returns template unchanged when no variables', () => {
40+
assert.strictEqual(resolveDate('no variables here', KNOWN_DATE), 'no variables here');
41+
});
42+
test('locale isolation: two calls with different locales do not bleed', () => {
43+
const de = resolveDate('${weekday}', KNOWN_DATE, 'de');
44+
const en = resolveDate('${weekday}', KNOWN_DATE, 'en');
45+
assert.strictEqual(de, 'Dienstag');
46+
assert.strictEqual(en, 'Tuesday');
47+
// third call without locale should not inherit 'de'
48+
const neutral = resolveDate('${weekday}', KNOWN_DATE);
49+
assert.strictEqual(neutral, en);
50+
});
51+
});
52+
53+
suite('toMomentFormat — named variables', () => {
54+
test('maps ${year} to YYYY', () => {
55+
assert.strictEqual(toMomentFormat('${year}'), 'YYYY');
56+
});
57+
test('maps ${month} to MM', () => {
58+
assert.strictEqual(toMomentFormat('${month}'), 'MM');
59+
});
60+
test('maps ${day} to DD', () => {
61+
assert.strictEqual(toMomentFormat('${day}'), 'DD');
62+
});
63+
test('maps ${localTime} to LT', () => {
64+
assert.strictEqual(toMomentFormat('${localTime}'), 'LT');
65+
});
66+
test('maps ${localDate} to LL', () => {
67+
assert.strictEqual(toMomentFormat('${localDate}'), 'LL');
68+
});
69+
test('maps ${weekday} to dddd', () => {
70+
assert.strictEqual(toMomentFormat('${weekday}'), 'dddd');
71+
});
72+
test('maps ${week} to w — fixes the prior gap', () => {
73+
assert.strictEqual(toMomentFormat('${week}'), 'w');
74+
});
75+
test('handles mixed template', () => {
76+
assert.strictEqual(toMomentFormat('${year}/${month}/${day}'), 'YYYY/MM/DD');
77+
});
78+
});
79+
80+
suite('toMomentFormat — custom format passthrough', () => {
81+
test('passes ${d:YY} through as YY', () => {
82+
assert.strictEqual(toMomentFormat('${d:YY}'), 'YY');
83+
});
84+
test('passes ${d:dddd} through as dddd', () => {
85+
assert.strictEqual(toMomentFormat('${d:dddd}'), 'dddd');
86+
});
87+
});
88+
89+
suite('toMomentFormat — edge cases', () => {
90+
test('returns template unchanged when no variables', () => {
91+
assert.strictEqual(toMomentFormat('no variables'), 'no variables');
92+
});
93+
});
94+
});

src/util/dates.ts

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -118,93 +118,3 @@ export function formatDate(date: Date, template: string, locale: string): string
118118
}
119119

120120

121-
/**
122-
* Checks whether any embedded expressions with date formats are in the template, and replaces them in the value using the given date.
123-
*
124-
* @param st
125-
* @param date
126-
*/
127-
// https://regex101.com/r/i5MUpx/1/
128-
// private regExpDateFormats: RegExp = new RegExp(/\$\{(?:(year|month|day|localTime|localDate|weekday)|(d:\w+))\}/g);
129-
// fix for #52
130-
// private regExpDateFormats: RegExp = new RegExp(/\$\{(?:(year|month|day|localTime|localDate|weekday)|(d:\w+))\}/g);
131-
const regExpDateFormats: RegExp = new RegExp(/\$\{(?:(year|month|day|localTime|localDate|weekday)|(d:[\s\S]+?))\}/g);
132-
133-
export function replaceDateFormats(template: string, date: Date, locale?: string): string {
134-
let matches : RegExpMatchArray | null = template.match(regExpDateFormats);
135-
if(matches === null) {
136-
return template;
137-
}
138-
139-
let mom: moment.Moment = moment(date);
140-
moment.locale(locale);
141-
142-
matches.forEach(match => {
143-
switch (match) {
144-
case "${year}":
145-
template = template.replace(match, mom.format("YYYY")); break;
146-
case "${month}":
147-
template = template.replace(match, mom.format("MM")); break;
148-
case "${day}":
149-
template = template.replace(match, mom.format("DD")); break;
150-
case "${localTime}":
151-
template = template.replace(match, mom.format("LT")); break;
152-
case "${localDate}":
153-
template = template.replace(match, mom.format("LL")); break;
154-
case "${weekday}":
155-
template = template.replace(match, mom.format("dddd")); break;
156-
case "${week}":
157-
template = template.replace(match, mom.week() + ""); break;
158-
default:
159-
// check if custom format
160-
if (match.startsWith("${d:")) {
161-
162-
let modifier = match.substring(match.indexOf("d:") + 2, match.length - 1); // includes } at the end
163-
// st.template = st.template.replace(match, mom.format(modifier));
164-
// fix for #51
165-
template = template.replace(match, mom.format(modifier));
166-
break;
167-
}
168-
break;
169-
}
170-
});
171-
172-
return template;
173-
}
174-
175-
export function replaceDateTemplatesWithMomentsFormats(template: string): string {
176-
let matches: RegExpMatchArray | null = template.match(regExpDateFormats);
177-
if (matches === null) {
178-
return template;
179-
}
180-
181-
matches.forEach(match => {
182-
switch (match) {
183-
case "${year}":
184-
template = template.replace(match, "YYYY"); break;
185-
case "${month}":
186-
template = template.replace(match, "MM"); break;
187-
case "${day}":
188-
template = template.replace(match, "DD"); break;
189-
case "${localTime}":
190-
template = template.replace(match, "LT"); break;
191-
case "${localDate}":
192-
template = template.replace(match, "LL"); break;
193-
case "${weekday}":
194-
template = template.replace(match, "dddd"); break;
195-
default:
196-
// check if custom format
197-
if (match.startsWith("${d:")) {
198-
199-
let modifier = match.substring(match.indexOf("d:") + 2, match.length - 1); // includes } at the end
200-
// st.template = st.template.replace(match, mom.format(modifier));
201-
// fix for #51
202-
template = template.replace(match, modifier);
203-
break;
204-
}
205-
break;
206-
}
207-
});
208-
return template;
209-
210-
}

src/util/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ export {
3131
getDayOfWeekForString,
3232
getISOWeekYear,
3333
getMonthForString,
34-
replaceDateFormats,
35-
replaceDateTemplatesWithMomentsFormats,
3634
} from './dates';
3735
export {
3836
denormalizeFilename,

0 commit comments

Comments
 (0)