docxmlater 10.2.3 → 10.2.4

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.
@@ -627,6 +627,7 @@ export class ComplexField {
627
627
  */
628
628
  private _hasResultSection = false;
629
629
  private _formFieldData?: FormFieldData;
630
+ private trackingContext?: import('../tracking/TrackingContext').TrackingContext;
630
631
 
631
632
  /**
632
633
  * Creates a new complex field
@@ -843,6 +844,160 @@ export class ComplexField {
843
844
  return [...this.resultContent];
844
845
  }
845
846
 
847
+ /**
848
+ * Sets the tracking context for automatic change tracking
849
+ * @internal
850
+ */
851
+ _setTrackingContext(context: import('../tracking/TrackingContext').TrackingContext): void {
852
+ this.trackingContext = context;
853
+ }
854
+
855
+ /**
856
+ * Gets the accepted (visible) text from resultContent XMLElements.
857
+ * Processes w:r (plain runs), w:ins (accepted insertions), skips w:del/w:moveFrom.
858
+ * Falls back to this.result if resultContent is empty.
859
+ */
860
+ getAcceptedResultText(): string {
861
+ if (this.resultContent.length === 0) {
862
+ return this.result || '';
863
+ }
864
+ let text = '';
865
+ for (const element of this.resultContent) {
866
+ if (element.name === 'w:r') {
867
+ text += this.extractTextFromRunXml(element);
868
+ } else if (element.name === 'w:ins') {
869
+ for (const child of element.children || []) {
870
+ if (typeof child !== 'string' && child.name === 'w:r') {
871
+ text += this.extractTextFromRunXml(child);
872
+ }
873
+ }
874
+ }
875
+ // Skip w:del, w:moveFrom (deleted/moved text not visible)
876
+ }
877
+ return text || this.result || '';
878
+ }
879
+
880
+ /**
881
+ * Extracts text content from a w:r (run) XMLElement
882
+ */
883
+ private extractTextFromRunXml(run: XMLElement): string {
884
+ let text = '';
885
+ for (const child of run.children || []) {
886
+ if (typeof child !== 'string' && (child.name === 'w:t' || child.name === 'w:delText')) {
887
+ for (const t of child.children || []) {
888
+ if (typeof t === 'string') text += t;
889
+ }
890
+ }
891
+ }
892
+ return text;
893
+ }
894
+
895
+ /**
896
+ * Sets the field result with tracked changes (del/ins pair).
897
+ * Creates a w:del wrapping the old text and a w:ins wrapping the new text,
898
+ * preserving formatting from the original result runs.
899
+ *
900
+ * @param newText - New result text to display
901
+ * @param author - Author name for the revision
902
+ * @param options - Optional formatting control
903
+ */
904
+ setTrackedResult(
905
+ newText: string,
906
+ author: string,
907
+ options?: {
908
+ formatting?: RunFormatting;
909
+ preserveFormatting?: boolean;
910
+ }
911
+ ): this {
912
+ const currentText = this.getAcceptedResultText();
913
+ if (currentText === newText) return this; // No-op if unchanged
914
+
915
+ const date = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
916
+
917
+ // Get revision IDs from TrackingContext if available, else Date.now()-based
918
+ let delId: number, insId: number;
919
+ if (this.trackingContext) {
920
+ const rm = this.trackingContext.getRevisionManager();
921
+ delId = rm.consumeNextId();
922
+ insId = rm.consumeNextId();
923
+ } else {
924
+ const base = Date.now() % 100000000;
925
+ delId = base;
926
+ insId = base + 1;
927
+ }
928
+
929
+ // Resolve formatting: explicit > preserve > resultFormatting
930
+ let rPr: XMLElement | null = null;
931
+ if (options?.formatting) {
932
+ rPr = this.createRunProperties(options.formatting);
933
+ } else if (options?.preserveFormatting) {
934
+ rPr = this.extractFirstVisibleRunProperties();
935
+ }
936
+
937
+ // Build <w:del> with old text
938
+ const delRunChildren: (string | XMLElement)[] = [];
939
+ if (rPr) delRunChildren.push(rPr);
940
+ delRunChildren.push({
941
+ name: 'w:delText',
942
+ attributes: { 'xml:space': 'preserve' },
943
+ children: [currentText],
944
+ });
945
+ const delElement: XMLElement = {
946
+ name: 'w:del',
947
+ attributes: { 'w:id': String(delId), 'w:author': author, 'w:date': date },
948
+ children: [{ name: 'w:r', children: delRunChildren }],
949
+ };
950
+
951
+ // Build <w:ins> with new text
952
+ const insRunChildren: (string | XMLElement)[] = [];
953
+ if (rPr) insRunChildren.push(rPr);
954
+ insRunChildren.push({
955
+ name: 'w:t',
956
+ attributes: { 'xml:space': 'preserve' },
957
+ children: [newText],
958
+ });
959
+ const insElement: XMLElement = {
960
+ name: 'w:ins',
961
+ attributes: { 'w:id': String(insId), 'w:author': author, 'w:date': date },
962
+ children: [{ name: 'w:r', children: insRunChildren }],
963
+ };
964
+
965
+ // Replace content: resultContent takes priority in toXML()
966
+ this.resultContent = [delElement, insElement];
967
+ this.result = newText;
968
+ this.resultRevisions = [];
969
+
970
+ return this;
971
+ }
972
+
973
+ /**
974
+ * Extracts w:rPr from the first visible run in resultContent
975
+ */
976
+ private extractFirstVisibleRunProperties(): XMLElement | null {
977
+ for (const element of this.resultContent) {
978
+ if (element.name === 'w:r') {
979
+ const rPr = (element.children || []).find(
980
+ (c): c is XMLElement => typeof c !== 'string' && c.name === 'w:rPr'
981
+ );
982
+ if (rPr) return rPr;
983
+ } else if (element.name === 'w:ins') {
984
+ for (const child of element.children || []) {
985
+ if (typeof child !== 'string' && child.name === 'w:r') {
986
+ const rPr = (child.children || []).find(
987
+ (c): c is XMLElement => typeof c !== 'string' && c.name === 'w:rPr'
988
+ );
989
+ if (rPr) return rPr;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ // Fall back to resultFormatting
995
+ if (this.resultFormatting) {
996
+ return this.createRunProperties(this.resultFormatting);
997
+ }
998
+ return null;
999
+ }
1000
+
846
1001
  /**
847
1002
  * Sets whether this field spans multiple paragraphs
848
1003
  */