1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
/**
* Custom mustache-like template renderer for card display.
*
* Syntax: `{{FieldName}}` is replaced with the corresponding field value.
*
* Features:
* - Simple `{{FieldName}}` replacement
* - Case-sensitive field matching
* - Missing fields are replaced with empty string
* - Whitespace around field names is trimmed: `{{ Front }}` works like `{{Front}}`
*
* Examples:
* - Simple: `{{Front}}` → "What is the capital of Japan?"
* - With text: `Q: {{Front}}` → "Q: What is the capital of Japan?"
* - Multiple fields: `{{Word}} - {{Reading}}` → "日本語 - にほんご"
*/
/**
* Regex to match mustache-style placeholders: {{fieldName}}
* Captures the field name (with optional whitespace that will be trimmed)
*/
const TEMPLATE_PATTERN = /\{\{\s*([^}]+?)\s*\}\}/g;
/**
* Field values for template rendering.
* Keys are field names, values are the corresponding content.
*/
export type FieldValues = Record<string, string>;
/**
* Renders a mustache-like template by replacing `{{FieldName}}` placeholders
* with corresponding values from the fieldValues object.
*
* @param template - The template string with `{{FieldName}}` placeholders
* @param fieldValues - Object mapping field names to their values
* @returns The rendered string with all placeholders replaced
*
* @example
* ```typescript
* renderTemplate("{{Front}}", { Front: "Hello" })
* // Returns: "Hello"
*
* renderTemplate("Q: {{Question}}\nHint: {{Hint}}", {
* Question: "What is 2+2?",
* Hint: "It's even"
* })
* // Returns: "Q: What is 2+2?\nHint: It's even"
* ```
*/
export function renderTemplate(
template: string,
fieldValues: FieldValues,
): string {
return template.replace(TEMPLATE_PATTERN, (_, fieldName: string) => {
const trimmedName = fieldName.trim();
return fieldValues[trimmedName] ?? "";
});
}
/**
* Extracts all field names used in a template.
*
* @param template - The template string to analyze
* @returns Array of unique field names found in the template
*
* @example
* ```typescript
* extractFieldNames("{{Front}} and {{Back}}")
* // Returns: ["Front", "Back"]
*
* extractFieldNames("{{Word}} - {{Word}}")
* // Returns: ["Word"] (duplicates removed)
* ```
*/
export function extractFieldNames(template: string): string[] {
const names = new Set<string>();
// Use matchAll to get all matches without assignment in loop
const matches = template.matchAll(TEMPLATE_PATTERN);
for (const match of matches) {
const fieldName = match[1];
if (fieldName) {
names.add(fieldName.trim());
}
}
return Array.from(names);
}
/**
* Validates that all field names used in a template exist in the provided field values.
*
* @param template - The template string to validate
* @param fieldValues - Object mapping field names to their values
* @returns Object with `valid` boolean and `missingFields` array if invalid
*
* @example
* ```typescript
* validateTemplate("{{Front}}", { Front: "Hello" })
* // Returns: { valid: true, missingFields: [] }
*
* validateTemplate("{{Front}} {{Back}}", { Front: "Hello" })
* // Returns: { valid: false, missingFields: ["Back"] }
* ```
*/
export function validateTemplate(
template: string,
fieldValues: FieldValues,
): { valid: boolean; missingFields: string[] } {
const usedFields = extractFieldNames(template);
const availableFields = new Set(Object.keys(fieldValues));
const missingFields = usedFields.filter(
(field) => !availableFields.has(field),
);
return {
valid: missingFields.length === 0,
missingFields,
};
}
/**
* Options for rendering a card's display content.
*/
export interface RenderCardOptions {
/** The front template (e.g., "{{Front}}") */
frontTemplate: string;
/** The back template (e.g., "{{Back}}") */
backTemplate: string;
/** Field values from the note */
fieldValues: FieldValues;
/** Whether this is a reversed card */
isReversed: boolean;
}
/**
* Renders a card's front and back content based on templates and note field values.
*
* For normal cards (isReversed = false):
* - Front: Render frontTemplate
* - Back: Render backTemplate
*
* For reversed cards (isReversed = true):
* - Front: Render backTemplate
* - Back: Render frontTemplate
*
* @param options - The rendering options
* @returns Object with rendered `front` and `back` strings
*
* @example
* ```typescript
* // Normal card
* renderCard({
* frontTemplate: "{{Front}}",
* backTemplate: "{{Back}}",
* fieldValues: { Front: "Question", Back: "Answer" },
* isReversed: false
* })
* // Returns: { front: "Question", back: "Answer" }
*
* // Reversed card
* renderCard({
* frontTemplate: "{{Front}}",
* backTemplate: "{{Back}}",
* fieldValues: { Front: "Question", Back: "Answer" },
* isReversed: true
* })
* // Returns: { front: "Answer", back: "Question" }
* ```
*/
export function renderCard(options: RenderCardOptions): {
front: string;
back: string;
} {
const { frontTemplate, backTemplate, fieldValues, isReversed } = options;
if (isReversed) {
return {
front: renderTemplate(backTemplate, fieldValues),
back: renderTemplate(frontTemplate, fieldValues),
};
}
return {
front: renderTemplate(frontTemplate, fieldValues),
back: renderTemplate(backTemplate, fieldValues),
};
}
|