python-hwpx-automation 6.0.3__py3-none-any.whl

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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Split-run aware HWPX form filling helpers.
3
+
4
+ The helpers in this module are a clean-room implementation based on local
5
+ tests and verified public behavior descriptions. They operate on one section
6
+ XML document at a time, preserving run structure while editing placeholder
7
+ fragments inside ``hp:t`` text nodes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from collections.abc import Mapping, Sequence
14
+ from dataclasses import dataclass
15
+
16
+ import lxml.etree as etree
17
+
18
+ __all__ = [
19
+ "MISSING_CHAR_PR_ID_REF",
20
+ "PLACEHOLDER_RE",
21
+ "FillReport",
22
+ "HeterogeneousWarning",
23
+ "Placeholder",
24
+ "TextFragment",
25
+ "fill_section_bytes",
26
+ "find_split_placeholders",
27
+ "heterogeneous_warnings",
28
+ ]
29
+
30
+ PLACEHOLDER_RE = re.compile(r"\{\{[A-Za-z0-9_.:-]+\}\}")
31
+ MISSING_CHAR_PR_ID_REF = "<missing>"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TextFragment:
36
+ paragraph_index: int
37
+ run_index: int
38
+ text_index: int
39
+ char_pr_id_ref: str | None
40
+ local_start: int
41
+ local_end: int
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Placeholder:
46
+ key: str
47
+ paragraph_index: int
48
+ start: int
49
+ end: int
50
+ split: bool
51
+ charprid_refs: tuple[str, ...]
52
+ fragments: tuple[TextFragment, ...]
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class HeterogeneousWarning:
57
+ key: str
58
+ paragraph_index: int
59
+ charprid_refs: tuple[str, ...]
60
+ message: str
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class FillReport:
65
+ replacements: int
66
+ placeholders_found: int
67
+ missing_keys: tuple[str, ...]
68
+ warnings: tuple[HeterogeneousWarning, ...]
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class _TextNode:
73
+ element: etree._Element
74
+ text: str
75
+ run_index: int
76
+ text_index: int
77
+ char_pr_id_ref: str | None
78
+ start: int
79
+ end: int
80
+
81
+
82
+ def _local_name(tag: object) -> str:
83
+ if not isinstance(tag, str):
84
+ return ""
85
+ return tag.rsplit("}", 1)[-1]
86
+
87
+
88
+ def _parse_section(section_bytes: bytes) -> etree._Element:
89
+ try:
90
+ return etree.fromstring(section_bytes)
91
+ except etree.XMLSyntaxError as exc:
92
+ raise ValueError("invalid section XML") from exc
93
+
94
+
95
+ def _iter_paragraphs(root: etree._Element) -> list[etree._Element]:
96
+ return [element for element in root.iter() if _local_name(element.tag) == "p"]
97
+
98
+
99
+ def _has_nested_block_ancestor(
100
+ run: etree._Element,
101
+ element: etree._Element,
102
+ ) -> bool:
103
+ parent = element.getparent()
104
+ while parent is not None and parent is not run:
105
+ if _local_name(parent.tag) in {"p", "subList", "tbl", "tr", "tc"}:
106
+ return True
107
+ parent = parent.getparent()
108
+ return False
109
+
110
+
111
+ def _iter_run_text_elements(run: etree._Element) -> list[etree._Element]:
112
+ elements: list[etree._Element] = []
113
+ for element in run.iter():
114
+ if _local_name(element.tag) != "t":
115
+ continue
116
+ if _has_nested_block_ancestor(run, element):
117
+ continue
118
+ elements.append(element)
119
+ return elements
120
+
121
+
122
+ def _ensure_plain_text_element(element: etree._Element) -> None:
123
+ if len(element) == 0:
124
+ return
125
+ text_content = "".join(element.itertext())
126
+ if "{{" in text_content or "}}" in text_content:
127
+ raise ValueError("inline hp:t placeholder content is not supported")
128
+
129
+
130
+ def _paragraph_text_nodes(paragraph: etree._Element) -> list[_TextNode]:
131
+ nodes: list[_TextNode] = []
132
+ cursor = 0
133
+ text_index = 0
134
+ runs = [element for element in paragraph if _local_name(element.tag) == "run"]
135
+ for run_index, run in enumerate(runs):
136
+ char_pr_id_ref = run.get("charPrIDRef")
137
+ for child in _iter_run_text_elements(run):
138
+ _ensure_plain_text_element(child)
139
+ text = child.text or ""
140
+ start = cursor
141
+ cursor += len(text)
142
+ nodes.append(
143
+ _TextNode(
144
+ element=child,
145
+ text=text,
146
+ run_index=run_index,
147
+ text_index=text_index,
148
+ char_pr_id_ref=char_pr_id_ref,
149
+ start=start,
150
+ end=cursor,
151
+ )
152
+ )
153
+ text_index += 1
154
+ return nodes
155
+
156
+
157
+ def _unique_refs(nodes: Sequence[_TextNode]) -> tuple[str, ...]:
158
+ refs: list[str] = []
159
+ for node in nodes:
160
+ ref = (
161
+ node.char_pr_id_ref
162
+ if node.char_pr_id_ref is not None
163
+ else MISSING_CHAR_PR_ID_REF
164
+ )
165
+ if ref not in refs:
166
+ refs.append(ref)
167
+ return tuple(refs)
168
+
169
+
170
+ def _placeholder_from_match(
171
+ paragraph_index: int,
172
+ nodes: Sequence[_TextNode],
173
+ match: re.Match[str],
174
+ ) -> Placeholder | None:
175
+ start, end = match.span()
176
+ touched = [node for node in nodes if node.start < end and node.end > start]
177
+ if not touched:
178
+ return None
179
+
180
+ fragments = tuple(
181
+ TextFragment(
182
+ paragraph_index=paragraph_index,
183
+ run_index=node.run_index,
184
+ text_index=node.text_index,
185
+ char_pr_id_ref=node.char_pr_id_ref,
186
+ local_start=max(start, node.start) - node.start,
187
+ local_end=min(end, node.end) - node.start,
188
+ )
189
+ for node in touched
190
+ )
191
+ run_indexes = {node.run_index for node in touched}
192
+ return Placeholder(
193
+ key=match.group(0),
194
+ paragraph_index=paragraph_index,
195
+ start=start,
196
+ end=end,
197
+ split=len(run_indexes) > 1,
198
+ charprid_refs=_unique_refs(touched),
199
+ fragments=fragments,
200
+ )
201
+
202
+
203
+ def _find_placeholders_in_nodes(
204
+ paragraph_index: int,
205
+ nodes: Sequence[_TextNode],
206
+ ) -> list[Placeholder]:
207
+ logical = "".join(node.text for node in nodes)
208
+ placeholders: list[Placeholder] = []
209
+ for match in PLACEHOLDER_RE.finditer(logical):
210
+ placeholder = _placeholder_from_match(paragraph_index, nodes, match)
211
+ if placeholder is not None:
212
+ placeholders.append(placeholder)
213
+ return placeholders
214
+
215
+
216
+ def find_split_placeholders(section_bytes: bytes) -> list[Placeholder]:
217
+ """Find ``{{key}}`` placeholders in section XML.
218
+
219
+ Text is evaluated paragraph-by-paragraph, so placeholders split across
220
+ paragraph boundaries are intentionally not detected.
221
+ """
222
+
223
+ root = _parse_section(section_bytes)
224
+ results: list[Placeholder] = []
225
+ for paragraph_index, paragraph in enumerate(_iter_paragraphs(root)):
226
+ nodes = _paragraph_text_nodes(paragraph)
227
+ if nodes:
228
+ results.extend(_find_placeholders_in_nodes(paragraph_index, nodes))
229
+ return results
230
+
231
+
232
+ def heterogeneous_warnings(
233
+ placeholders: Sequence[Placeholder],
234
+ ) -> list[HeterogeneousWarning]:
235
+ """Report placeholders that span multiple character property references."""
236
+
237
+ warnings: list[HeterogeneousWarning] = []
238
+ for placeholder in placeholders:
239
+ if len(placeholder.charprid_refs) <= 1:
240
+ continue
241
+ refs = ", ".join(placeholder.charprid_refs)
242
+ warnings.append(
243
+ HeterogeneousWarning(
244
+ key=placeholder.key,
245
+ paragraph_index=placeholder.paragraph_index,
246
+ charprid_refs=placeholder.charprid_refs,
247
+ message=(
248
+ f"{placeholder.key} crosses multiple charPrIDRef values "
249
+ f"in paragraph {placeholder.paragraph_index}: {refs}"
250
+ ),
251
+ )
252
+ )
253
+ return warnings
254
+
255
+
256
+ def _remove_linesegarray(paragraph: etree._Element) -> None:
257
+ for element in list(paragraph.iter()):
258
+ if _local_name(element.tag).lower() != "linesegarray":
259
+ continue
260
+ parent = element.getparent()
261
+ if parent is not None:
262
+ parent.remove(element)
263
+
264
+
265
+ def _replace_match_in_nodes(
266
+ nodes: Sequence[_TextNode],
267
+ placeholder: Placeholder,
268
+ replacement: str,
269
+ ) -> None:
270
+ touched = [
271
+ node
272
+ for node in nodes
273
+ if node.start < placeholder.end and node.end > placeholder.start
274
+ ]
275
+ if not touched:
276
+ return
277
+
278
+ updated: dict[_TextNode, str] = {}
279
+ for node in touched:
280
+ local_start = max(placeholder.start, node.start) - node.start
281
+ local_end = min(placeholder.end, node.end) - node.start
282
+ current_text = node.element.text or ""
283
+ updated[node] = current_text[:local_start] + current_text[local_end:]
284
+
285
+ first = touched[0]
286
+ insert_at = placeholder.start - first.start
287
+ first_text = updated[first]
288
+ updated[first] = first_text[:insert_at] + replacement + first_text[insert_at:]
289
+
290
+ for node, text in updated.items():
291
+ node.element.text = text
292
+
293
+
294
+ def fill_section_bytes(
295
+ section_bytes: bytes,
296
+ values: Mapping[str, str],
297
+ ) -> tuple[bytes, FillReport]:
298
+ """Fill mapped placeholders in one section XML document."""
299
+
300
+ root = _parse_section(section_bytes)
301
+ replacements = 0
302
+ placeholders_found = 0
303
+ missing_keys: list[str] = []
304
+ all_placeholders: list[Placeholder] = []
305
+ paragraphs = _iter_paragraphs(root)
306
+
307
+ for paragraph_index in reversed(range(len(paragraphs))):
308
+ paragraph = paragraphs[paragraph_index]
309
+ nodes = _paragraph_text_nodes(paragraph)
310
+ placeholders = _find_placeholders_in_nodes(paragraph_index, nodes)
311
+ placeholders_found += len(placeholders)
312
+ all_placeholders.extend(placeholders)
313
+ paragraph_changed = False
314
+
315
+ for placeholder in reversed(placeholders):
316
+ if placeholder.key not in values:
317
+ if placeholder.key not in missing_keys:
318
+ missing_keys.append(placeholder.key)
319
+ continue
320
+ _replace_match_in_nodes(nodes, placeholder, values[placeholder.key])
321
+ replacements += 1
322
+ paragraph_changed = True
323
+
324
+ if paragraph_changed:
325
+ _remove_linesegarray(paragraph)
326
+
327
+ report = FillReport(
328
+ replacements=replacements,
329
+ placeholders_found=placeholders_found,
330
+ missing_keys=tuple(missing_keys),
331
+ warnings=tuple(heterogeneous_warnings(all_placeholders)),
332
+ )
333
+ return etree.tostring(root, encoding="UTF-8"), report