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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- 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
|