diff --git a/docs/docx.md b/docs/docx.md new file mode 100644 index 0000000..c8e60e0 --- /dev/null +++ b/docs/docx.md @@ -0,0 +1,96 @@ +## Templating docx + +`ooxml-templater-js`' docx templater uses the same [templating syntax](./template-syntax.md) +as xlsx, with a difference in special calls, and the ability to use +xlsx-like functionality within tables (`r#repeatRow` and `c#repeatCol`). + +You can use the regular function call and variable access expressions: + +``` +Hello, [:name]! +``` + +With an input object of: + +```json +{ "name": "John" } +``` + +Will be evaluated to: + +``` +Hello, John! +``` + +And functions do work the same: + +``` +Cities you've been to: [join [map [:places] place { [:place name] }] ", "] +``` + +With an input object of: + +```json +{ + "places": [ + { "name": "London" }, + { "name": "Paris" }, + { "name": "Berlin" } + ] +} +``` + +Will be evaluated to: + +``` +Cities you've been to: London, Paris, Berlin +``` + +> Given that the standard builtin functions are available. + +### Special calls + +Here are some of the special calls that are available in paragraphs: + +#### `[p#repeatParagraph]` + +``` +[p#repeatParagraph [:count] index] +``` + +Repeats the paragraph the given number of times. + +Here's an example: + +``` +[p#repeatParagraph 10 index]number [:index] +``` + +Will be evaluated to: + +``` +number 0 +number 1 +number 2 +number 3 +number 4 +number 5 +number 6 +number 7 +number 8 +number 9 +``` + +> Each lines is a paragraph. + +#### That's it for now + +### Tables + +Tables in docx are quite different from xlsx, It does not have the same uniform +structure as xlsx "tables", or more like sheets. There are multiple ways to +calculate the size of a table, and its quite complicated to bring a single API +to make it work for all table sizes. + +Tables in docx will support the following special calls: `r#repeatRow` and +`c#repeatCol` that will work the same way as they do in xlsx. diff --git a/src/docx.ts b/src/docx.ts index e6a9d55..27a7c95 100644 --- a/src/docx.ts +++ b/src/docx.ts @@ -6,19 +6,7 @@ import { } from "fast-xml-parser"; import { BlobReader, BlobWriter, ZipReader, ZipWriter } from "@zip.js/zip.js"; import { docxClosingTags } from "./docx-closing-tags-list"; -import { startVisiting, Visitors } from "./visitor-editor"; - -const createTemplaterVisitors: (data: any) => Visitors = (data) => ({ - after: { - "w:t": [ - (doc) => ({ - newObj: JSON.parse(handleTemplate(JSON.stringify(doc), data)), - childCtx: undefined, - }), - ], - }, - before: {}, -}); +import { startVisiting } from "./visitor-editor"; export async function docxFillTemplate( docx: ReadableStream, @@ -52,7 +40,7 @@ export async function docxFillTemplate( preserveOrder: true, ignoreAttributes: false, parseTagValue: false, - trimValues: true, + trimValues: false, unpairedTags: docxClosingTags, suppressUnpairedNode: false, suppressEmptyNode: true, @@ -61,13 +49,10 @@ export async function docxFillTemplate( const parser = new XMLParser(options); const doc = parser.parse(data); - const filledDoc = await startVisiting( - doc, - createTemplaterVisitors(input), - ); + const result = await templateDocument(doc, input); const builder = new XMLBuilder(options); - const newDoc: string = builder.build(filledDoc); + const newDoc: string = builder.build(result); await zipWriter.add( entry.filename, @@ -86,21 +71,110 @@ export async function docxFillTemplate( zipWriter.close(); } -const re = /\${([^}]+)}/g; +type BodyElement = + | { + type: "paragraph"; + obj: any; + text: { path: string[]; text: string } | undefined; + } + | { type: "table"; obj: any } + | { type: "other"; obj: any }; + +function rebuildBodyElements(items: BodyElement[]): any[] { + // rebuild the elements + const newBodyItems = items.map((item) => { + if (item.type === "paragraph") { + const { text } = item; + if (!text) return item.obj; -function handleTemplate(haystack: string, data: any): string { - let match; - let result = haystack; + const { path, text: textValue } = text; - console.log(" handling template"); - while ((match = re.exec(haystack)) != null) { - console.log(" match"); - console.log(" " + match[1]); + const t = path.reduce((acc, p) => acc[p], item.obj); - const val = data[match[1]!] ?? ""; - result = result.replace(match[0], val); - console.log(" replaced with " + val); + t["#text"] = textValue; + + return item.obj; + } else if (item.type === "table") { + return item.obj; + } else { + return item.obj; + } + }); + + return newBodyItems; +} + +async function collectBodyElements(body: any): Promise { + // collect all the elements inside this body + const bodyChildren = Array.isArray(body) ? body : [body]; + const items: BodyElement[] = []; + + for (const item of bodyChildren) { + if (item["w:p"]) { + // this is a paragraph + let curItemText: { path: string[]; text: string } | undefined; + + await startVisiting(item, { + before: { + "w:t": [ + (children, path) => { + if (!Array.isArray(children)) return; + const textIdx = children.findIndex((a) => !!a["#text"]); + if (textIdx === -1) return; + + const text = children[textIdx]["#text"]; + if (typeof text !== "string") return; + + console.log("PATHHHHH"); + console.log(path); + + curItemText = { text, path: [...path, String(textIdx)] }; + }, + ], + }, + after: {}, + }); + + items.push({ + type: "paragraph", + obj: item, + text: curItemText, + }); + } else if (item["w:tbl"]) { + // this is a table + items.push({ type: "table", obj: item }); + } else { + items.push({ type: "other", obj: item }); + } } - return result; + return items; +} + +async function getBody(xml: any): Promise { + let bodyContent; + + await startVisiting(xml, { + before: { + "w:body": [(children) => (bodyContent = children)], + }, + after: {}, + }); + + return bodyContent; +} + +async function templateDocument(xml: any, input: any): Promise { + const body = await getBody(xml); + if (!body) return xml; + + const items = await collectBodyElements(body); + const newBodyItems = rebuildBodyElements(items); + + return await startVisiting(xml, { + before: {}, + after: { + "w:body": [() => ({ newObj: newBodyItems })], + }, + }); } diff --git a/src/visitor-editor.ts b/src/visitor-editor.ts index 1c86dcb..b989858 100644 --- a/src/visitor-editor.ts +++ b/src/visitor-editor.ts @@ -2,12 +2,14 @@ export type Visitors = { before: { [key: string]: (( doc: any, + path: string[], parentCtx?: any, ) => { newObj?: any; childCtx?: any } | void)[]; }; after: { [key: string]: (( doc: any, + path: string[], parentCtx?: any, ) => { newObj?: any; childCtx?: any } | void)[]; }; @@ -35,7 +37,7 @@ function visitObject( console.log(" str: " + JSON.stringify(obj)); for (const visitor of visitors.before[key]) { - const result = visitor(theObj, thisCtx); + const result = visitor(theObj, [...path], thisCtx); if (result) { if (result.newObj) { @@ -64,7 +66,7 @@ function visitObject( console.log(" str: " + JSON.stringify(obj)); for (const visitor of visitors.after[key]) { - const result = visitor(theObj, thisCtx); + const result = visitor(theObj, [...path], thisCtx); if (result) { if (result.newObj) { diff --git a/src/xlsx.ts b/src/xlsx.ts index ba4ee28..0134134 100644 --- a/src/xlsx.ts +++ b/src/xlsx.ts @@ -417,7 +417,7 @@ class XlsxTemplater { await startVisiting(parsedSheet, { before: { mergeCell: [ - (obj, _ctx) => { + (obj) => { console.log(obj); mergeInfo.push( ...(Array.isArray(obj) ? obj : [obj]).map((o) => { @@ -433,7 +433,7 @@ class XlsxTemplater { }, ], col: [ - (obj, _ctx) => { + (obj) => { const cols = Array.isArray(obj) ? obj : [obj]; for (const col of cols) { @@ -452,7 +452,7 @@ class XlsxTemplater { }, ], row: [ - (obj, _ctx) => { + (obj) => { const rows = Array.isArray(obj) ? obj : [obj]; for (const row of rows) { @@ -471,7 +471,7 @@ class XlsxTemplater { }, ], c: [ - (obj, _ctx) => { + (obj) => { const cells = Array.isArray(obj) ? obj : [obj]; for (const cell of cells) { @@ -499,11 +499,10 @@ class XlsxTemplater { before: {}, after: { c: [ - (obj, ctx) => { + (obj) => { if (obj["v"]) { // a single node return { - childCtx: ctx, newObj: obj["@_t"] === "s" && isNumeric(obj["v"]) ? { @@ -515,10 +514,9 @@ class XlsxTemplater { }; } - if (!Array.isArray(obj)) return { childCtx: ctx }; + if (!Array.isArray(obj)) return {}; return { - childCtx: ctx, newObj: obj.map((o) => o["@_t"] === "s" && isNumeric(o["v"]) ? {