coffee-templater 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +369 -2
- package/dist/index.cjs +647 -0
- package/dist/index.d.cts +494 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.js +597 -0
- package/package.json +84 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 coffee-templater contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,370 @@
|
|
|
1
|
-
#
|
|
1
|
+
# coffee-templater
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Open-source templating engine for **Word (.docx)**, **Excel (.xlsx)** and **PowerPoint (.pptx)** — plus a
|
|
4
|
+
JSON document model to parse Office files on the backend and build them back.
|
|
5
|
+
|
|
6
|
+
Powered by [coffee-templater-engine](https://github.com/vitJR1/coffee-templater-engine) — a Rust core
|
|
7
|
+
compiled to WebAssembly and bundled inside this package. The same engine is meant to back Go and Python
|
|
8
|
+
libraries, so documents render identically everywhere.
|
|
9
|
+
|
|
10
|
+
- Variables, loops, conditions, `else`, filters, images — **all free and MIT-licensed**
|
|
11
|
+
- Configurable tag delimiters (`{{x}}`, `${x}`, `[[x]]`, `<<x>>`, …) and syntax sigils
|
|
12
|
+
- Escaping: `\{{not a tag}}` prints the delimiters literally; values are always XML-escaped
|
|
13
|
+
- Tags split by Word into several runs (`{{na` + `me}}`) are handled transparently
|
|
14
|
+
- Loops repeat inline text, whole paragraphs, table rows (Word, PowerPoint) or sheet rows (Excel)
|
|
15
|
+
- Excel: numbers stay numbers, rows below a loop shift down, formulas / merged cells are adjusted
|
|
16
|
+
- `inspect()` lists template variables and returns a sample data shape — build forms or validate input
|
|
17
|
+
- `parse()` → plain JSON model → `build()`: edit documents programmatically without losing formatting
|
|
18
|
+
- Safe for untrusted uploads: limits against zip bombs, deep XML and runaway loops
|
|
19
|
+
- TypeScript-first, no native dependencies (the engine is WebAssembly), works in Node.js ≥ 18 and
|
|
20
|
+
browsers (ESM + CJS)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install coffee-templater
|
|
24
|
+
|
|
25
|
+
# or straight from GitHub (a tag, branch or commit):
|
|
26
|
+
npm install github:vitJR1/coffee-templater.js#v0.1.1
|
|
27
|
+
# or the prebuilt package attached to a GitHub Release:
|
|
28
|
+
npm install https://github.com/vitJR1/coffee-templater.js/releases/download/v0.1.1/coffee-templater-0.1.1.tgz
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
35
|
+
import { createTemplater } from 'coffee-templater'
|
|
36
|
+
|
|
37
|
+
const templater = createTemplater() // options below
|
|
38
|
+
|
|
39
|
+
const output = templater.render(readFileSync('invoice.docx'), {
|
|
40
|
+
number: 42,
|
|
41
|
+
client: { name: 'Ivan Petrov' },
|
|
42
|
+
items: [
|
|
43
|
+
{ name: 'Coffee', qty: 2, price: 10.5 },
|
|
44
|
+
{ name: 'Tea', qty: 1, price: 4 },
|
|
45
|
+
],
|
|
46
|
+
paid: false,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
writeFileSync('invoice-42.docx', output)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The same call works for `.xlsx` and `.pptx` — the format is detected from the file.
|
|
53
|
+
|
|
54
|
+
Compile once, render many times (much faster for batch generation):
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const template = templater.compile(readFileSync('invoice.docx'))
|
|
58
|
+
for (const invoice of invoices) save(template.render(invoice))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Template syntax
|
|
62
|
+
|
|
63
|
+
| Syntax | Meaning |
|
|
64
|
+
| ---------------------------------------- | ----------------------------------------------------------- |
|
|
65
|
+
| `{{name}}` | variable |
|
|
66
|
+
| `{{client.address.city}}`, `{{items[0].name}}`, `{{map["key with spaces"]}}` | paths |
|
|
67
|
+
| `{{price \| number: 2}}` | filter with arguments |
|
|
68
|
+
| `{{#items}}…{{/items}}` | loop over an array / context object / truthy check |
|
|
69
|
+
| `{{#each items}}…{{/each}}` | explicit loop (arrays and objects, `@key` for objects) |
|
|
70
|
+
| `{{#if paid}}…{{else}}…{{/if}}` | condition |
|
|
71
|
+
| `{{#unless paid}}…{{/unless}}` | negative condition |
|
|
72
|
+
| `{{^items}}…{{/items}}` | inverted section: rendered when value is empty/falsy |
|
|
73
|
+
| `{{/}}` | closes the innermost section |
|
|
74
|
+
| `{{%logo}}` | image |
|
|
75
|
+
| `\{{text}}` | literal `{{text}}` (escaped delimiter) |
|
|
76
|
+
|
|
77
|
+
Inside loops:
|
|
78
|
+
|
|
79
|
+
| Expression | Value |
|
|
80
|
+
| ----------------------- | ------------------------------------------- |
|
|
81
|
+
| `{{.}}` / `{{this}}` | current item |
|
|
82
|
+
| `{{name}}` | property of the current item, then outer scopes |
|
|
83
|
+
| `{{../title}}` | value from the parent scope |
|
|
84
|
+
| `{{@index}}` / `{{@number}}` | 0-based / 1-based position |
|
|
85
|
+
| `{{@first}}`, `{{@last}}`, `{{@length}}`, `{{@key}}` | loop info |
|
|
86
|
+
|
|
87
|
+
String literals accept `"…"`, `'…'` and the typographic quotes Word inserts automatically (`“…”`, `«…»`).
|
|
88
|
+
|
|
89
|
+
### How loops expand
|
|
90
|
+
|
|
91
|
+
The engine decides what to repeat from where the opening and closing tags are:
|
|
92
|
+
|
|
93
|
+
| Tags placement | Repeated content |
|
|
94
|
+
| ---------------------------------------------------- | ------------------------------------- |
|
|
95
|
+
| in the same paragraph / cell | the text between the tags |
|
|
96
|
+
| each alone in its own paragraph | the paragraphs between them (tag paragraphs are removed) |
|
|
97
|
+
| in different cells of one table row (Word/PowerPoint)| the whole table row |
|
|
98
|
+
| in different cells of one row (Excel) | the whole sheet row |
|
|
99
|
+
| in different rows | the rows from the first to the last |
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Word paragraph loop Word / PowerPoint table row Excel row
|
|
103
|
+
────────────────── ───────────────────────────── ─────────────────────────────────────
|
|
104
|
+
{{#items}} | {{#items}}{{name}} | {{qty}}{{/items}} | A3: {{#items}}{{name}} B3: {{qty}} C3: {{/items}}
|
|
105
|
+
• {{name}} — {{price}}
|
|
106
|
+
{{/items}}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`{{else}}` in a paragraph/row loop must be alone in its own paragraph/row.
|
|
110
|
+
|
|
111
|
+
### Filters
|
|
112
|
+
|
|
113
|
+
Built-in: `upper`, `lower`, `capitalize`, `trim`, `replace`, `truncate`, `prepend`, `append`, `default`,
|
|
114
|
+
`number` (`{{n | number: 2, " ", ","}}` → `1 234,50`), `fixed`, `round`, `currency`
|
|
115
|
+
(`{{n | currency: "EUR", "de-DE"}}`), `percent`, `date` (`{{d | date: "DD.MM.YYYY HH:mm"}}`), `join`, `length`,
|
|
116
|
+
`first`, `last`, `reverse`, `sort` (`{{#each items | sort: "price", "desc"}}`), `where`, `slice`, `json`,
|
|
117
|
+
comparison/logic for conditions: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `not`, `and`, `or`, `contains`, `empty`,
|
|
118
|
+
and `yesno` (`{{paid | yesno: "Paid", "Due"}}`).
|
|
119
|
+
|
|
120
|
+
Custom filters:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const templater = createTemplater({
|
|
124
|
+
filters: {
|
|
125
|
+
money: (value, currency = 'RUB') => `${Number(value).toFixed(2)} ${currency}`,
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
// {{ total | money: "USD" }}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Unknown filters are reported when the template is compiled.
|
|
132
|
+
|
|
133
|
+
### Images
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
templater.render(template, {
|
|
137
|
+
logo: { data: pngBytes, width: 120 }, // Uint8Array | ArrayBuffer | Buffer | base64 | data URL
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`width`/`height` are in pixels; if only one is given the other follows the aspect ratio, if none — the
|
|
142
|
+
natural size. PNG, JPEG, GIF, BMP and SVG are detected automatically (`extension` overrides).
|
|
143
|
+
|
|
144
|
+
- **Word** — the picture is inserted inline where the tag is.
|
|
145
|
+
- **PowerPoint** — put `{{%logo}}` in a text box: the box is replaced by the picture, fitted and centered
|
|
146
|
+
in the box's area (placeholders inherit the area from the layout).
|
|
147
|
+
- **Excel** — not supported yet.
|
|
148
|
+
|
|
149
|
+
A falsy value removes the tag (and in PowerPoint the text box).
|
|
150
|
+
|
|
151
|
+
### Excel specifics
|
|
152
|
+
|
|
153
|
+
- A cell that contains only one tag gets the value's type: numbers and booleans become real numeric/boolean
|
|
154
|
+
cells; text around the tag keeps the cell a string.
|
|
155
|
+
- When loops add or remove rows, everything below is shifted: cell references, formulas (`=SUM(D3:D3)`
|
|
156
|
+
becomes `=SUM(D3:D12)`, per-row formulas like `=B3*C3` follow their row), merged cells, conditional
|
|
157
|
+
formatting, data validation and hyperlinks. `calcChain.xml` is dropped and Excel recalculates on open.
|
|
158
|
+
|
|
159
|
+
## Options
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
createTemplater({
|
|
163
|
+
delimiters: ['{{', '}}'], // or { start: '${', end: '}' }
|
|
164
|
+
escapeChar: '\\', // `\{{x}}` → literal; `false` disables escaping
|
|
165
|
+
syntax: { // all sigils/keywords are configurable
|
|
166
|
+
section: '#', inverted: '^', close: '/', image: '%',
|
|
167
|
+
else: 'else', if: 'if', unless: 'unless', each: 'each',
|
|
168
|
+
},
|
|
169
|
+
filters: {}, // merged with built-ins
|
|
170
|
+
nullGetter: (tag) => '', // value for undefined/null (tag.raw, tag.expression, tag.part)
|
|
171
|
+
strict: false, // throw TemplateError 'missing_value' instead of nullGetter
|
|
172
|
+
linebreaks: true, // '\n' → line break, '\t' → tab (Word)
|
|
173
|
+
stringify: (value) => '…', // how non-strings are printed (default: numbers, ISO dates, JSON)
|
|
174
|
+
timeZone: 'Europe/Moscow', // zone for JS Dates in data: 'local' or IANA name (default: UTC)
|
|
175
|
+
limits: { maxEntries: 10_000, maxLoopIterations: 1_000_000 /* … */ }, // safety limits, 0 = off
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Dates.** A JS `Date` is an instant and is formatted in **UTC** by default, so a template renders the
|
|
180
|
+
same on every server. Set `timeZone` to print local time. Date strings without a zone
|
|
181
|
+
(`"2024-05-17"`) are printed as written.
|
|
182
|
+
|
|
183
|
+
**Browsers.** Optionally call `await initEngine()` once at startup: it compiles the WebAssembly engine
|
|
184
|
+
asynchronously instead of on the first call.
|
|
185
|
+
|
|
186
|
+
## Inspecting templates
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const info = templater.inspect(readFileSync('invoice.docx'))
|
|
190
|
+
|
|
191
|
+
info.variables
|
|
192
|
+
// ['number', 'client.name', 'items[].name', 'items[].qty', 'items[].price', 'paid', 'logo']
|
|
193
|
+
|
|
194
|
+
info.schema
|
|
195
|
+
// { number: '', client: { name: '' }, items: [{ name: '', qty: '', price: '' }], paid: true, logo: null }
|
|
196
|
+
|
|
197
|
+
info.tags // full tag tree per part (sections with children, filters, raw tag text)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Errors
|
|
201
|
+
|
|
202
|
+
Template mistakes throw a `TemplateError` with a `code`, the `tag` and the package `part`:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { TemplateError } from 'coffee-templater'
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
templater.compile(file)
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof TemplateError) console.log(e.code, e.tag, e.part)
|
|
211
|
+
// 'unclosed_section' '{{#items}}' 'word/document.xml'
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Template codes: `unclosed_tag`, `unclosed_section`, `unmatched_close`, `invalid_expression`, `unknown_filter`,
|
|
216
|
+
`filter_failed`, `missing_value`, `invalid_section`, `invalid_image`, `unsupported`.
|
|
217
|
+
Engine codes: `invalid_document`, `unsupported_format`, `invalid_model`, `invalid_options`,
|
|
218
|
+
`limit_exceeded`, `host_error`, `internal`.
|
|
219
|
+
|
|
220
|
+
All mistakes in tags are reported at once: `e.errors` lists every `{ code, message, tag, part }`,
|
|
221
|
+
and the error itself describes the first one.
|
|
222
|
+
|
|
223
|
+
## Document model: parse → JSON → build
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { parse, build, extractText } from 'coffee-templater'
|
|
227
|
+
|
|
228
|
+
const original = readFileSync('contract.docx')
|
|
229
|
+
const doc = parse(original) // DocxDocument | XlsxDocument | PptxDocument (plain JSON)
|
|
230
|
+
|
|
231
|
+
// store it, send it to a frontend, edit it...
|
|
232
|
+
const body = doc.format === 'docx' && doc.parts.find((p) => p.kind === 'body')!
|
|
233
|
+
body.blocks.push({
|
|
234
|
+
type: 'paragraph',
|
|
235
|
+
style: 'Heading2',
|
|
236
|
+
children: [{ type: 'run', text: 'Appendix', bold: true }],
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const updated = build(doc, { base: original })
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Formatting is kept: every paragraph/run/cell carries its original properties XML in `props`, and the
|
|
243
|
+
convenient fields (`bold`, `size`, `color`, `align`, …) are applied on top only when you change them.
|
|
244
|
+
Anything the model does not cover (images, fields, bookmarks, content controls) is kept as `raw` XML.
|
|
245
|
+
|
|
246
|
+
### Word (`DocxDocument`)
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
{
|
|
250
|
+
format: 'docx',
|
|
251
|
+
parts: [{
|
|
252
|
+
path: 'word/document.xml', kind: 'body', // also 'header' / 'footer' parts
|
|
253
|
+
blocks: [
|
|
254
|
+
{ type: 'paragraph', style: 'Heading1', align: 'center', list: { id: 1, level: 0 },
|
|
255
|
+
children: [
|
|
256
|
+
{ type: 'run', text: 'Hello\nworld', bold: true, italic: true, underline: true, strike: false,
|
|
257
|
+
color: 'FF0000', highlight: 'yellow', size: 14, font: 'Arial', vertAlign: 'superscript' },
|
|
258
|
+
{ type: 'hyperlink', url: 'https://example.com', children: [{ type: 'run', text: 'link' }] },
|
|
259
|
+
{ type: 'break', kind: 'page' },
|
|
260
|
+
{ type: 'raw', xml: '<w:r><w:drawing>…</w:drawing></w:r>' },
|
|
261
|
+
] },
|
|
262
|
+
{ type: 'table', rows: [{ cells: [{ blocks: [/* paragraphs */], colSpan: 2, vMerge: 'restart' }] }] },
|
|
263
|
+
],
|
|
264
|
+
}],
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`\n` and `\t` in run text are line breaks and tabs. `build()` without `base` creates a new document with
|
|
269
|
+
default styles (`Normal`, `Title`, `Heading1-3`, `ListParagraph`, `TableGrid`).
|
|
270
|
+
|
|
271
|
+
### Excel (`XlsxDocument`)
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
{
|
|
275
|
+
format: 'xlsx',
|
|
276
|
+
sheets: [{
|
|
277
|
+
name: 'Report', path: 'xl/worksheets/sheet1.xml',
|
|
278
|
+
merges: ['A1:D1'],
|
|
279
|
+
rows: [{ index: 1, height: 20, cells: [
|
|
280
|
+
{ ref: 'A1', value: 'Name' },
|
|
281
|
+
{ ref: 'B1', value: 42, style: 3 },
|
|
282
|
+
{ ref: 'C1', value: '2024-05-17', type: 'date' }, // date-formatted cells become ISO strings
|
|
283
|
+
{ ref: 'D1', value: 84, formula: 'B1*2' },
|
|
284
|
+
{ ref: 'E1', value: true },
|
|
285
|
+
] }],
|
|
286
|
+
}],
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Helpers for backend imports/exports:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { parseXlsxFile, sheetToObjects, sheetToArrays, sheetFromArrays, build } from 'coffee-templater'
|
|
294
|
+
|
|
295
|
+
const rows = sheetToObjects(parseXlsxFile(upload).sheets[0]) // [{ Name: 'Tea', Qty: 2 }, ...]
|
|
296
|
+
|
|
297
|
+
const file = build({
|
|
298
|
+
format: 'xlsx',
|
|
299
|
+
sheets: [sheetFromArrays('Export', [['Name', 'Date'], ['Ann', new Date()]])],
|
|
300
|
+
}) // no base needed
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Renaming, adding and removing sheets is supported. Shared formulas are expanded into regular formulas.
|
|
304
|
+
|
|
305
|
+
### PowerPoint (`PptxDocument`)
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
{
|
|
309
|
+
format: 'pptx',
|
|
310
|
+
slides: [{
|
|
311
|
+
path: 'ppt/slides/slide1.xml',
|
|
312
|
+
notes: 'Speaker notes',
|
|
313
|
+
shapes: [
|
|
314
|
+
{ type: 'text', id: 2, name: 'Title 1', placeholder: 'title',
|
|
315
|
+
paragraphs: [{ align: 'center', level: 0,
|
|
316
|
+
children: [{ type: 'run', text: 'Hello', bold: true, size: 40, color: '1F4E79', font: 'Arial' }] }] },
|
|
317
|
+
{ type: 'table', id: 4, name: 'Table 3', rows: [{ cells: [{ paragraphs: [/* … */] }] }] },
|
|
318
|
+
{ type: 'picture', id: 5, name: 'Logo' }, // other shapes are kept from the base file by id
|
|
319
|
+
],
|
|
320
|
+
}],
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Reorder or remove slides/shapes by reordering/removing them in the arrays. Table rows can be added (the last
|
|
325
|
+
row is cloned). Building a pptx requires `base`.
|
|
326
|
+
|
|
327
|
+
### Plain text
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
extractText(file) // docx: paragraphs; xlsx: tab-separated rows per sheet; pptx: text per slide
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Limitations / roadmap
|
|
334
|
+
|
|
335
|
+
- Images in Excel templates
|
|
336
|
+
- Repeating whole slides in PowerPoint (`{{#slides}}`)
|
|
337
|
+
- Excel: shared formulas inside repeated rows and table objects (`ListObject`) ranges are not adjusted
|
|
338
|
+
- Tracked changes and comments are preserved as raw XML, not modelled
|
|
339
|
+
|
|
340
|
+
Contributions are welcome!
|
|
341
|
+
|
|
342
|
+
## Development
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm install # requires Node.js 22+ for the dev tooling
|
|
346
|
+
npm test # vitest
|
|
347
|
+
npm run typecheck
|
|
348
|
+
npm run build # tsup → dist (ESM + CJS + d.ts)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The engine is not vendored: `npm run engine` (run automatically before test/typecheck/build) downloads
|
|
352
|
+
`coffee_core.wasm` from the engine's GitHub Release pinned in `package.json` → `coffeeEngine`, verifies its
|
|
353
|
+
sha256 and embeds it. To develop against a local engine build:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
COFFEE_ENGINE_WASM=../coffee-templater-engine/dist/coffee_core.wasm npm test
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Test fixtures are generated with real Office libraries: see `test/fixtures/generate.py`.
|
|
360
|
+
See [DOCS.md](DOCS.md) (Russian) for the full guide and internals.
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT
|
|
365
|
+
|
|
366
|
+
## Support
|
|
367
|
+
|
|
368
|
+
If this project helps you, consider buying me a coffee ☕
|
|
369
|
+
|
|
370
|
+
- PayPal ypi-2-22@mail.ru
|