Skip to content

Commit

Permalink
feat,wip: integrate SheetTemplater with xlsx
Browse files Browse the repository at this point in the history
  • Loading branch information
iyxan23 committed Sep 15, 2024
1 parent 57a4a3a commit acbf29f
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/sheet/sheet-templater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface TemplatableCell {
cloneWithTextContent(content: string): ThisType<this>;
}

type Indexable2DArray<T> = Record<number, Record<number, T>>;
export type Indexable2DArray<T> = Record<number, Record<number, T>>;

export function createTemplaterNoArgsFunction<R>(
call: () => R,
Expand Down
153 changes: 145 additions & 8 deletions src/xlsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
} from "fast-xml-parser";
import { startVisiting } from "./visitor-editor";
import { Sheet } from "./sheet/sheet";
import { SheetTemplater, TemplatableCell } from "./sheet/sheet-templater";

const SHARED_STRINGS_ENTRY = "xl/sharedStrings.xml";

export async function xlsxFillTemplate(
xlsx: ReadableStream,
output: WritableStream,
// @ts-expect-error will be used later
input: any,
) {
const zipWriter = new ZipWriter(output);
Expand Down Expand Up @@ -53,6 +53,7 @@ export async function xlsxFillTemplate(

const result = await templater.template(
await streamToText(contentStream.readable),
input,
);

await zipWriter.add(entry.filename, new BlobReader(new Blob([result])));
Expand All @@ -61,6 +62,52 @@ export async function xlsxFillTemplate(
zipWriter.close();
}

class XlsxCell implements TemplatableCell {
cell: any;
text: string;

constructor(cell: any) {
// make sure it has the `v` inside of the cell
const cellV = cell["v"];

if (cellV === undefined)
throw new Error("Invalid cell, does not contain 'v'");

this.cell = cell;
this.text = String(cellV);

delete this.cell["v"];
}

buildCell(reference: `${string}${number}`): any {
this.cell["@_r"] = reference;
this.cell["v"] = this.text;

if (isNumeric(this.text)) {
this.cell["@_t"] = "n";
} else {
this.cell["@_t"] = "str";
}

return this.cell;
}

getTextContent(): string {
return this.text;
}

editTextContent(content: string): XlsxCell {
this.text = content;
return this;
}

cloneWithTextContent(content: string): XlsxCell {
const clone: XlsxCell = Object.assign({}, this);
clone.text = content;
return clone;
}
}

class XlsxTemplater {
// a list of raw XML, make sure to pass it as-is. tends to be <t> tags
private sharedStrings: any[] = [];
Expand Down Expand Up @@ -89,7 +136,7 @@ class XlsxTemplater {
console.log(this.sharedStrings);
}

async template(templateContent: string): Promise<string> {
async template(templateContent: string, data: any): Promise<string> {
const options: X2jOptions & XmlBuilderOptions = {
ignoreAttributes: false,
parseTagValue: false,
Expand All @@ -110,25 +157,99 @@ class XlsxTemplater {
// then we turn this sheet into a Sheet object so we can work with it easier
const extractedData = await this.extract(sheetFilled);

console.log(extractedData);
console.log(extractedData.sheet.getSheet());
// pass the sheet into SheetTemplater for it to do the actual templating
const templater = new SheetTemplater(extractedData.sheet, {
rowInfo: extractedData.rowInfo,
colInfo: { ...extractedData.colInfo },
});

const templateResult = templater.interpret(data);
if (templateResult.status === "failed") {
throw new Error(templateResult.error.message);
}

const templatedSheet = templateResult.result;

// "inject" is a bit misleading, we're essentially injecting the cells that
// have been templated into the original sheet, without changing anything
// else other than the fields that do contain the cols, rows and cells.
const injectedSheet = this.inject(
templatedSheet.sheet,
sheetFilled,
templatedSheet.rowInfo ?? {},
templatedSheet.colInfo ?? {},
);

console.log("injected");
console.log(JSON.stringify(injectedSheet, null, 2));

const builder = new XMLBuilder(options);
const result: string = builder.build(sheetFilled);
const result: string = builder.build(injectedSheet);

return result;
}

async inject(
sheetData: Sheet<XlsxCell>,
xlsxData: any,
rowInfo: Record<number, any>,
colInfo: Record<number, any>,
): Promise<any> {
const rows: Record<number, { c: any[] | any } & any> = {};

const { rowBound, colBound } = sheetData.getBounds();
sheetData.optimizeSheet({ rowBound, colBound });

for (let r = 0; r <= rowBound; r++) {
const row: any[] = [];

for (let c = 0; c <= colBound; c++) {
const cell = sheetData.getCell(c, r);
if (cell === null) continue;

row.push(cell.buildCell(createAddressNumber(c, r)));
}

rows[r] = { ...rowInfo[r], c: row.length === 1 ? row[0] : row };
}

const visited = await startVisiting(xlsxData, {
before: {},
after: {
col: [
() => {
return {
newObj: Object.keys(colInfo)
.toSorted((a, b) => parseInt(a) - parseInt(b))
.map((i) => colInfo[parseInt(i)]),
};
},
],
sheetData: [
() => {
return {
newObj: {
row: rows,
},
};
},
],
},
});

return visited;
}

async extract(parsedSheet: any): Promise<{
sheet: Sheet<any>;
sheet: Sheet<XlsxCell>;
rowInfo: Record<number, any>;
colInfo: (any & { min: number; max: number })[];
mergeInfo: {
start: { col: number; row: number };
end: { col: number; row: number };
}[];
}> {
const sheet = new Sheet();
const sheet = new Sheet<XlsxCell>();
const rowInfo: Record<number, any> = {};
const colInfo: (any & { min: number; max: number })[] = [];
const mergeInfo: {
Expand Down Expand Up @@ -202,7 +323,7 @@ class XlsxTemplater {
const { col, row } = parseAddressNumber(cell["@_r"]);
delete cell["@_r"];

sheet.setCell(col, row, cell);
sheet.setCell(col, row, new XlsxCell(cell));
}
},
],
Expand Down Expand Up @@ -279,6 +400,22 @@ function parseAddressNumber(address: string): { row: number; col: number } {
return { row, col };
}

function createAddressNumber(col: number, row: number): `${string}${number}` {
if (col < 0 || row < 0) {
throw new Error("Column and row must be non-negative.");
}

const oneIndexedRow = row + 1;

let columnLetter = "";
while (col >= 0) {
columnLetter = String.fromCharCode(65 + (col % 26)) + columnLetter;
col = Math.floor(col / 26) - 1;
}

return `${columnLetter}${oneIndexedRow}`;
}

async function streamToText(
stream: ReadableStream<Uint8Array>,
): Promise<string> {
Expand Down

0 comments on commit acbf29f

Please sign in to comment.