@@ -915,20 +915,15 @@ function processRunForFormatting(run, paragraph, charOffset, textSpans, formatHi
915915
916916/**
917917 * Format REMOVAL via surgical text replacement (pure OOXML approach).
918- * Instead of using w:rPrChange (which Word ignores via insertOoxml),
919- * we treat format removal as a text replacement:
920- * - Original formatted run → wrapped in w:del
921- * - New unformatted run → wrapped in w:ins
922- *
923- * This appears as a surgical text replacement in track changes,
924- * which Word properly honors.
918+ * UPDATED: Uses w:rPr modification with w:rPrChange instead of text replacement (w:del/w:ins).
919+ * This prevents the "strikeout + new text" visualization and shows a proper formatting change.
925920 */
926921function applyFormatRemovalAsSurgicalReplacement ( xmlDoc , textSpans , existingFormatHints , serializer , author , generateRedlines = true ) {
927922 let hasAnyChanges = false ;
928923 const processedRuns = new Set ( ) ;
929924 const dateStr = new Date ( ) . toISOString ( ) ;
930925
931- console . log ( `[OxmlEngine] Surgical format removal: ${ existingFormatHints . length } hints to process` ) ;
926+ console . log ( `[OxmlEngine] Surgical format removal: ${ existingFormatHints . length } hints to process (using w:rPrChange) ` ) ;
932927
933928 for ( const hint of existingFormatHints ) {
934929 const run = hint . run ;
@@ -942,96 +937,54 @@ function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingForm
942937
943938 console . log ( `[OxmlEngine] Processing run for surgical format removal, format:` , hint . format ) ;
944939
945- // Get the text content from this run
946- let textContent = '' ;
947- for ( const child of run . childNodes ) {
948- if ( child . nodeName === 'w:t' ) {
949- textContent += child . textContent || '' ;
950- }
951- }
952-
953- if ( ! textContent ) {
954- console . log ( '[OxmlEngine] Skipping run with no text content' ) ;
955- continue ;
940+ // Get or create w:rPr
941+ let rPr = run . getElementsByTagName ( 'w:rPr' ) [ 0 ] ;
942+ if ( ! rPr ) {
943+ rPr = xmlDoc . createElement ( 'w:rPr' ) ;
944+ run . insertBefore ( rPr , run . firstChild ) ;
956945 }
957946
958- const parentNode = run . parentNode ;
959-
947+ // If generating redlines, track the change
960948 if ( generateRedlines ) {
961- // Create w:del container with the original formatted run
962- const delContainer = xmlDoc . createElement ( 'w:del' ) ;
963- delContainer . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
964- delContainer . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
965- delContainer . setAttribute ( 'w:date' , dateStr ) ;
949+ // Check if there's already a track change on this run's properties
950+ // If so, we append to it or replace is tricky. Simple approach: Add new w:rPrChange.
951+ // But OOXML usually expects one w:rPrChange.
966952
967- // Clone the run for deletion (keeps original formatting)
968- const deletedRun = run . cloneNode ( true ) ;
969-
970- // Convert w:t to w:delText in the deleted run
971- const tElements = deletedRun . getElementsByTagName ( 'w:t' ) ;
972- const tArray = Array . from ( tElements ) ;
973- for ( const t of tArray ) {
974- const delText = xmlDoc . createElement ( 'w:delText' ) ;
975- delText . setAttribute ( 'xml:space' , 'preserve' ) ;
976- delText . textContent = t . textContent ;
977- t . parentNode . replaceChild ( delText , t ) ;
978- }
953+ // Format: <w:rPr> ... <w:rPrChange ...> <w:rPr>...original...</w:rPr> </w:rPrChange> </w:rPr>
979954
980- delContainer . appendChild ( deletedRun ) ;
955+ // 1. Create w:rPrChange element
956+ const rPrChange = xmlDoc . createElement ( 'w:rPrChange' ) ;
957+ rPrChange . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
958+ rPrChange . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
959+ rPrChange . setAttribute ( 'w:date' , dateStr ) ;
981960
982- // Create w:ins container with unformatted run
983- const insContainer = xmlDoc . createElement ( 'w:ins' ) ;
984- insContainer . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
985- insContainer . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
986- insContainer . setAttribute ( 'w:date' , dateStr ) ;
987-
988- // Create new run with explicit format overrides
989- const newRun = xmlDoc . createElement ( 'w:r' ) ;
990-
991- const originalRPr = run . getElementsByTagName ( 'w:rPr' ) [ 0 ] ;
992- const newRPr = originalRPr ? originalRPr . cloneNode ( true ) : xmlDoc . createElement ( 'w:rPr' ) ;
993-
994- // Remove formatting elements and add explicit overrides
995- applyFormatOverridesToRPr ( xmlDoc , newRPr , hint . format ) ;
996-
997- if ( newRPr . childNodes . length > 0 ) {
998- newRun . appendChild ( newRPr ) ;
961+ // 2. Snapshot current rPr state (excluding any existing rPrChange)
962+ const originalRPrSnapshot = xmlDoc . createElement ( 'w:rPr' ) ;
963+ for ( const child of Array . from ( rPr . childNodes ) ) {
964+ if ( child . nodeName !== 'w:rPrChange' ) {
965+ originalRPrSnapshot . appendChild ( child . cloneNode ( true ) ) ;
966+ }
999967 }
968+ rPrChange . appendChild ( originalRPrSnapshot ) ;
1000969
1001- // Add the text element
1002- const newT = xmlDoc . createElement ( 'w:t' ) ;
1003- newT . setAttribute ( 'xml:space' , 'preserve' ) ;
1004- newT . textContent = textContent ;
1005- newRun . appendChild ( newT ) ;
1006-
1007- insContainer . appendChild ( newRun ) ;
1008-
1009- // Insert w:del before the original run, then w:ins after w:del
1010- parentNode . insertBefore ( delContainer , run ) ;
1011- parentNode . insertBefore ( insContainer , run ) ;
1012-
1013- // Remove the original run
1014- parentNode . removeChild ( run ) ;
1015-
1016- console . log ( `[OxmlEngine] Created surgical replacement for "${ textContent } "` ) ;
1017- } else {
1018- // Without redlines, remove formatting by explicit overrides on the run
1019- let rPr = run . getElementsByTagName ( 'w:rPr' ) [ 0 ] ;
1020- if ( ! rPr ) {
1021- rPr = xmlDoc . createElement ( 'w:rPr' ) ;
1022- run . insertBefore ( rPr , run . firstChild ) ;
970+ // 3. Remove any existing w:rPrChange to avoid duplicates strings
971+ const existingChange = rPr . getElementsByTagName ( 'w:rPrChange' ) [ 0 ] ;
972+ if ( existingChange ) {
973+ rPr . removeChild ( existingChange ) ;
1023974 }
1024975
1025- applyFormatOverridesToRPr ( xmlDoc , rPr , hint . format ) ;
1026-
1027- console . log ( `[OxmlEngine] Removed formatting via overrides (no redlines) for "${ textContent } "` ) ;
976+ // 4. Append new w:rPrChange to rPr (usually last child of rPr)
977+ rPr . appendChild ( rPrChange ) ;
1028978 }
1029979
980+ // Apply format overrides (unbold, unitalic, etc.)
981+ applyFormatOverridesToRPr ( xmlDoc , rPr , hint . format ) ;
982+
1030983 hasAnyChanges = true ;
1031984 }
1032985
1033986 if ( hasAnyChanges ) {
1034- console . log ( '[OxmlEngine] Surgical format removal completed successfully' ) ;
987+ console . log ( '[OxmlEngine] Surgical format removal completed successfully (Pure Format Mode) ' ) ;
1035988 return { oxml : serializer . serializeToString ( xmlDoc ) , hasChanges : true } ;
1036989 }
1037990
@@ -1041,7 +994,8 @@ function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingForm
1041994
1042995/**
1043996 * Format ADDITION via surgical text replacement (pure OOXML approach).
1044- * Wraps the original run in w:del and inserts a formatted w:ins run.
997+ * UPDATED: Uses w:rPr modification with w:rPrChange instead of text replacement (w:del/w:ins).
998+ * This prevents the "strikeout + new text" visualization and shows a proper formatting change.
1045999 */
10461000function applyFormatAdditionsAsSurgicalReplacement ( xmlDoc , textSpans , formatHints , serializer , author , generateRedlines = true ) {
10471001 let hasAnyChanges = false ;
@@ -1106,51 +1060,51 @@ function applyFormatAdditionsAsSurgicalReplacement(xmlDoc, textSpans, formatHint
11061060 const parentNode = span . runElement . parentNode ;
11071061 if ( ! parentNode ) continue ;
11081062
1063+ // PURE FORMAT CHANGE LOGIC (New)
1064+ const run = span . runElement ;
1065+
1066+ // Get or create w:rPr
1067+ let rPr = run . getElementsByTagName ( 'w:rPr' ) [ 0 ] ;
1068+ if ( ! rPr ) {
1069+ rPr = xmlDoc . createElement ( 'w:rPr' ) ;
1070+ run . insertBefore ( rPr , run . firstChild ) ;
1071+ }
1072+
1073+ // If generating redlines, track the change
11091074 if ( generateRedlines ) {
1110- const delContainer = xmlDoc . createElement ( 'w:del' ) ;
1111- delContainer . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
1112- delContainer . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
1113- delContainer . setAttribute ( 'w:date' , dateStr ) ;
1114-
1115- const deletedRun = span . runElement . cloneNode ( true ) ;
1116- const tElements = deletedRun . getElementsByTagName ( 'w:t' ) ;
1117- const tArray = Array . from ( tElements ) ;
1118- for ( const t of tArray ) {
1119- const delText = xmlDoc . createElement ( 'w:delText' ) ;
1120- delText . setAttribute ( 'xml:space' , 'preserve' ) ;
1121- delText . textContent = t . textContent ;
1122- t . parentNode . replaceChild ( delText , t ) ;
1123- }
1124- delContainer . appendChild ( deletedRun ) ;
1125-
1126- const insContainer = xmlDoc . createElement ( 'w:ins' ) ;
1127- insContainer . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
1128- insContainer . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
1129- insContainer . setAttribute ( 'w:date' , dateStr ) ;
1130-
1131- const newRun = xmlDoc . createElement ( 'w:r' ) ;
1132- const newRPr = buildAddedFormatRPr ( xmlDoc , span . runElement , desiredFormat ) ;
1133- if ( newRPr && newRPr . childNodes . length > 0 ) {
1134- newRun . appendChild ( newRPr ) ;
1135- }
1136- const newT = xmlDoc . createElement ( 'w:t' ) ;
1137- newT . setAttribute ( 'xml:space' , 'preserve' ) ;
1138- newT . textContent = textContent ;
1139- newRun . appendChild ( newT ) ;
1140- insContainer . appendChild ( newRun ) ;
1141-
1142- parentNode . insertBefore ( delContainer , span . runElement ) ;
1143- parentNode . insertBefore ( insContainer , span . runElement ) ;
1144- parentNode . removeChild ( span . runElement ) ;
1145- } else {
1146- let rPr = span . runElement . getElementsByTagName ( 'w:rPr' ) [ 0 ] || null ;
1147- if ( ! rPr ) {
1148- rPr = xmlDoc . createElement ( 'w:rPr' ) ;
1149- span . runElement . insertBefore ( rPr , span . runElement . firstChild ) ;
1075+ // Check if there's already a track change on this run's properties (avoid duplicates)
1076+ // If one exists, we ideally merge, but replacing the whole rPrChange logic is cleaner
1077+ // for "State A -> State B" transition.
1078+
1079+ // 1. Create w:rPrChange element
1080+ const rPrChange = xmlDoc . createElement ( 'w:rPrChange' ) ;
1081+ rPrChange . setAttribute ( 'w:id' , Math . floor ( Math . random ( ) * 1000000 ) . toString ( ) ) ;
1082+ rPrChange . setAttribute ( 'w:author' , author || 'Gemini AI' ) ;
1083+ rPrChange . setAttribute ( 'w:date' , dateStr ) ;
1084+
1085+ // 2. Snapshot current rPr state (excluding any existing rPrChange)
1086+ const originalRPrSnapshot = xmlDoc . createElement ( 'w:rPr' ) ;
1087+ for ( const child of Array . from ( rPr . childNodes ) ) {
1088+ if ( child . nodeName !== 'w:rPrChange' ) {
1089+ originalRPrSnapshot . appendChild ( child . cloneNode ( true ) ) ;
1090+ }
11501091 }
1151- applyFormatAdditionsToRPr ( xmlDoc , rPr , desiredFormat ) ;
1092+ rPrChange . appendChild ( originalRPrSnapshot ) ;
1093+
1094+ // 3. Remove any existing w:rPrChange
1095+ const existingChange = rPr . getElementsByTagName ( 'w:rPrChange' ) [ 0 ] ;
1096+ if ( existingChange ) {
1097+ rPr . removeChild ( existingChange ) ;
1098+ }
1099+
1100+ // 4. Append new w:rPrChange to rPr
1101+ rPr . appendChild ( rPrChange ) ;
11521102 }
11531103
1104+ // Apply format additions (merging with existing)
1105+ // Helper function should correctly add w:b, w:i, etc. while respecting rPrChange position
1106+ applyFormatAdditionsToRPr ( xmlDoc , rPr , desiredFormat ) ;
1107+
11541108 hasAnyChanges = true ;
11551109 }
11561110
0 commit comments