@@ -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 = / < m e r g e C e l l s [ ^ > ] * > [ \s \S ] * ?< \/ m e r g e C e l l s > / ;
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 = / < m e r g e C e l l r e f = " ( [ 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 = / < s i > ( [ \s \S ] * ?) < \/ s i > / 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 = / \$ \{ t a b l e : ( [ a - z A - Z 0 - 9 _ ] + ) \. ( [ a - z A - Z 0 - 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 = / < r o w [ ^ > ] * ?> [ \s \S ] * ?< \/ r o w > / 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 ( / < r o w [ ^ > ] * 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 ( / < \/ ? s i > / 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 ( / ( < r o w [ ^ > ] * 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 ( / < r o w [ ^ > ] * 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 ( / < \/ s h e e t D a t a > / , `</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- }
0 commit comments