@@ -222,19 +222,32 @@ def reject_all(self) -> None:
222222 change .reject ()
223223
224224 def find_and_replace_tracked (
225- self , search_text : str , replace_text : str , author : str = ""
225+ self ,
226+ search_text : str ,
227+ replace_text : str ,
228+ author : str = "" ,
229+ * ,
230+ include_headers_footers : bool = False ,
226231 ) -> int :
227- """Find and replace in accepted-view text using tracked revisions."""
232+ """Find and replace in accepted-view text using tracked revisions.
233+
234+ Replacement always operates on `Paragraph.accepted_text`, meaning inserted
235+ text is searchable and deleted text is ignored.
236+
237+ When `include_headers_footers` is |True|, existing header and footer story
238+ parts are included in the traversal. Linked header/footer definitions are not
239+ materialized merely to support this search; only already-defined parts are
240+ traversed.
241+ """
228242 total_count = 0
229- for paragraph in self .paragraphs :
243+ for paragraph in _iter_paragraphs_in_container ( self ._body ) :
230244 total_count += paragraph .replace_tracked (search_text , replace_text , author = author )
231- for table in self .tables :
232- for row in table .rows :
233- for cell in row .cells :
234- for paragraph in cell .paragraphs :
235- total_count += paragraph .replace_tracked (
236- search_text , replace_text , author = author
237- )
245+ if include_headers_footers :
246+ for header_footer in _iter_defined_header_footer_containers (self ):
247+ for paragraph in _iter_paragraphs_in_container (header_footer ):
248+ total_count += paragraph .replace_tracked (
249+ search_text , replace_text , author = author
250+ )
238251 return total_count
239252
240253 @property
@@ -318,3 +331,43 @@ def clear_content(self) -> _Body:
318331 """
319332 self ._body .clear_content ()
320333 return self
334+
335+
336+ def _iter_paragraphs_in_container (container : BlockItemContainer ) -> Iterator [Paragraph ]:
337+ """Generate paragraphs in `container`, recursing into nested tables."""
338+ yield from container .paragraphs
339+
340+ for table in container .tables :
341+ yield from _iter_paragraphs_in_table (table )
342+
343+
344+ def _iter_paragraphs_in_table (table : Table ) -> Iterator [Paragraph ]:
345+ """Generate paragraphs in `table`, recursing into nested tables in cells."""
346+ for row in table .rows :
347+ for cell in row .cells :
348+ yield from _iter_paragraphs_in_container (cell )
349+
350+
351+ def _iter_defined_header_footer_containers (document : Document ) -> Iterator [BlockItemContainer ]:
352+ """Generate each existing header/footer container once, without creating parts."""
353+ seen_partnames : set [str ] = set ()
354+
355+ for section in document .sections :
356+ for header_footer in (
357+ section .header ,
358+ section .first_page_header ,
359+ section .even_page_header ,
360+ section .footer ,
361+ section .first_page_footer ,
362+ section .even_page_footer ,
363+ ):
364+ if not header_footer ._has_definition : # pyright: ignore[reportPrivateUsage]
365+ continue
366+
367+ definition = header_footer ._definition # pyright: ignore[reportPrivateUsage]
368+ partname = str (definition .partname )
369+ if partname in seen_partnames :
370+ continue
371+
372+ seen_partnames .add (partname )
373+ yield header_footer
0 commit comments