@js-ak/excel-toolbox 1.4.0 → 1.5.0
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/build/cjs/lib/template/index.js +1 -0
- package/build/cjs/lib/template/memory-write-stream.js +17 -0
- package/build/cjs/lib/template/template-fs.js +4 -223
- package/build/cjs/lib/template/template-memory.js +531 -0
- package/build/cjs/lib/template/utils/extract-xml-declaration.js +1 -1
- package/build/cjs/lib/template/utils/get-max-row-number.js +1 -1
- package/build/cjs/lib/template/utils/index.js +4 -0
- package/build/cjs/lib/template/utils/prepare-row-to-cells.js +13 -0
- package/build/cjs/lib/template/utils/process-merge-cells.js +40 -0
- package/build/cjs/lib/template/utils/process-merge-finalize.js +51 -0
- package/build/cjs/lib/template/utils/process-rows.js +160 -0
- package/build/cjs/lib/template/utils/process-shared-strings.js +45 -0
- package/build/cjs/lib/template/utils/to-excel-column-object.js +2 -10
- package/build/cjs/lib/template/utils/validate-worksheet-xml.js +65 -51
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +15 -10
- package/build/esm/lib/template/index.js +1 -0
- package/build/esm/lib/template/memory-write-stream.js +13 -0
- package/build/esm/lib/template/template-fs.js +4 -223
- package/build/esm/lib/template/template-memory.js +494 -0
- package/build/esm/lib/template/utils/extract-xml-declaration.js +1 -1
- package/build/esm/lib/template/utils/get-max-row-number.js +1 -1
- package/build/esm/lib/template/utils/index.js +4 -0
- package/build/esm/lib/template/utils/prepare-row-to-cells.js +10 -0
- package/build/esm/lib/template/utils/process-merge-cells.js +37 -0
- package/build/esm/lib/template/utils/process-merge-finalize.js +48 -0
- package/build/esm/lib/template/utils/process-rows.js +157 -0
- package/build/esm/lib/template/utils/process-shared-strings.js +42 -0
- package/build/esm/lib/template/utils/to-excel-column-object.js +2 -10
- package/build/esm/lib/template/utils/validate-worksheet-xml.js +65 -51
- package/build/esm/lib/template/utils/write-rows-to-stream.js +15 -10
- package/build/types/lib/template/index.d.ts +1 -0
- package/build/types/lib/template/memory-write-stream.d.ts +6 -0
- package/build/types/lib/template/template-memory.d.ts +85 -0
- package/build/types/lib/template/utils/index.d.ts +4 -0
- package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +1 -0
- package/build/types/lib/template/utils/process-merge-cells.d.ts +20 -0
- package/build/types/lib/template/utils/process-merge-finalize.d.ts +38 -0
- package/build/types/lib/template/utils/process-rows.d.ts +31 -0
- package/build/types/lib/template/utils/process-shared-strings.d.ts +20 -0
- package/build/types/lib/template/utils/validate-worksheet-xml.d.ts +16 -0
- package/build/types/lib/template/utils/write-rows-to-stream.d.ts +6 -2
- package/package.json +5 -3
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.MemoryWriteStream = void 0;
|
4
|
+
class MemoryWriteStream {
|
5
|
+
chunks = [];
|
6
|
+
write(chunk) {
|
7
|
+
this.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8"));
|
8
|
+
return true;
|
9
|
+
}
|
10
|
+
end() {
|
11
|
+
// no-op
|
12
|
+
}
|
13
|
+
toBuffer() {
|
14
|
+
return Buffer.concat(this.chunks);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
exports.MemoryWriteStream = MemoryWriteStream;
|
@@ -121,9 +121,9 @@ class TemplateFs {
|
|
121
121
|
* @experimental This API is experimental and might change in future versions.
|
122
122
|
*/
|
123
123
|
#expandTableRows(sheetXml, sharedStringsXml, replacements) {
|
124
|
-
const { initialMergeCells, mergeCellMatches, modifiedXml, } = processMergeCells(sheetXml);
|
125
|
-
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = processSharedStrings(sharedStringsXml);
|
126
|
-
const { lastIndex, resultRows, rowShift } = processRows({
|
124
|
+
const { initialMergeCells, mergeCellMatches, modifiedXml, } = Utils.processMergeCells(sheetXml);
|
125
|
+
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = Utils.processSharedStrings(sharedStringsXml);
|
126
|
+
const { lastIndex, resultRows, rowShift } = Utils.processRows({
|
127
127
|
mergeCellMatches,
|
128
128
|
replacements,
|
129
129
|
sharedIndexMap,
|
@@ -131,7 +131,7 @@ class TemplateFs {
|
|
131
131
|
sheetMergeCells,
|
132
132
|
sheetXml: modifiedXml,
|
133
133
|
});
|
134
|
-
return
|
134
|
+
return Utils.processMergeFinalize({
|
135
135
|
initialMergeCells,
|
136
136
|
lastIndex,
|
137
137
|
mergeCellMatches,
|
@@ -725,222 +725,3 @@ class TemplateFs {
|
|
725
725
|
}
|
726
726
|
}
|
727
727
|
exports.TemplateFs = TemplateFs;
|
728
|
-
function processMergeCells(sheetXml) {
|
729
|
-
// Regular expression for finding <mergeCells> block
|
730
|
-
const mergeCellsBlockRegex = /<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/;
|
731
|
-
// Find the first <mergeCells> block (if there are multiple, in xlsx usually there is only one)
|
732
|
-
const mergeCellsBlockMatch = sheetXml.match(mergeCellsBlockRegex);
|
733
|
-
const initialMergeCells = [];
|
734
|
-
const mergeCellMatches = [];
|
735
|
-
if (mergeCellsBlockMatch) {
|
736
|
-
const mergeCellsBlock = mergeCellsBlockMatch[0];
|
737
|
-
initialMergeCells.push(mergeCellsBlock);
|
738
|
-
// Extract <mergeCell ref="A1:B2"/> from this block
|
739
|
-
const mergeCellRegex = /<mergeCell ref="([A-Z]+\d+):([A-Z]+\d+)"\/>/g;
|
740
|
-
for (const match of mergeCellsBlock.matchAll(mergeCellRegex)) {
|
741
|
-
mergeCellMatches.push({ from: match[1], to: match[2] });
|
742
|
-
}
|
743
|
-
}
|
744
|
-
// Remove the <mergeCells> block from the XML
|
745
|
-
const modifiedXml = sheetXml.replace(mergeCellsBlockRegex, "");
|
746
|
-
return {
|
747
|
-
initialMergeCells,
|
748
|
-
mergeCellMatches,
|
749
|
-
modifiedXml,
|
750
|
-
};
|
751
|
-
}
|
752
|
-
;
|
753
|
-
function processSharedStrings(sharedStringsXml) {
|
754
|
-
// Final list of merged cells with all changes
|
755
|
-
const sheetMergeCells = [];
|
756
|
-
// Array for storing shared strings
|
757
|
-
const sharedStrings = [];
|
758
|
-
const sharedStringsHeader = Utils.extractXmlDeclaration(sharedStringsXml);
|
759
|
-
// Map for fast lookup of shared string index by content
|
760
|
-
const sharedIndexMap = new Map();
|
761
|
-
// Regular expression for finding <si> elements (shared string items)
|
762
|
-
const siRegex = /<si>([\s\S]*?)<\/si>/g;
|
763
|
-
// Parse sharedStringsXml and fill sharedStrings and sharedIndexMap
|
764
|
-
for (const match of sharedStringsXml.matchAll(siRegex)) {
|
765
|
-
const content = match[1];
|
766
|
-
if (!content)
|
767
|
-
throw new Error("Shared index not found");
|
768
|
-
const fullSi = `<si>${content}</si>`;
|
769
|
-
sharedIndexMap.set(content, sharedStrings.length);
|
770
|
-
sharedStrings.push(fullSi);
|
771
|
-
}
|
772
|
-
return {
|
773
|
-
sharedIndexMap,
|
774
|
-
sharedStrings,
|
775
|
-
sharedStringsHeader,
|
776
|
-
sheetMergeCells,
|
777
|
-
};
|
778
|
-
}
|
779
|
-
;
|
780
|
-
function processRows(data) {
|
781
|
-
const { mergeCellMatches, replacements, sharedIndexMap, sharedStrings, sheetMergeCells, sheetXml, } = data;
|
782
|
-
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
783
|
-
// Array for storing resulting XML rows
|
784
|
-
const resultRows = [];
|
785
|
-
// Previous position of processed part of XML
|
786
|
-
let lastIndex = 0;
|
787
|
-
// Shift for row numbers
|
788
|
-
let rowShift = 0;
|
789
|
-
// Regular expression for finding <row> elements
|
790
|
-
const rowRegex = /<row[^>]*?>[\s\S]*?<\/row>/g;
|
791
|
-
// Process each <row> element
|
792
|
-
for (const match of sheetXml.matchAll(rowRegex)) {
|
793
|
-
// Full XML row
|
794
|
-
const fullRow = match[0];
|
795
|
-
// Start position of the row in XML
|
796
|
-
const matchStart = match.index;
|
797
|
-
// End position of the row in XML
|
798
|
-
const matchEnd = matchStart + fullRow.length;
|
799
|
-
// Add the intermediate XML chunk (if any) between the previous and the current row
|
800
|
-
if (lastIndex !== matchStart) {
|
801
|
-
resultRows.push(sheetXml.slice(lastIndex, matchStart));
|
802
|
-
}
|
803
|
-
lastIndex = matchEnd;
|
804
|
-
// Get row number from r attribute
|
805
|
-
const originalRowNumber = parseInt(fullRow.match(/<row[^>]* r="(\d+)"/)?.[1] ?? "1", 10);
|
806
|
-
// Update row number based on rowShift
|
807
|
-
const shiftedRowNumber = originalRowNumber + rowShift;
|
808
|
-
// Find shared string indexes in cells of the current row
|
809
|
-
const sharedValueIndexes = [];
|
810
|
-
// Regular expression for finding a cell
|
811
|
-
const cellRegex = /<c[^>]*?r="([A-Z]+\d+)"[^>]*?>([\s\S]*?)<\/c>/g;
|
812
|
-
for (const cell of fullRow.matchAll(cellRegex)) {
|
813
|
-
const cellTag = cell[0];
|
814
|
-
// Check if the cell is a shared string
|
815
|
-
const isShared = /t="s"/.test(cellTag);
|
816
|
-
const valueMatch = cellTag.match(/<v>(\d+)<\/v>/);
|
817
|
-
if (isShared && valueMatch) {
|
818
|
-
sharedValueIndexes.push(parseInt(valueMatch[1], 10));
|
819
|
-
}
|
820
|
-
}
|
821
|
-
// Get the text content of shared strings by their indexes
|
822
|
-
const sharedTexts = sharedValueIndexes.map(i => sharedStrings[i]?.replace(/<\/?si>/g, "") ?? "");
|
823
|
-
// Find table placeholders in shared strings
|
824
|
-
const tablePlaceholders = sharedTexts.flatMap(e => [...e.matchAll(TABLE_REGEX)]);
|
825
|
-
// If there are no table placeholders, just shift the row
|
826
|
-
if (tablePlaceholders.length === 0) {
|
827
|
-
const updatedRow = fullRow
|
828
|
-
.replace(/(<row[^>]* r=")(\d+)(")/, `$1${shiftedRowNumber}$3`)
|
829
|
-
.replace(/<c r="([A-Z]+)(\d+)"/g, (_, col) => `<c r="${col}${shiftedRowNumber}"`);
|
830
|
-
resultRows.push(updatedRow);
|
831
|
-
// Update mergeCells for regular row with rowShift
|
832
|
-
const calculatedRowNumber = originalRowNumber + rowShift;
|
833
|
-
for (const { from, to } of mergeCellMatches) {
|
834
|
-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
835
|
-
const [, toCol] = to.match(/^([A-Z]+)(\d+)$/);
|
836
|
-
if (Number(fromRow) === calculatedRowNumber) {
|
837
|
-
const newFrom = `${fromCol}${shiftedRowNumber}`;
|
838
|
-
const newTo = `${toCol}${shiftedRowNumber}`;
|
839
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
840
|
-
}
|
841
|
-
}
|
842
|
-
continue;
|
843
|
-
}
|
844
|
-
// Get the table name from the first placeholder
|
845
|
-
const firstMatch = tablePlaceholders[0];
|
846
|
-
const tableName = firstMatch?.[1];
|
847
|
-
if (!tableName)
|
848
|
-
throw new Error("Table name not found");
|
849
|
-
// Get data for replacement from replacements
|
850
|
-
const array = replacements[tableName];
|
851
|
-
if (!array)
|
852
|
-
continue;
|
853
|
-
if (!Array.isArray(array))
|
854
|
-
throw new Error("Table data is not an array");
|
855
|
-
const tableRowStart = shiftedRowNumber;
|
856
|
-
// Find mergeCells to duplicate (mergeCells that start with the current row)
|
857
|
-
const mergeCellsToDuplicate = mergeCellMatches.filter(({ from }) => {
|
858
|
-
const match = from.match(/^([A-Z]+)(\d+)$/);
|
859
|
-
if (!match)
|
860
|
-
return false;
|
861
|
-
// Row number of the merge cell start position is in the second group
|
862
|
-
const rowNumber = match[2];
|
863
|
-
return Number(rowNumber) === tableRowStart;
|
864
|
-
});
|
865
|
-
// Change the current row to multiple rows from the data array
|
866
|
-
for (let i = 0; i < array.length; i++) {
|
867
|
-
const rowData = array[i];
|
868
|
-
let newRow = fullRow;
|
869
|
-
// Replace placeholders in shared strings with real data
|
870
|
-
sharedValueIndexes.forEach((originalIdx, idx) => {
|
871
|
-
const originalText = sharedTexts[idx];
|
872
|
-
if (!originalText)
|
873
|
-
throw new Error("Shared value not found");
|
874
|
-
// Replace placeholders ${tableName.field} with real data from array data
|
875
|
-
const replacedText = originalText.replace(TABLE_REGEX, (_, tbl, field) => tbl === tableName ? String(rowData?.[field] ?? "") : "");
|
876
|
-
// Add new text to shared strings if it doesn't exist
|
877
|
-
let newIndex;
|
878
|
-
if (sharedIndexMap.has(replacedText)) {
|
879
|
-
newIndex = sharedIndexMap.get(replacedText);
|
880
|
-
}
|
881
|
-
else {
|
882
|
-
newIndex = sharedStrings.length;
|
883
|
-
sharedIndexMap.set(replacedText, newIndex);
|
884
|
-
sharedStrings.push(`<si>${replacedText}</si>`);
|
885
|
-
}
|
886
|
-
// Replace the shared string index in the cell
|
887
|
-
newRow = newRow.replace(`<v>${originalIdx}</v>`, `<v>${newIndex}</v>`);
|
888
|
-
});
|
889
|
-
// Update row number and cell references
|
890
|
-
const newRowNum = shiftedRowNumber + i;
|
891
|
-
newRow = newRow
|
892
|
-
.replace(/<row[^>]* r="\d+"/, rowTag => rowTag.replace(/r="\d+"/, `r="${newRowNum}"`))
|
893
|
-
.replace(/<c r="([A-Z]+)\d+"/g, (_, col) => `<c r="${col}${newRowNum}"`);
|
894
|
-
resultRows.push(newRow);
|
895
|
-
// Add duplicate mergeCells for new rows
|
896
|
-
for (const { from, to } of mergeCellsToDuplicate) {
|
897
|
-
const [, colFrom, rowFrom] = from.match(/^([A-Z]+)(\d+)$/);
|
898
|
-
const [, colTo, rowTo] = to.match(/^([A-Z]+)(\d+)$/);
|
899
|
-
const newFrom = `${colFrom}${Number(rowFrom) + i}`;
|
900
|
-
const newTo = `${colTo}${Number(rowTo) + i}`;
|
901
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
902
|
-
}
|
903
|
-
}
|
904
|
-
// It increases the row shift by the number of added rows minus one replaced
|
905
|
-
rowShift += array.length - 1;
|
906
|
-
const delta = array.length - 1;
|
907
|
-
const calculatedRowNumber = originalRowNumber + rowShift - array.length + 1;
|
908
|
-
if (delta > 0) {
|
909
|
-
for (const merge of mergeCellMatches) {
|
910
|
-
const fromRow = parseInt(merge.from.match(/\d+$/)[0], 10);
|
911
|
-
if (fromRow > calculatedRowNumber) {
|
912
|
-
merge.from = merge.from.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
913
|
-
merge.to = merge.to.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
914
|
-
}
|
915
|
-
}
|
916
|
-
}
|
917
|
-
}
|
918
|
-
return { lastIndex, resultRows, rowShift };
|
919
|
-
}
|
920
|
-
;
|
921
|
-
function processBuild(data) {
|
922
|
-
const { initialMergeCells, lastIndex, mergeCellMatches, resultRows, rowShift, sharedStrings, sharedStringsHeader, sheetMergeCells, sheetXml, } = data;
|
923
|
-
for (const { from, to } of mergeCellMatches) {
|
924
|
-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
925
|
-
const [, toCol, toRow] = to.match(/^([A-Z]+)(\d+)$/);
|
926
|
-
const fromRowNum = Number(fromRow);
|
927
|
-
// These rows have already been processed, don't add duplicates
|
928
|
-
if (fromRowNum <= lastIndex)
|
929
|
-
continue;
|
930
|
-
const newFrom = `${fromCol}${fromRowNum + rowShift}`;
|
931
|
-
const newTo = `${toCol}${Number(toRow) + rowShift}`;
|
932
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
933
|
-
}
|
934
|
-
resultRows.push(sheetXml.slice(lastIndex));
|
935
|
-
// Form XML for mergeCells if there are any
|
936
|
-
const mergeXml = sheetMergeCells.length
|
937
|
-
? `<mergeCells count="${sheetMergeCells.length}">${sheetMergeCells.join("")}</mergeCells>`
|
938
|
-
: initialMergeCells;
|
939
|
-
// Insert mergeCells before the closing sheetData tag
|
940
|
-
const sheetWithMerge = resultRows.join("").replace(/<\/sheetData>/, `</sheetData>${mergeXml}`);
|
941
|
-
// Return modified sheet XML and shared strings
|
942
|
-
return {
|
943
|
-
shared: `${sharedStringsHeader}\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${sharedStrings.length}" uniqueCount="${sharedStrings.length}">${sharedStrings.join("")}</sst>`,
|
944
|
-
sheet: Utils.updateDimension(sheetWithMerge),
|
945
|
-
};
|
946
|
-
}
|