mintdoc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,447 @@
1
+ // src/core/lexer.ts
2
+ function tokenize(xml) {
3
+ const tokens = [];
4
+ const tagRegex = /\{([^}]+)\}/g;
5
+ let lastIndex = 0;
6
+ let match;
7
+ while ((match = tagRegex.exec(xml)) !== null) {
8
+ if (match.index > lastIndex) {
9
+ tokens.push({ type: "xml", content: xml.slice(lastIndex, match.index) });
10
+ }
11
+ const raw = match[0];
12
+ const inner = match[1].trim();
13
+ tokens.push(classifyTag(inner, raw));
14
+ lastIndex = match.index + match[0].length;
15
+ }
16
+ if (lastIndex < xml.length) {
17
+ tokens.push({ type: "xml", content: xml.slice(lastIndex) });
18
+ }
19
+ return tokens;
20
+ }
21
+ function classifyTag(inner, raw) {
22
+ if (inner === ":else") {
23
+ return { type: "condition-else", raw };
24
+ }
25
+ if (inner === "/if") {
26
+ return { type: "condition-close", raw };
27
+ }
28
+ if (inner.startsWith("/")) {
29
+ return { type: "loop-close", name: inner.slice(1).trim(), raw };
30
+ }
31
+ if (inner.startsWith("#if ")) {
32
+ return { type: "condition-open", expression: inner.slice(4).trim(), raw };
33
+ }
34
+ if (inner.startsWith("#")) {
35
+ return { type: "loop-open", name: inner.slice(1).trim(), raw };
36
+ }
37
+ const parts = inner.split("|").map((p) => p.trim());
38
+ const name = parts[0];
39
+ const formatters = parts.slice(1);
40
+ return { type: "variable", name, formatters, raw };
41
+ }
42
+
43
+ // src/core/parser.ts
44
+ function parse(tokens) {
45
+ const { nodes, index } = parseNodes(tokens, 0, null);
46
+ if (index < tokens.length) {
47
+ const unexpected = tokens[index];
48
+ throw new MintdocParseError(
49
+ `Unexpected closing tag "${unexpected.raw ?? ""}" without a matching opening tag.`
50
+ );
51
+ }
52
+ return nodes;
53
+ }
54
+ function parseNodes(tokens, start, closingTag) {
55
+ const nodes = [];
56
+ let i = start;
57
+ while (i < tokens.length) {
58
+ const token = tokens[i];
59
+ switch (token.type) {
60
+ case "xml":
61
+ nodes.push({ type: "text", content: token.content });
62
+ i++;
63
+ break;
64
+ case "variable":
65
+ nodes.push({
66
+ type: "variable",
67
+ name: token.name,
68
+ formatters: token.formatters
69
+ });
70
+ i++;
71
+ break;
72
+ case "loop-open": {
73
+ const loopName = token.name;
74
+ const children = parseNodes(tokens, i + 1, loopName);
75
+ const closer = tokens[children.index];
76
+ if (!closer || closer.type !== "loop-close") {
77
+ throw new MintdocParseError(
78
+ `Unclosed loop "{#${loopName}}". Add a matching "{/${loopName}}" tag.`
79
+ );
80
+ }
81
+ if (closer.type === "loop-close" && closer.name !== loopName) {
82
+ throw new MintdocParseError(
83
+ `Mismatched loop tags: opened "{#${loopName}}" but closed with "{/${closer.name}}".`
84
+ );
85
+ }
86
+ nodes.push({ type: "loop", name: loopName, children: children.nodes });
87
+ i = children.index + 1;
88
+ break;
89
+ }
90
+ case "condition-open": {
91
+ const expression = token.expression;
92
+ const trueBranch = parseNodes(tokens, i + 1, "if");
93
+ let trueChildren = trueBranch.nodes;
94
+ let falseChildren = [];
95
+ let endIndex = trueBranch.index;
96
+ const next = tokens[endIndex];
97
+ if (next && next.type === "condition-else") {
98
+ const falseBranch = parseNodes(tokens, endIndex + 1, "if");
99
+ falseChildren = falseBranch.nodes;
100
+ endIndex = falseBranch.index;
101
+ }
102
+ const closerToken = tokens[endIndex];
103
+ if (!closerToken || closerToken.type !== "condition-close") {
104
+ throw new MintdocParseError(
105
+ `Unclosed condition "{#if ${expression}}". Add a matching "{/if}" tag.`
106
+ );
107
+ }
108
+ nodes.push({
109
+ type: "condition",
110
+ expression,
111
+ trueChildren,
112
+ falseChildren
113
+ });
114
+ i = endIndex + 1;
115
+ break;
116
+ }
117
+ // Closing / else tokens signal the end of the current nesting level
118
+ case "loop-close":
119
+ case "condition-close":
120
+ case "condition-else":
121
+ return { nodes, index: i };
122
+ default:
123
+ i++;
124
+ }
125
+ }
126
+ if (closingTag !== null) {
127
+ throw new MintdocParseError(
128
+ `Unexpected end of template. Missing closing tag for "{#${closingTag}}".`
129
+ );
130
+ }
131
+ return { nodes, index: i };
132
+ }
133
+ var MintdocParseError = class extends Error {
134
+ constructor(message) {
135
+ super(message);
136
+ this.name = "MintdocParseError";
137
+ }
138
+ };
139
+
140
+ // src/utils/expressions.ts
141
+ function resolveValue(path, data) {
142
+ if (data == null || typeof data !== "object") {
143
+ return path === "." ? data : void 0;
144
+ }
145
+ if (path === ".") {
146
+ const record = data;
147
+ return "." in record ? record["."] : data;
148
+ }
149
+ const parts = path.split(".");
150
+ let current = data;
151
+ for (const part of parts) {
152
+ if (current == null || typeof current !== "object") return void 0;
153
+ current = current[part];
154
+ }
155
+ return current;
156
+ }
157
+ function isTruthy(value) {
158
+ if (Array.isArray(value)) return value.length > 0;
159
+ return Boolean(value);
160
+ }
161
+
162
+ // src/utils/xml.ts
163
+ function escapeXml(str) {
164
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
165
+ }
166
+ function mergeRuns(xml) {
167
+ const wtRegex = /<w:t(\s[^>]*)?>([^]*?)<\/w:t>/g;
168
+ const matches = [];
169
+ let m;
170
+ while ((m = wtRegex.exec(xml)) !== null) {
171
+ matches.push({
172
+ fullMatch: m[0],
173
+ attrs: m[1] || "",
174
+ content: m[2],
175
+ index: m.index
176
+ });
177
+ }
178
+ if (matches.length === 0) return xml;
179
+ const newContents = /* @__PURE__ */ new Map();
180
+ let i = 0;
181
+ while (i < matches.length) {
182
+ const content = newContents.get(i) ?? matches[i].content;
183
+ const openCount = countChar(content, "{");
184
+ const closeCount = countChar(content, "}");
185
+ if (openCount > closeCount) {
186
+ let merged = content;
187
+ let j = i + 1;
188
+ while (j < matches.length) {
189
+ const nextContent = newContents.get(j) ?? matches[j].content;
190
+ merged += nextContent;
191
+ newContents.set(j, "");
192
+ const opens = countChar(merged, "{");
193
+ const closes = countChar(merged, "}");
194
+ if (closes >= opens) break;
195
+ j++;
196
+ }
197
+ newContents.set(i, merged);
198
+ }
199
+ i++;
200
+ }
201
+ if (newContents.size === 0) return xml;
202
+ let result = "";
203
+ let cursor = 0;
204
+ for (let k = 0; k < matches.length; k++) {
205
+ const mt = matches[k];
206
+ if (!newContents.has(k)) {
207
+ result += xml.slice(cursor, mt.index + mt.fullMatch.length);
208
+ cursor = mt.index + mt.fullMatch.length;
209
+ continue;
210
+ }
211
+ result += xml.slice(cursor, mt.index);
212
+ const attrs = mt.attrs;
213
+ const preserveSpace = attrs.includes('xml:space="preserve"') ? "" : ' xml:space="preserve"';
214
+ result += `<w:t${attrs}${preserveSpace}>${newContents.get(k)}</w:t>`;
215
+ cursor = mt.index + mt.fullMatch.length;
216
+ }
217
+ result += xml.slice(cursor);
218
+ return result;
219
+ }
220
+ function promoteTableLoopTags(xml) {
221
+ const trRegex = /<w:tr\b[^>]*>[\s\S]*?<\/w:tr>/g;
222
+ return xml.replace(trRegex, (rowXml) => {
223
+ const openRegex = /\{#(?!if[\s}])([\w.]+)\}/g;
224
+ const closeRegex = /\{\/(?!if\})([\w.]+)\}/g;
225
+ const opens = [];
226
+ const closes = [];
227
+ let match;
228
+ while ((match = openRegex.exec(rowXml)) !== null) opens.push(match[1]);
229
+ while ((match = closeRegex.exec(rowXml)) !== null) closes.push(match[1]);
230
+ if (opens.length === 1 && closes.length === 1 && opens[0] === closes[0]) {
231
+ const name = opens[0];
232
+ const cleaned = rowXml.replace(`{#${name}}`, "").replace(`{/${name}}`, "");
233
+ return `{#${name}}${cleaned}{/${name}}`;
234
+ }
235
+ return rowXml;
236
+ });
237
+ }
238
+ function countChar(str, char) {
239
+ let count = 0;
240
+ for (let i = 0; i < str.length; i++) {
241
+ if (str[i] === char) count++;
242
+ }
243
+ return count;
244
+ }
245
+
246
+ // src/core/renderer.ts
247
+ function render(nodes, data, formatters) {
248
+ let output = "";
249
+ for (const node of nodes) {
250
+ switch (node.type) {
251
+ case "text":
252
+ output += node.content;
253
+ break;
254
+ case "variable":
255
+ output += renderVariable(node.name, node.formatters, data, formatters);
256
+ break;
257
+ case "loop":
258
+ output += renderLoop(node.name, node.children, data, formatters);
259
+ break;
260
+ case "condition":
261
+ output += renderCondition(
262
+ node.expression,
263
+ node.trueChildren,
264
+ node.falseChildren,
265
+ data,
266
+ formatters
267
+ );
268
+ break;
269
+ }
270
+ }
271
+ return output;
272
+ }
273
+ function renderVariable(name, fmtNames, data, formatters) {
274
+ let value = resolveValue(name, data);
275
+ for (const fmtName of fmtNames) {
276
+ const fn = formatters[fmtName];
277
+ if (!fn) {
278
+ throw new MintdocRenderError(
279
+ `Unknown formatter "${fmtName}". Check your template and available formatters.`
280
+ );
281
+ }
282
+ value = fn(value);
283
+ }
284
+ if (value == null) return "";
285
+ return escapeXml(String(value));
286
+ }
287
+ function renderLoop(name, children, data, formatters) {
288
+ const collection = resolveValue(name, data);
289
+ if (!Array.isArray(collection)) {
290
+ if (collection == null) return "";
291
+ throw new MintdocRenderError(
292
+ `Loop "{#${name}}" expects an array but received ${typeof collection}.`
293
+ );
294
+ }
295
+ let output = "";
296
+ for (let i = 0; i < collection.length; i++) {
297
+ const item = collection[i];
298
+ const scope = createLoopScope(data, item, i, collection.length);
299
+ output += render(children, scope, formatters);
300
+ }
301
+ return output;
302
+ }
303
+ function renderCondition(expression, trueChildren, falseChildren, data, formatters) {
304
+ const value = resolveValue(expression, data);
305
+ if (isTruthy(value)) {
306
+ return render(trueChildren, data, formatters);
307
+ }
308
+ return render(falseChildren, data, formatters);
309
+ }
310
+ function createLoopScope(parentData, item, index, length) {
311
+ const base = {
312
+ ...parentData,
313
+ ".": item,
314
+ "@index": index,
315
+ "@first": index === 0,
316
+ "@last": index === length - 1
317
+ };
318
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
319
+ Object.assign(base, item);
320
+ }
321
+ return base;
322
+ }
323
+ var MintdocRenderError = class extends Error {
324
+ constructor(message) {
325
+ super(message);
326
+ this.name = "MintdocRenderError";
327
+ }
328
+ };
329
+
330
+ // src/utils/zip.ts
331
+ import PizZip from "pizzip";
332
+ function openZip(buffer) {
333
+ return new PizZip(buffer);
334
+ }
335
+ function getFile(zip, path) {
336
+ const file = zip.file(path);
337
+ if (!file) return null;
338
+ return file.asText();
339
+ }
340
+ function setFile(zip, path, content) {
341
+ zip.file(path, content);
342
+ }
343
+ function generateZip(zip) {
344
+ return zip.generate({
345
+ type: "nodebuffer",
346
+ compression: "DEFLATE",
347
+ compressionOptions: { level: 9 }
348
+ });
349
+ }
350
+
351
+ // src/formats/docx.ts
352
+ var TEMPLATE_TARGETS = [
353
+ "word/document.xml",
354
+ // Headers & footers (Word uses numbered names)
355
+ ...Array.from({ length: 3 }, (_, i) => `word/header${i + 1}.xml`),
356
+ ...Array.from({ length: 3 }, (_, i) => `word/footer${i + 1}.xml`)
357
+ ];
358
+ function processDocx(buffer, data, formatters) {
359
+ const zip = openZip(buffer);
360
+ for (const target of TEMPLATE_TARGETS) {
361
+ const xml = getFile(zip, target);
362
+ if (xml === null) continue;
363
+ const processed = processXml(xml, data, formatters);
364
+ setFile(zip, target, processed);
365
+ }
366
+ return generateZip(zip);
367
+ }
368
+ function processXml(xml, data, formatters) {
369
+ const merged = mergeRuns(xml);
370
+ const promoted = promoteTableLoopTags(merged);
371
+ const tokens = tokenize(promoted);
372
+ const ast = parse(tokens);
373
+ return render(ast, data, formatters);
374
+ }
375
+
376
+ // src/plugins/formatters.ts
377
+ var builtinFormatters = {
378
+ uppercase: (value) => String(value ?? "").toUpperCase(),
379
+ lowercase: (value) => String(value ?? "").toLowerCase(),
380
+ capitalize: (value) => {
381
+ const str = String(value ?? "");
382
+ return str.charAt(0).toUpperCase() + str.slice(1);
383
+ }
384
+ };
385
+
386
+ // src/plugins/plugin-api.ts
387
+ function collectFormatters(plugins, extraFormatters) {
388
+ const merged = { ...builtinFormatters };
389
+ for (const plugin of plugins) {
390
+ if (plugin.formatters) {
391
+ Object.assign(merged, plugin.formatters);
392
+ }
393
+ }
394
+ if (extraFormatters) {
395
+ Object.assign(merged, extraFormatters);
396
+ }
397
+ return merged;
398
+ }
399
+
400
+ // src/index.ts
401
+ var VERSION = "0.1.0";
402
+ var Mintdoc = class {
403
+ constructor() {
404
+ this.plugins = [];
405
+ this.extraFormatters = {};
406
+ }
407
+ /**
408
+ * Register a plugin (adds formatters, tag handlers, etc.).
409
+ */
410
+ use(plugin) {
411
+ this.plugins.push(plugin);
412
+ return this;
413
+ }
414
+ /**
415
+ * Register one or more custom formatters.
416
+ */
417
+ addFormatters(formatters) {
418
+ Object.assign(this.extraFormatters, formatters);
419
+ return this;
420
+ }
421
+ /**
422
+ * Render a .docx template with the given data.
423
+ *
424
+ * @param template - The .docx file as a Buffer, ArrayBuffer or Uint8Array.
425
+ * @param data - The data object to inject into the template.
426
+ * @returns A Buffer containing the rendered .docx file.
427
+ */
428
+ render(template, data) {
429
+ const formatters = collectFormatters(this.plugins, this.extraFormatters);
430
+ return processDocx(template, data, formatters);
431
+ }
432
+ };
433
+ function render2(template, data, options) {
434
+ const formatters = collectFormatters(
435
+ options?.plugins ?? [],
436
+ options?.formatters
437
+ );
438
+ return processDocx(template, data, formatters);
439
+ }
440
+ export {
441
+ Mintdoc,
442
+ MintdocParseError,
443
+ MintdocRenderError,
444
+ VERSION,
445
+ render2 as render
446
+ };
447
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/lexer.ts","../src/core/parser.ts","../src/utils/expressions.ts","../src/utils/xml.ts","../src/core/renderer.ts","../src/utils/zip.ts","../src/formats/docx.ts","../src/plugins/formatters.ts","../src/plugins/plugin-api.ts","../src/index.ts"],"sourcesContent":["import type { Token } from \"./types\";\n\n/**\n * Tokenize an XML string that has already been through run-merging.\n *\n * Splits the XML into a flat list of tokens:\n * - `xml` — raw XML content (not a template tag)\n * - `variable` — {varName} or {varName | formatter}\n * - `loop-open` — {#collection}\n * - `loop-close` — {/collection}\n * - `condition-open` — {#if expression}\n * - `condition-else` — {:else}\n * - `condition-close` — {/if}\n */\nexport function tokenize(xml: string): Token[] {\n const tokens: Token[] = [];\n const tagRegex = /\\{([^}]+)\\}/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = tagRegex.exec(xml)) !== null) {\n // Push any XML content before this tag\n if (match.index > lastIndex) {\n tokens.push({ type: \"xml\", content: xml.slice(lastIndex, match.index) });\n }\n\n const raw = match[0];\n const inner = match[1].trim();\n tokens.push(classifyTag(inner, raw));\n\n lastIndex = match.index + match[0].length;\n }\n\n // Push any remaining XML after the last tag\n if (lastIndex < xml.length) {\n tokens.push({ type: \"xml\", content: xml.slice(lastIndex) });\n }\n\n return tokens;\n}\n\n/**\n * Classify the inner content of a template tag into the correct token type.\n */\nfunction classifyTag(inner: string, raw: string): Token {\n // {:else}\n if (inner === \":else\") {\n return { type: \"condition-else\", raw };\n }\n\n // {/if}\n if (inner === \"/if\") {\n return { type: \"condition-close\", raw };\n }\n\n // {/collection}\n if (inner.startsWith(\"/\")) {\n return { type: \"loop-close\", name: inner.slice(1).trim(), raw };\n }\n\n // {#if expression}\n if (inner.startsWith(\"#if \")) {\n return { type: \"condition-open\", expression: inner.slice(4).trim(), raw };\n }\n\n // {#collection}\n if (inner.startsWith(\"#\")) {\n return { type: \"loop-open\", name: inner.slice(1).trim(), raw };\n }\n\n // {variable | formatter1 | formatter2}\n const parts = inner.split(\"|\").map((p) => p.trim());\n const name = parts[0];\n const formatters = parts.slice(1);\n\n return { type: \"variable\", name, formatters, raw };\n}\n","import type { ASTNode, Token } from \"./types\";\n\n/**\n * Parse a flat token list into an AST (tree of nodes).\n *\n * Structural tags ({#loop}, {#if}, {:else}, {/…}) define nesting.\n * Throws descriptive errors on mismatched or unclosed tags.\n */\nexport function parse(tokens: Token[]): ASTNode[] {\n const { nodes, index } = parseNodes(tokens, 0, null);\n\n if (index < tokens.length) {\n const unexpected = tokens[index];\n throw new MintdocParseError(\n `Unexpected closing tag \"${(unexpected as { raw?: string }).raw ?? \"\"}\" without a matching opening tag.`,\n );\n }\n\n return nodes;\n}\n\n// ---------------------------------------------------------------------------\n// Internal recursive descent parser\n// ---------------------------------------------------------------------------\n\ninterface ParseResult {\n nodes: ASTNode[];\n index: number;\n}\n\nfunction parseNodes(\n tokens: Token[],\n start: number,\n closingTag: string | null,\n): ParseResult {\n const nodes: ASTNode[] = [];\n let i = start;\n\n while (i < tokens.length) {\n const token = tokens[i];\n\n switch (token.type) {\n case \"xml\":\n nodes.push({ type: \"text\", content: token.content });\n i++;\n break;\n\n case \"variable\":\n nodes.push({\n type: \"variable\",\n name: token.name,\n formatters: token.formatters,\n });\n i++;\n break;\n\n case \"loop-open\": {\n const loopName = token.name;\n const children = parseNodes(tokens, i + 1, loopName);\n\n // Verify the closing tag matches\n const closer = tokens[children.index];\n if (!closer || closer.type !== \"loop-close\") {\n throw new MintdocParseError(\n `Unclosed loop \"{#${loopName}}\". Add a matching \"{/${loopName}}\" tag.`,\n );\n }\n if (closer.type === \"loop-close\" && closer.name !== loopName) {\n throw new MintdocParseError(\n `Mismatched loop tags: opened \"{#${loopName}}\" but closed with \"{/${closer.name}}\".`,\n );\n }\n\n nodes.push({ type: \"loop\", name: loopName, children: children.nodes });\n i = children.index + 1;\n break;\n }\n\n case \"condition-open\": {\n const expression = token.expression;\n const trueBranch = parseNodes(tokens, i + 1, \"if\");\n\n let trueChildren = trueBranch.nodes;\n let falseChildren: ASTNode[] = [];\n let endIndex = trueBranch.index;\n\n const next = tokens[endIndex];\n\n if (next && next.type === \"condition-else\") {\n // Parse the else branch\n const falseBranch = parseNodes(tokens, endIndex + 1, \"if\");\n falseChildren = falseBranch.nodes;\n endIndex = falseBranch.index;\n }\n\n const closerToken = tokens[endIndex];\n if (!closerToken || closerToken.type !== \"condition-close\") {\n throw new MintdocParseError(\n `Unclosed condition \"{#if ${expression}}\". Add a matching \"{/if}\" tag.`,\n );\n }\n\n nodes.push({\n type: \"condition\",\n expression,\n trueChildren,\n falseChildren,\n });\n i = endIndex + 1;\n break;\n }\n\n // Closing / else tokens signal the end of the current nesting level\n case \"loop-close\":\n case \"condition-close\":\n case \"condition-else\":\n return { nodes, index: i };\n\n default:\n i++;\n }\n }\n\n // If we expected a closing tag but reached end of tokens\n if (closingTag !== null) {\n throw new MintdocParseError(\n `Unexpected end of template. Missing closing tag for \"{#${closingTag}}\".`,\n );\n }\n\n return { nodes, index: i };\n}\n\n// ---------------------------------------------------------------------------\n// Error class\n// ---------------------------------------------------------------------------\n\nexport class MintdocParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MintdocParseError\";\n }\n}\n","/**\n * Resolve a dot-notation path against a data object.\n *\n * Examples:\n * resolveValue(\"name\", { name: \"Alice\" }) → \"Alice\"\n * resolveValue(\"company.name\", { company: { name: \"Acme\" } }) → \"Acme\"\n * resolveValue(\".\", currentItem) → currentItem\n */\nexport function resolveValue(path: string, data: unknown): unknown {\n if (data == null || typeof data !== \"object\") {\n return path === \".\" ? data : undefined;\n }\n\n // \".\" resolves to the special scope key set by loop iterations\n if (path === \".\") {\n const record = data as Record<string, unknown>;\n return \".\" in record ? record[\".\"] : data;\n }\n\n const parts = path.split(\".\");\n let current: unknown = data;\n\n for (const part of parts) {\n if (current == null || typeof current !== \"object\") return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n/**\n * Evaluate whether a value is \"truthy\" in the template sense.\n *\n * - `false`, `null`, `undefined`, `0`, `\"\"` → false\n * - Empty arrays `[]` → false\n * - Everything else → true\n */\nexport function isTruthy(value: unknown): boolean {\n if (Array.isArray(value)) return value.length > 0;\n return Boolean(value);\n}\n","/**\n * Escape special characters for safe insertion into XML text nodes.\n */\nexport function escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/**\n * Merge template tags that Word has split across multiple <w:t> runs.\n *\n * Word often breaks text like `{firstName}` into separate runs:\n * <w:r><w:t>{first</w:t></w:r><w:r><w:t>Name}</w:t></w:r>\n *\n * This function detects unclosed `{` in a <w:t> element and pulls text from\n * subsequent <w:t> elements until the matching `}` is found, consolidating\n * the tag into a single <w:t>.\n */\nexport function mergeRuns(xml: string): string {\n // Collect every <w:t ...>...</w:t> with its position\n const wtRegex = /<w:t(\\s[^>]*)?>([^]*?)<\\/w:t>/g;\n\n interface WtMatch {\n fullMatch: string;\n attrs: string;\n content: string;\n index: number;\n }\n\n const matches: WtMatch[] = [];\n let m: RegExpExecArray | null;\n while ((m = wtRegex.exec(xml)) !== null) {\n matches.push({\n fullMatch: m[0],\n attrs: m[1] || \"\",\n content: m[2],\n index: m.index,\n });\n }\n\n if (matches.length === 0) return xml;\n\n // Track which matches need their content replaced\n const newContents: Map<number, string> = new Map();\n\n let i = 0;\n while (i < matches.length) {\n const content = newContents.get(i) ?? matches[i].content;\n const openCount = countChar(content, \"{\");\n const closeCount = countChar(content, \"}\");\n\n if (openCount > closeCount) {\n // Unclosed brace — pull text from the next <w:t> elements\n let merged = content;\n let j = i + 1;\n\n while (j < matches.length) {\n const nextContent = newContents.get(j) ?? matches[j].content;\n merged += nextContent;\n newContents.set(j, \"\");\n const opens = countChar(merged, \"{\");\n const closes = countChar(merged, \"}\");\n if (closes >= opens) break;\n j++;\n }\n\n newContents.set(i, merged);\n }\n\n i++;\n }\n\n // Rebuild the XML string with updated contents\n if (newContents.size === 0) return xml;\n\n let result = \"\";\n let cursor = 0;\n\n for (let k = 0; k < matches.length; k++) {\n const mt = matches[k];\n if (!newContents.has(k)) {\n // No change — copy everything up to and including this match\n result += xml.slice(cursor, mt.index + mt.fullMatch.length);\n cursor = mt.index + mt.fullMatch.length;\n continue;\n }\n\n // Copy everything before this match\n result += xml.slice(cursor, mt.index);\n\n // Write the updated <w:t> element\n const attrs = mt.attrs;\n const preserveSpace =\n attrs.includes('xml:space=\"preserve\"') ? \"\" : ' xml:space=\"preserve\"';\n result += `<w:t${attrs}${preserveSpace}>${newContents.get(k)}</w:t>`;\n cursor = mt.index + mt.fullMatch.length;\n }\n\n // Append any remaining XML after the last match\n result += xml.slice(cursor);\n\n return result;\n}\n\n/**\n * Promote loop tags that are inside table cells to wrap the entire table row.\n *\n * When a user writes a loop inside a table (e.g. {#items} in the first cell\n * and {/items} in the last cell), the tags need to wrap the whole <w:tr>\n * element so that entire rows are duplicated — not just cell contents.\n *\n * Before: <w:tr><w:tc>..{#items}{name}..</w:tc><w:tc>..{price}{/items}..</w:tc></w:tr>\n * After: {#items}<w:tr><w:tc>..{name}..</w:tc><w:tc>..{price}..</w:tc></w:tr>{/items}\n */\nexport function promoteTableLoopTags(xml: string): string {\n // Match each table row (non-greedy — works for non-nested tables)\n const trRegex = /<w:tr\\b[^>]*>[\\s\\S]*?<\\/w:tr>/g;\n\n return xml.replace(trRegex, (rowXml) => {\n // Find loop open tags (exclude conditions: {#if ...})\n const openRegex = /\\{#(?!if[\\s}])([\\w.]+)\\}/g;\n const closeRegex = /\\{\\/(?!if\\})([\\w.]+)\\}/g;\n\n const opens: string[] = [];\n const closes: string[] = [];\n\n let match;\n while ((match = openRegex.exec(rowXml)) !== null) opens.push(match[1]);\n while ((match = closeRegex.exec(rowXml)) !== null) closes.push(match[1]);\n\n // Only promote when a single loop wraps the row\n if (\n opens.length === 1 &&\n closes.length === 1 &&\n opens[0] === closes[0]\n ) {\n const name = opens[0];\n const cleaned = rowXml\n .replace(`{#${name}}`, \"\")\n .replace(`{/${name}}`, \"\");\n return `{#${name}}${cleaned}{/${name}}`;\n }\n\n return rowXml;\n });\n}\n\nfunction countChar(str: string, char: string): number {\n let count = 0;\n for (let i = 0; i < str.length; i++) {\n if (str[i] === char) count++;\n }\n return count;\n}\n","import type { ASTNode, FormatterMap } from \"./types\";\nimport { resolveValue, isTruthy } from \"../utils/expressions\";\nimport { escapeXml } from \"../utils/xml\";\n\n/**\n * Render an AST tree into a final XML string using the provided data\n * and formatters.\n */\nexport function render(\n nodes: ASTNode[],\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): string {\n let output = \"\";\n\n for (const node of nodes) {\n switch (node.type) {\n case \"text\":\n output += node.content;\n break;\n\n case \"variable\":\n output += renderVariable(node.name, node.formatters, data, formatters);\n break;\n\n case \"loop\":\n output += renderLoop(node.name, node.children, data, formatters);\n break;\n\n case \"condition\":\n output += renderCondition(\n node.expression,\n node.trueChildren,\n node.falseChildren,\n data,\n formatters,\n );\n break;\n }\n }\n\n return output;\n}\n\n// ---------------------------------------------------------------------------\n// Node renderers\n// ---------------------------------------------------------------------------\n\nfunction renderVariable(\n name: string,\n fmtNames: string[],\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): string {\n let value = resolveValue(name, data);\n\n for (const fmtName of fmtNames) {\n const fn = formatters[fmtName];\n if (!fn) {\n throw new MintdocRenderError(\n `Unknown formatter \"${fmtName}\". Check your template and available formatters.`,\n );\n }\n value = fn(value);\n }\n\n if (value == null) return \"\";\n return escapeXml(String(value));\n}\n\nfunction renderLoop(\n name: string,\n children: ASTNode[],\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): string {\n const collection = resolveValue(name, data);\n\n if (!Array.isArray(collection)) {\n if (collection == null) return \"\";\n throw new MintdocRenderError(\n `Loop \"{#${name}}\" expects an array but received ${typeof collection}.`,\n );\n }\n\n let output = \"\";\n\n for (let i = 0; i < collection.length; i++) {\n const item = collection[i];\n const scope = createLoopScope(data, item, i, collection.length);\n output += render(children, scope, formatters);\n }\n\n return output;\n}\n\nfunction renderCondition(\n expression: string,\n trueChildren: ASTNode[],\n falseChildren: ASTNode[],\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): string {\n const value = resolveValue(expression, data);\n\n if (isTruthy(value)) {\n return render(trueChildren, data, formatters);\n }\n return render(falseChildren, data, formatters);\n}\n\n// ---------------------------------------------------------------------------\n// Scope helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Create a child scope for a loop iteration.\n *\n * - Parent data remains accessible (inner properties shadow outer ones).\n * - `.` refers to the current item.\n * - `@index` is the 0-based iteration index.\n * - `@first` / `@last` are booleans.\n */\nfunction createLoopScope(\n parentData: Record<string, unknown>,\n item: unknown,\n index: number,\n length: number,\n): Record<string, unknown> {\n const base: Record<string, unknown> = {\n ...parentData,\n \".\": item,\n \"@index\": index,\n \"@first\": index === 0,\n \"@last\": index === length - 1,\n };\n\n if (item !== null && typeof item === \"object\" && !Array.isArray(item)) {\n Object.assign(base, item as Record<string, unknown>);\n }\n\n return base;\n}\n\n// ---------------------------------------------------------------------------\n// Error class\n// ---------------------------------------------------------------------------\n\nexport class MintdocRenderError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"MintdocRenderError\";\n }\n}\n","import PizZip from \"pizzip\";\n\n/**\n * Open a .docx (or any Office Open XML) buffer as a PizZip instance.\n */\nexport function openZip(buffer: Buffer | ArrayBuffer | Uint8Array): PizZip {\n return new PizZip(buffer);\n}\n\n/**\n * Read a file inside the ZIP archive as a UTF-8 string.\n * Returns `null` if the file does not exist.\n */\nexport function getFile(zip: PizZip, path: string): string | null {\n const file = zip.file(path);\n if (!file) return null;\n return file.asText();\n}\n\n/**\n * Write (or overwrite) a file inside the ZIP archive.\n */\nexport function setFile(zip: PizZip, path: string, content: string): void {\n zip.file(path, content);\n}\n\n/**\n * Generate the ZIP as a Node.js Buffer.\n */\nexport function generateZip(zip: PizZip): Buffer {\n return zip.generate({\n type: \"nodebuffer\",\n compression: \"DEFLATE\",\n compressionOptions: { level: 9 },\n });\n}\n","import type { FormatterMap } from \"../core/types\";\nimport { tokenize } from \"../core/lexer\";\nimport { parse } from \"../core/parser\";\nimport { render } from \"../core/renderer\";\nimport { mergeRuns, promoteTableLoopTags } from \"../utils/xml\";\nimport { openZip, getFile, setFile, generateZip } from \"../utils/zip\";\n\n/**\n * Files inside the .docx archive that can contain template tags.\n * We process document.xml first, then headers/footers.\n */\nconst TEMPLATE_TARGETS = [\n \"word/document.xml\",\n // Headers & footers (Word uses numbered names)\n ...Array.from({ length: 3 }, (_, i) => `word/header${i + 1}.xml`),\n ...Array.from({ length: 3 }, (_, i) => `word/footer${i + 1}.xml`),\n];\n\n/**\n * Process a .docx template buffer with the given data.\n *\n * Pipeline:\n * 1. Open the docx (ZIP)\n * 2. For each XML target: merge split runs → tokenize → parse → render\n * 3. Re-package the ZIP and return a Buffer\n */\nexport function processDocx(\n buffer: Buffer | ArrayBuffer | Uint8Array,\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): Buffer {\n const zip = openZip(buffer);\n\n for (const target of TEMPLATE_TARGETS) {\n const xml = getFile(zip, target);\n if (xml === null) continue;\n\n const processed = processXml(xml, data, formatters);\n setFile(zip, target, processed);\n }\n\n return generateZip(zip);\n}\n\n/**\n * Run the full template pipeline on a raw XML string.\n */\nfunction processXml(\n xml: string,\n data: Record<string, unknown>,\n formatters: FormatterMap,\n): string {\n const merged = mergeRuns(xml);\n const promoted = promoteTableLoopTags(merged);\n const tokens = tokenize(promoted);\n const ast = parse(tokens);\n return render(ast, data, formatters);\n}\n","import type { FormatterMap } from \"../core/types\";\n\n/**\n * Built-in formatters shipped with the free core.\n */\nexport const builtinFormatters: FormatterMap = {\n uppercase: (value) => String(value ?? \"\").toUpperCase(),\n lowercase: (value) => String(value ?? \"\").toLowerCase(),\n capitalize: (value) => {\n const str = String(value ?? \"\");\n return str.charAt(0).toUpperCase() + str.slice(1);\n },\n};\n","import type { MintdocPlugin, FormatterMap } from \"../core/types\";\nimport { builtinFormatters } from \"./formatters\";\n\n/**\n * Collect all formatters from registered plugins, merged on top of builtins.\n */\nexport function collectFormatters(\n plugins: MintdocPlugin[],\n extraFormatters?: FormatterMap,\n): FormatterMap {\n const merged: FormatterMap = { ...builtinFormatters };\n\n for (const plugin of plugins) {\n if (plugin.formatters) {\n Object.assign(merged, plugin.formatters);\n }\n }\n\n if (extraFormatters) {\n Object.assign(merged, extraFormatters);\n }\n\n return merged;\n}\n","import type { FormatterMap, MintdocPlugin, RenderOptions } from \"./core/types\";\nimport { processDocx } from \"./formats/docx\";\nimport { collectFormatters } from \"./plugins/plugin-api\";\n\nexport const VERSION = \"0.1.0\";\n\n/**\n * Mintdoc — modern document templating engine.\n *\n * @example\n * ```ts\n * import { Mintdoc } from \"mintdoc\";\n *\n * const doc = new Mintdoc();\n * const output = doc.render(templateBuffer, { name: \"Alice\" });\n * ```\n */\nexport class Mintdoc {\n private plugins: MintdocPlugin[] = [];\n private extraFormatters: FormatterMap = {};\n\n /**\n * Register a plugin (adds formatters, tag handlers, etc.).\n */\n use(plugin: MintdocPlugin): this {\n this.plugins.push(plugin);\n return this;\n }\n\n /**\n * Register one or more custom formatters.\n */\n addFormatters(formatters: FormatterMap): this {\n Object.assign(this.extraFormatters, formatters);\n return this;\n }\n\n /**\n * Render a .docx template with the given data.\n *\n * @param template - The .docx file as a Buffer, ArrayBuffer or Uint8Array.\n * @param data - The data object to inject into the template.\n * @returns A Buffer containing the rendered .docx file.\n */\n render(\n template: Buffer | ArrayBuffer | Uint8Array,\n data: Record<string, unknown>,\n ): Buffer {\n const formatters = collectFormatters(this.plugins, this.extraFormatters);\n return processDocx(template, data, formatters);\n }\n}\n\n/**\n * Shorthand: render a template without creating an instance.\n *\n * @example\n * ```ts\n * import { render } from \"mintdoc\";\n *\n * const output = render(templateBuffer, { name: \"Alice\" });\n * ```\n */\nexport function render(\n template: Buffer | ArrayBuffer | Uint8Array,\n data: Record<string, unknown>,\n options?: RenderOptions,\n): Buffer {\n const formatters = collectFormatters(\n options?.plugins ?? [],\n options?.formatters,\n );\n return processDocx(template, data, formatters);\n}\n\n// Re-export public types\nexport type { MintdocPlugin, RenderOptions, FormatterMap } from \"./core/types\";\nexport { MintdocParseError } from \"./core/parser\";\nexport { MintdocRenderError } from \"./core/renderer\";\n"],"mappings":"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","names":["render"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "mintdoc",
3
+ "version": "0.1.0",
4
+ "description": "Mint perfect documents from templates. A modern document templating engine for .docx files.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ },
24
+ "keywords": [
25
+ "docx",
26
+ "template",
27
+ "templating",
28
+ "document",
29
+ "generation",
30
+ "word",
31
+ "office",
32
+ "openxml"
33
+ ],
34
+ "author": "Yourica",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Yourica/mintdoc.git"
39
+ },
40
+ "homepage": "https://github.com/Yourica/mintdoc#readme",
41
+ "dependencies": {
42
+ "fast-xml-parser": "^5.2.0",
43
+ "pizzip": "^3.2.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^4.0.18"
50
+ }
51
+ }