Skip to content

Commit

Permalink
wip: partially impl docx templating
Browse files Browse the repository at this point in the history
  • Loading branch information
iyxan23 committed Sep 26, 2024
1 parent 6fb2f0d commit 8b2da57
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 41 deletions.
96 changes: 96 additions & 0 deletions docs/docx.md
Original file line number Diff line number Diff line change
@@ -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.
136 changes: 105 additions & 31 deletions src/docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -52,7 +40,7 @@ export async function docxFillTemplate(
preserveOrder: true,
ignoreAttributes: false,
parseTagValue: false,
trimValues: true,
trimValues: false,
unpairedTags: docxClosingTags,
suppressUnpairedNode: false,
suppressEmptyNode: true,
Expand All @@ -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,
Expand All @@ -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<BodyElement[]> {
// 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<any | undefined> {
let bodyContent;

await startVisiting(xml, {
before: {
"w:body": [(children) => (bodyContent = children)],
},
after: {},
});

return bodyContent;
}

async function templateDocument(xml: any, input: any): Promise<any> {
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 })],
},
});
}
6 changes: 4 additions & 2 deletions src/visitor-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[];
};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 6 additions & 8 deletions src/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -433,7 +433,7 @@ class XlsxTemplater {
},
],
col: [
(obj, _ctx) => {
(obj) => {
const cols = Array.isArray(obj) ? obj : [obj];

for (const col of cols) {
Expand All @@ -452,7 +452,7 @@ class XlsxTemplater {
},
],
row: [
(obj, _ctx) => {
(obj) => {
const rows = Array.isArray(obj) ? obj : [obj];

for (const row of rows) {
Expand All @@ -471,7 +471,7 @@ class XlsxTemplater {
},
],
c: [
(obj, _ctx) => {
(obj) => {
const cells = Array.isArray(obj) ? obj : [obj];

for (const cell of cells) {
Expand Down Expand Up @@ -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"])
? {
Expand All @@ -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"])
? {
Expand Down

0 comments on commit 8b2da57

Please sign in to comment.