Skip to content

Commit d901f97

Browse files
committed
feat: tagged template
1 parent 7542c37 commit d901f97

File tree

6 files changed

+79
-38
lines changed

6 files changed

+79
-38
lines changed

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"imports": {
1414
"@deno/dnt": "jsr:@deno/dnt@^0.41.1",
15+
"@kokr/text": "npm:@kokr/text@^0.4.1",
1516
"@std/assert": "jsr:@std/assert@^0.222.1",
1617
"@std/fmt": "jsr:@std/fmt@^0.222.1",
1718
"@std/testing": "jsr:@std/testing@^0.222.1"

formatter.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assertEquals } from "@std/assert/assert-equals";
2+
import { text } from "@kokr/text";
23
import { Formatter } from "./formatter.ts";
34

45
Deno.test("formatter, plural", () => {
@@ -36,6 +37,7 @@ Deno.test("formatter, plural", () => {
3637
const formatter = new Formatter({
3738
locale: "ko",
3839
messages: messages.ko,
40+
taggedTemplate: text,
3941
});
4042

4143
assertEquals(

formatter.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import { Interpreter } from "./interpreter.ts";
22
import { defaultHooks } from "./plugins/default.ts";
3-
import type { FormatParameters, Hook, Runtime } from "./types.ts";
3+
import type {
4+
FormatParameters,
5+
Hook,
6+
Runtime,
7+
TaggedTemplateHandler,
8+
} from "./types.ts";
49

510
export interface FormatterOptions<Messages extends Record<string, string>> {
611
locale?: string;
712
messages?: Messages;
813
hooks?: Record<string, Hook>;
14+
taggedTemplate?: TaggedTemplateHandler;
915
}
1016

1117
export class Formatter<Messages extends Record<string, string>> {
1218
readonly runtime: Runtime;
1319
readonly messages: Messages;
1420
constructor({ messages, ...options }: FormatterOptions<Messages>) {
1521
this.runtime = new Interpreter(
16-
options.hooks ?? defaultHooks,
17-
options.locale,
22+
{
23+
locale: options.locale,
24+
hooks: options.hooks ?? defaultHooks,
25+
taggedTemplate: options.taggedTemplate,
26+
},
1827
);
1928
this.messages = messages ?? {} as Messages;
2029
}

interpreter.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ Deno.test("interpreter, basic template value", () => {
3232

3333
Deno.test("interpreter, template value with method", () => {
3434
const runtime = new Interpreter({
35-
upper: (source, _, ctx) => `${ctx.out ?? source}`.toUpperCase(),
36-
trim: (source, _, ctx) => `${ctx.out ?? source}`.trim(),
35+
hooks: {
36+
upper: (source, _, ctx) => `${ctx.out ?? source}`.toUpperCase(),
37+
trim: (source, _, ctx) => `${ctx.out ?? source}`.trim(),
38+
},
3739
});
3840

3941
assertEquals(
@@ -85,7 +87,7 @@ Deno.test("interpreter, template value with method", () => {
8587
});
8688

8789
Deno.test("interpreter, template with method that returns template", () => {
88-
const runtime = new Interpreter(defaultHooks);
90+
const runtime = new Interpreter({ hooks: defaultHooks });
8991

9092
assertEquals(
9193
runtime.executeAst([

interpreter.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,38 @@ import type {
88
HookInfo,
99
PrimitiveType,
1010
Runtime,
11+
TaggedTemplateHandler,
1112
} from "./types.ts";
1213

14+
export interface InterpreterOptions {
15+
locale?: string | null;
16+
hooks?: Record<string, Hook>;
17+
taggedTemplate?: TaggedTemplateHandler;
18+
}
19+
20+
function defaultTaggedTemplateHandler(
21+
strings: TemplateStringsArray,
22+
...args: unknown[]
23+
): string {
24+
return strings.reduce((carry, string, index) => {
25+
if (index > 0) {
26+
carry += args[index - 1];
27+
}
28+
return carry + string;
29+
});
30+
}
31+
1332
export class Interpreter implements Runtime {
1433
_cache: Map<string, AstTemplate> = new Map();
1534
_decorator: (source: PrimitiveType) => unknown;
35+
_taggedTemplate: TaggedTemplateHandler;
1636

17-
constructor(hooks: Record<string, Hook>, locale?: string | null) {
18-
this._decorator = createDecorator(hooks, {
19-
locale: locale ?? null,
37+
constructor(options: InterpreterOptions = {}) {
38+
this._decorator = createDecorator(options.hooks ?? {}, {
39+
locale: options.locale ?? null,
2040
});
41+
this._taggedTemplate = options.taggedTemplate ??
42+
defaultTaggedTemplateHandler;
2143
}
2244

2345
execute(text: string, parameters: FormatParameters): string {
@@ -31,38 +53,38 @@ export class Interpreter implements Runtime {
3153

3254
executeAst(ast: AstTemplate, parameters: FormatParameters): string {
3355
const [strings, values] = ast;
34-
let result = strings[0] ?? "";
35-
values.forEach(([valueName, methods], valueIndex) => {
36-
const self = parameters[valueName];
56+
return this._taggedTemplate(
57+
Object.assign(strings, { raw: strings }),
58+
...values.map(([valueName, methods]) => {
59+
const self = parameters[valueName];
3760

38-
let value = this._decorator(self);
39-
for (const [methodName, methodArgs] of methods) {
40-
const method = getSafeMethod(value, methodName);
41-
if (method) {
42-
value = method(
43-
...methodArgs.map((methodArg) => {
44-
switch (methodArg[0]) {
45-
case 2:
46-
return methodArg[1];
47-
case 3:
48-
return methodArg[1];
49-
case 4:
50-
return () =>
51-
this.executeAst(methodArg[1], { _: self, ...parameters });
52-
default:
53-
throw new Error(
54-
`Unknown method argument type: ${methodArg[0]}`,
55-
);
56-
}
57-
}),
58-
);
61+
let value = this._decorator(self);
62+
for (const [methodName, methodArgs] of methods) {
63+
const method = getSafeMethod(value, methodName);
64+
if (method) {
65+
value = method(
66+
...methodArgs.map((methodArg) => {
67+
switch (methodArg[0]) {
68+
case 2:
69+
return methodArg[1];
70+
case 3:
71+
return methodArg[1];
72+
case 4:
73+
return () =>
74+
this.executeAst(methodArg[1], { _: self, ...parameters });
75+
default:
76+
throw new Error(
77+
`Unknown method argument type: ${methodArg[0]}`,
78+
);
79+
}
80+
}),
81+
);
82+
}
5983
}
60-
}
6184

62-
result += value && value.toString ? value.toString() : (value ?? "");
63-
result += strings[valueIndex + 1] ?? "";
64-
});
65-
return result;
85+
return value && value.toString ? value.toString() : (value ?? "");
86+
}),
87+
);
6688
}
6789
}
6890

types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ export type Hook = (
2525
ctx: HookContext,
2626
info: HookInfo,
2727
) => string | null;
28+
29+
export type TaggedTemplateHandler = (
30+
strings: TemplateStringsArray,
31+
...args: unknown[]
32+
) => string;

0 commit comments

Comments
 (0)