Skip to content

Commit e3c4a7d

Browse files
committed
fix: updated TemplateFs methods
added tests
1 parent 3429cb2 commit e3c4a7d

24 files changed

+1583
-392
lines changed

src/lib/template/template-fs.ts

Lines changed: 4 additions & 318 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,16 @@ export class TemplateFs {
105105
initialMergeCells,
106106
mergeCellMatches,
107107
modifiedXml,
108-
} = processMergeCells(sheetXml);
108+
} = Utils.processMergeCells(sheetXml);
109109

110110
const {
111111
sharedIndexMap,
112112
sharedStrings,
113113
sharedStringsHeader,
114114
sheetMergeCells,
115-
} = processSharedStrings(sharedStringsXml);
115+
} = Utils.processSharedStrings(sharedStringsXml);
116116

117-
const { lastIndex, resultRows, rowShift } = processRows({
117+
const { lastIndex, resultRows, rowShift } = Utils.processRows({
118118
mergeCellMatches,
119119
replacements,
120120
sharedIndexMap,
@@ -123,7 +123,7 @@ export class TemplateFs {
123123
sheetXml: modifiedXml,
124124
});
125125

126-
return processBuild({
126+
return Utils.processMergeFinalize({
127127
initialMergeCells,
128128
lastIndex,
129129
mergeCellMatches,
@@ -873,317 +873,3 @@ export class TemplateFs {
873873
return new TemplateFs(new Set(Object.keys(files)), destination);
874874
}
875875
}
876-
877-
function processMergeCells(sheetXml: string) {
878-
// Regular expression for finding <mergeCells> block
879-
const mergeCellsBlockRegex = /<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/;
880-
881-
// Find the first <mergeCells> block (if there are multiple, in xlsx usually there is only one)
882-
const mergeCellsBlockMatch = sheetXml.match(mergeCellsBlockRegex);
883-
884-
const initialMergeCells: string[] = [];
885-
const mergeCellMatches: { from: string; to: string }[] = [];
886-
887-
if (mergeCellsBlockMatch) {
888-
const mergeCellsBlock = mergeCellsBlockMatch[0];
889-
initialMergeCells.push(mergeCellsBlock);
890-
891-
// Extract <mergeCell ref="A1:B2"/> from this block
892-
const mergeCellRegex = /<mergeCell ref="([A-Z]+\d+):([A-Z]+\d+)"\/>/g;
893-
for (const match of mergeCellsBlock.matchAll(mergeCellRegex)) {
894-
mergeCellMatches.push({ from: match[1]!, to: match[2]! });
895-
}
896-
}
897-
898-
// Remove the <mergeCells> block from the XML
899-
const modifiedXml = sheetXml.replace(mergeCellsBlockRegex, "");
900-
901-
return {
902-
initialMergeCells,
903-
mergeCellMatches,
904-
modifiedXml,
905-
};
906-
};
907-
908-
function processSharedStrings(sharedStringsXml: string) {
909-
// Final list of merged cells with all changes
910-
const sheetMergeCells: string[] = [];
911-
912-
// Array for storing shared strings
913-
const sharedStrings: string[] = [];
914-
const sharedStringsHeader = Utils.extractXmlDeclaration(sharedStringsXml);
915-
916-
// Map for fast lookup of shared string index by content
917-
const sharedIndexMap = new Map<string, number>();
918-
919-
// Regular expression for finding <si> elements (shared string items)
920-
const siRegex = /<si>([\s\S]*?)<\/si>/g;
921-
922-
// Parse sharedStringsXml and fill sharedStrings and sharedIndexMap
923-
for (const match of sharedStringsXml.matchAll(siRegex)) {
924-
const content = match[1];
925-
926-
if (!content) throw new Error("Shared index not found");
927-
928-
const fullSi = `<si>${content}</si>`;
929-
sharedIndexMap.set(content, sharedStrings.length);
930-
sharedStrings.push(fullSi);
931-
}
932-
933-
return {
934-
sharedIndexMap,
935-
sharedStrings,
936-
sharedStringsHeader,
937-
sheetMergeCells,
938-
};
939-
};
940-
941-
function processRows(data: {
942-
replacements: Record<string, unknown>;
943-
sharedIndexMap: Map<string, number>;
944-
mergeCellMatches: { from: string; to: string }[];
945-
sharedStrings: string[];
946-
sheetMergeCells: string[];
947-
sheetXml: string;
948-
}) {
949-
const {
950-
mergeCellMatches,
951-
replacements,
952-
sharedIndexMap,
953-
sharedStrings,
954-
sheetMergeCells,
955-
sheetXml,
956-
} = data;
957-
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
958-
959-
// Array for storing resulting XML rows
960-
const resultRows: string[] = [];
961-
962-
// Previous position of processed part of XML
963-
let lastIndex = 0;
964-
965-
// Shift for row numbers
966-
let rowShift = 0;
967-
968-
// Regular expression for finding <row> elements
969-
const rowRegex = /<row[^>]*?>[\s\S]*?<\/row>/g;
970-
971-
// Process each <row> element
972-
for (const match of sheetXml.matchAll(rowRegex)) {
973-
// Full XML row
974-
const fullRow = match[0];
975-
976-
// Start position of the row in XML
977-
const matchStart = match.index!;
978-
979-
// End position of the row in XML
980-
const matchEnd = matchStart + fullRow.length;
981-
982-
// Add the intermediate XML chunk (if any) between the previous and the current row
983-
if (lastIndex !== matchStart) {
984-
resultRows.push(sheetXml.slice(lastIndex, matchStart));
985-
}
986-
987-
lastIndex = matchEnd;
988-
989-
// Get row number from r attribute
990-
const originalRowNumber = parseInt(fullRow.match(/<row[^>]* r="(\d+)"/)?.[1] ?? "1", 10);
991-
992-
// Update row number based on rowShift
993-
const shiftedRowNumber = originalRowNumber + rowShift;
994-
995-
// Find shared string indexes in cells of the current row
996-
const sharedValueIndexes: number[] = [];
997-
998-
// Regular expression for finding a cell
999-
const cellRegex = /<c[^>]*?r="([A-Z]+\d+)"[^>]*?>([\s\S]*?)<\/c>/g;
1000-
1001-
for (const cell of fullRow.matchAll(cellRegex)) {
1002-
const cellTag = cell[0];
1003-
// Check if the cell is a shared string
1004-
const isShared = /t="s"/.test(cellTag);
1005-
const valueMatch = cellTag.match(/<v>(\d+)<\/v>/);
1006-
1007-
if (isShared && valueMatch) {
1008-
sharedValueIndexes.push(parseInt(valueMatch[1]!, 10));
1009-
}
1010-
}
1011-
1012-
// Get the text content of shared strings by their indexes
1013-
const sharedTexts = sharedValueIndexes.map(i => sharedStrings[i]?.replace(/<\/?si>/g, "") ?? "");
1014-
1015-
// Find table placeholders in shared strings
1016-
const tablePlaceholders = sharedTexts.flatMap(e => [...e.matchAll(TABLE_REGEX)]);
1017-
1018-
// If there are no table placeholders, just shift the row
1019-
if (tablePlaceholders.length === 0) {
1020-
const updatedRow = fullRow
1021-
.replace(/(<row[^>]* r=")(\d+)(")/, `$1${shiftedRowNumber}$3`)
1022-
.replace(/<c r="([A-Z]+)(\d+)"/g, (_, col) => `<c r="${col}${shiftedRowNumber}"`);
1023-
1024-
resultRows.push(updatedRow);
1025-
1026-
// Update mergeCells for regular row with rowShift
1027-
const calculatedRowNumber = originalRowNumber + rowShift;
1028-
1029-
for (const { from, to } of mergeCellMatches) {
1030-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/)!;
1031-
const [, toCol] = to.match(/^([A-Z]+)(\d+)$/)!;
1032-
1033-
if (Number(fromRow) === calculatedRowNumber) {
1034-
const newFrom = `${fromCol}${shiftedRowNumber}`;
1035-
const newTo = `${toCol}${shiftedRowNumber}`;
1036-
1037-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
1038-
}
1039-
}
1040-
1041-
continue;
1042-
}
1043-
1044-
// Get the table name from the first placeholder
1045-
const firstMatch = tablePlaceholders[0];
1046-
const tableName = firstMatch?.[1];
1047-
if (!tableName) throw new Error("Table name not found");
1048-
1049-
// Get data for replacement from replacements
1050-
const array = replacements[tableName];
1051-
if (!array) continue;
1052-
if (!Array.isArray(array)) throw new Error("Table data is not an array");
1053-
1054-
const tableRowStart = shiftedRowNumber;
1055-
1056-
// Find mergeCells to duplicate (mergeCells that start with the current row)
1057-
const mergeCellsToDuplicate = mergeCellMatches.filter(({ from }) => {
1058-
const match = from.match(/^([A-Z]+)(\d+)$/);
1059-
1060-
if (!match) return false;
1061-
1062-
// Row number of the merge cell start position is in the second group
1063-
const rowNumber = match[2];
1064-
1065-
return Number(rowNumber) === tableRowStart;
1066-
});
1067-
1068-
// Change the current row to multiple rows from the data array
1069-
for (let i = 0; i < array.length; i++) {
1070-
const rowData = array[i];
1071-
let newRow = fullRow;
1072-
1073-
// Replace placeholders in shared strings with real data
1074-
sharedValueIndexes.forEach((originalIdx, idx) => {
1075-
const originalText = sharedTexts[idx];
1076-
if (!originalText) throw new Error("Shared value not found");
1077-
1078-
// Replace placeholders ${tableName.field} with real data from array data
1079-
const replacedText = originalText.replace(TABLE_REGEX, (_, tbl, field) =>
1080-
tbl === tableName ? String(rowData?.[field] ?? "") : "",
1081-
);
1082-
1083-
// Add new text to shared strings if it doesn't exist
1084-
let newIndex: number;
1085-
if (sharedIndexMap.has(replacedText)) {
1086-
newIndex = sharedIndexMap.get(replacedText)!;
1087-
} else {
1088-
newIndex = sharedStrings.length;
1089-
sharedIndexMap.set(replacedText, newIndex);
1090-
sharedStrings.push(`<si>${replacedText}</si>`);
1091-
}
1092-
1093-
// Replace the shared string index in the cell
1094-
newRow = newRow.replace(`<v>${originalIdx}</v>`, `<v>${newIndex}</v>`);
1095-
});
1096-
1097-
// Update row number and cell references
1098-
const newRowNum = shiftedRowNumber + i;
1099-
newRow = newRow
1100-
.replace(/<row[^>]* r="\d+"/, rowTag => rowTag.replace(/r="\d+"/, `r="${newRowNum}"`))
1101-
.replace(/<c r="([A-Z]+)\d+"/g, (_, col) => `<c r="${col}${newRowNum}"`);
1102-
1103-
resultRows.push(newRow);
1104-
1105-
// Add duplicate mergeCells for new rows
1106-
for (const { from, to } of mergeCellsToDuplicate) {
1107-
const [, colFrom, rowFrom] = from.match(/^([A-Z]+)(\d+)$/)!;
1108-
const [, colTo, rowTo] = to.match(/^([A-Z]+)(\d+)$/)!;
1109-
const newFrom = `${colFrom}${Number(rowFrom) + i}`;
1110-
const newTo = `${colTo}${Number(rowTo) + i}`;
1111-
1112-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
1113-
}
1114-
}
1115-
1116-
// It increases the row shift by the number of added rows minus one replaced
1117-
rowShift += array.length - 1;
1118-
1119-
const delta = array.length - 1;
1120-
1121-
const calculatedRowNumber = originalRowNumber + rowShift - array.length + 1;
1122-
1123-
if (delta > 0) {
1124-
for (const merge of mergeCellMatches) {
1125-
const fromRow = parseInt(merge.from.match(/\d+$/)![0], 10);
1126-
if (fromRow > calculatedRowNumber) {
1127-
merge.from = merge.from.replace(/\d+$/, r => `${parseInt(r) + delta}`);
1128-
merge.to = merge.to.replace(/\d+$/, r => `${parseInt(r) + delta}`);
1129-
}
1130-
}
1131-
}
1132-
}
1133-
1134-
return { lastIndex, resultRows, rowShift };
1135-
};
1136-
1137-
function processBuild(data: {
1138-
initialMergeCells: string[];
1139-
lastIndex: number;
1140-
mergeCellMatches: { from: string; to: string }[];
1141-
resultRows: string[];
1142-
rowShift: number;
1143-
sharedStrings: string[];
1144-
sharedStringsHeader: string | null;
1145-
sheetMergeCells: string[];
1146-
sheetXml: string;
1147-
}) {
1148-
const {
1149-
initialMergeCells,
1150-
lastIndex,
1151-
mergeCellMatches,
1152-
resultRows,
1153-
rowShift,
1154-
sharedStrings,
1155-
sharedStringsHeader,
1156-
sheetMergeCells,
1157-
sheetXml,
1158-
} = data;
1159-
1160-
for (const { from, to } of mergeCellMatches) {
1161-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/)!;
1162-
const [, toCol, toRow] = to.match(/^([A-Z]+)(\d+)$/)!;
1163-
1164-
const fromRowNum = Number(fromRow);
1165-
// These rows have already been processed, don't add duplicates
1166-
if (fromRowNum <= lastIndex) continue;
1167-
1168-
const newFrom = `${fromCol}${fromRowNum + rowShift}`;
1169-
const newTo = `${toCol}${Number(toRow) + rowShift}`;
1170-
1171-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
1172-
}
1173-
1174-
resultRows.push(sheetXml.slice(lastIndex));
1175-
1176-
// Form XML for mergeCells if there are any
1177-
const mergeXml = sheetMergeCells.length
1178-
? `<mergeCells count="${sheetMergeCells.length}">${sheetMergeCells.join("")}</mergeCells>`
1179-
: initialMergeCells;
1180-
1181-
// Insert mergeCells before the closing sheetData tag
1182-
const sheetWithMerge = resultRows.join("").replace(/<\/sheetData>/, `</sheetData>${mergeXml}`);
1183-
1184-
// Return modified sheet XML and shared strings
1185-
return {
1186-
shared: `${sharedStringsHeader}\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${sharedStrings.length}" uniqueCount="${sharedStrings.length}">${sharedStrings.join("")}</sst>`,
1187-
sheet: Utils.updateDimension(sheetWithMerge),
1188-
};
1189-
}

src/lib/template/utils/column-index-to-letter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ export function columnIndexToLetter(index: number): string {
1818
}
1919

2020
let letters = "";
21+
2122
while (index >= 0) {
2223
letters = String.fromCharCode((index % 26) + 65) + letters;
2324
index = Math.floor(index / 26) - 1;
2425
}
26+
2527
return letters;
2628
}

0 commit comments

Comments
 (0)