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,621 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Narrow typed native bridge used only by blueprint replay.
3
+
4
+ This module accepts validated blueprint records. It deliberately exposes no
5
+ XML, namespace, package-part, or arbitrary-attribute input.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, cast
12
+
13
+ from hwpx.document import HwpxDocument
14
+ from hwpx.oxml.namespaces import HH, HP
15
+
16
+ from ..commands import _add, _insert_direct_child, _insert_inline, _remove_inline_element, _table_caption
17
+ from ..document import HwpxAgentDocument, NodeRecord
18
+ from ..model import AgentContractError
19
+
20
+ _HWP_UNITS_PER_MM = 7200 / 25.4
21
+ _INLINE_KINDS = frozenset({"table", "picture", "shape", "footnote", "endnote"})
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class CreatedBinding:
26
+ kind: str
27
+ native: Any
28
+ native_id: str | None = None
29
+
30
+
31
+ def _local_name(element: Any) -> str:
32
+ return str(getattr(element, "tag", "")).rsplit("}", 1)[-1]
33
+
34
+
35
+ def _direct_child(element: Any, kind: str) -> Any | None:
36
+ return next((child for child in element if _local_name(child) == kind), None)
37
+
38
+
39
+ def _mm_units(value: object, default: int) -> int:
40
+ if value is None:
41
+ return default
42
+ return max(1, round(float(cast(Any, value)) * _HWP_UNITS_PER_MM))
43
+
44
+
45
+ def _bool(value: object, default: bool = False) -> bool:
46
+ return value if isinstance(value, bool) else default
47
+
48
+
49
+ def create_paragraph_style(
50
+ document: HwpxDocument, properties: dict[str, Any], *, source_key: str
51
+ ) -> dict[str, str]:
52
+ """Create one allow-listed paragraph style with a collision-free name."""
53
+
54
+ if not document._root.headers: # type: ignore[attr-defined]
55
+ raise AgentContractError("unsupported_content", "target has no style header", target=source_key)
56
+ header = document._root.headers[0] # type: ignore[attr-defined]
57
+ styles = header._styles_element() # type: ignore[attr-defined]
58
+ if styles is None:
59
+ raise AgentContractError("unsupported_content", "target has no style registry", target=source_key)
60
+ base_name = str(properties.get("name") or "Blueprint Style")[:200]
61
+ existing_names = {str(style.name) for style in document.styles.values() if style.name}
62
+ target_name = base_name
63
+ if target_name in existing_names:
64
+ target_name = f"{base_name}__bp_{source_key.rsplit(':', 1)[-1][:8]}"
65
+ para_pr_id = header.ensure_paragraph_format(
66
+ base_para_pr_id="0",
67
+ alignment=properties.get("alignment"),
68
+ line_spacing_percent=properties.get("lineSpacingPercent"),
69
+ break_setting={
70
+ key: value
71
+ for key, value in {
72
+ "keep_with_next": properties.get("keepWithNext"),
73
+ "page_break_before": properties.get("breakBefore"),
74
+ }.items()
75
+ if isinstance(value, bool)
76
+ }
77
+ or None,
78
+ )
79
+ style_id = header._allocate_ref_id(styles, f"{HH}style") # type: ignore[attr-defined]
80
+ attrs = {
81
+ "id": style_id,
82
+ "type": str(properties.get("type") or "PARA"),
83
+ "name": target_name,
84
+ "engName": str(properties.get("englishName") or target_name)[:200],
85
+ "paraPrIDRef": para_pr_id,
86
+ "charPrIDRef": "0",
87
+ "nextStyleIDRef": style_id,
88
+ "langID": "0",
89
+ "lockForm": "1" if properties.get("lockForm") else "0",
90
+ }
91
+ styles.append(styles.makeelement(f"{HH}style", attrs))
92
+ header._update_item_count(styles, f"{HH}style") # type: ignore[attr-defined]
93
+ header.mark_dirty()
94
+ return {"styleIDRef": style_id, "paraPrIDRef": para_pr_id, "name": target_name}
95
+
96
+
97
+ def create_character_format(document: HwpxDocument, properties: dict[str, Any]) -> str:
98
+ return document.ensure_run_style(
99
+ bold=_bool(properties.get("bold")),
100
+ italic=_bool(properties.get("italic")),
101
+ underline=_bool(properties.get("underline")),
102
+ color=properties.get("color"),
103
+ size=properties.get("fontSizePt"),
104
+ )
105
+
106
+
107
+ def create_numbering(document: HwpxDocument, properties: dict[str, Any]) -> str:
108
+ raw_type = str(properties.get("type") or "NUMBER").upper()
109
+ kind = "bullet" if raw_type == "BULLET" else "number"
110
+ level = max(0, int(properties.get("level") or 0))
111
+ refs = document.ensure_numbering(kind=kind, levels=[{} for _ in range(level + 1)])
112
+ return refs[level]
113
+
114
+
115
+ def _move_to_host_run(paragraph: Any, native: Any, host_run: Any) -> None:
116
+ element = getattr(native, "element", native)
117
+ temporary_run = None
118
+ for run in list(paragraph.element):
119
+ if _local_name(run) == "run" and any(child is element for child in run):
120
+ temporary_run = run
121
+ break
122
+ if temporary_run is not None and temporary_run is not host_run.element:
123
+ temporary_run.remove(element)
124
+ if len(temporary_run) == 0:
125
+ paragraph.element.remove(temporary_run)
126
+ host_run.element.append(element)
127
+ paragraph.section.mark_dirty()
128
+ _remove_empty_text_placeholders(host_run)
129
+
130
+
131
+ def _remove_empty_text_placeholders(run: Any) -> None:
132
+ """Drop builder placeholders before attaching native controls.
133
+
134
+ Hancom ignores a table or inline control that follows an empty ``t`` in the
135
+ same run even though the semantic reader can still see it. Preserve real
136
+ run text, but remove empty builder-created text nodes from control runs.
137
+ """
138
+
139
+ for child in list(run.element):
140
+ if _local_name(child) == "t" and not "".join(child.itertext()) and not child.tail:
141
+ run.element.remove(child)
142
+
143
+
144
+ class TypedNativeBridge:
145
+ """Construct validated blueprint nodes through existing typed primitives."""
146
+
147
+ def __init__(
148
+ self,
149
+ document: HwpxDocument,
150
+ manifest: dict[str, Any],
151
+ dependency_targets: dict[str, dict[str, Any]],
152
+ ) -> None:
153
+ self.document = document
154
+ self.manifest = manifest
155
+ self.nodes = {str(node["blueprintId"]): node for node in manifest["nodes"]}
156
+ self.dependency_targets = dependency_targets
157
+ self.created: dict[str, CreatedBinding] = {}
158
+ self.references = {
159
+ (str(item["from"]), str(item["field"])): str(item["to"])
160
+ for item in manifest["references"]
161
+ }
162
+ self._identity_seed = self._max_numeric_identity()
163
+
164
+ def _max_numeric_identity(self) -> int:
165
+ maximum = 0
166
+ for section in self.document.sections:
167
+ for element in section.element.iter():
168
+ for name in ("id", "instid", "instId"):
169
+ value = str(element.get(name) or "")
170
+ if value.isdigit():
171
+ maximum = max(maximum, int(value))
172
+ return maximum
173
+
174
+ def _identity(self) -> str:
175
+ self._identity_seed += 1
176
+ return str(self._identity_seed)
177
+
178
+ def _dependency(self, node: dict[str, Any], field: str) -> dict[str, Any] | None:
179
+ refs = list(node[field])
180
+ if not refs:
181
+ return None
182
+ if len(refs) != 1:
183
+ raise AgentContractError(
184
+ "invariant_violation", f"{node['kind']} requires at most one {field}", target=node["blueprintId"]
185
+ )
186
+ return self.dependency_targets[str(refs[0])]
187
+
188
+ def _apply_paragraph_dependencies(self, paragraph: Any, node: dict[str, Any]) -> None:
189
+ style = self._dependency(node, "styleRefs")
190
+ if style is not None:
191
+ paragraph.style_id_ref = style["identity"]["styleIDRef"]
192
+ paragraph.para_pr_id_ref = style["identity"]["paraPrIDRef"]
193
+ numbering = self._dependency(node, "numberingRefs")
194
+ if numbering is not None:
195
+ definition = self.document.paragraph_property(numbering["identity"]["paraPrIDRef"])
196
+ heading = definition.heading if definition is not None else None
197
+ if heading is None or not self.document._root.headers: # type: ignore[attr-defined]
198
+ raise AgentContractError("invariant_violation", "mapped numbering has no heading")
199
+ paragraph.para_pr_id_ref = self.document._root.headers[0].ensure_paragraph_format( # type: ignore[attr-defined]
200
+ base_para_pr_id=paragraph.para_pr_id_ref,
201
+ heading={"type": heading.type or "NONE", "idRef": heading.id_ref or 0, "level": heading.level or 0},
202
+ )
203
+
204
+ def _populate_run(self, run: Any, node: dict[str, Any]) -> None:
205
+ character = self._dependency(node, "styleRefs")
206
+ if character is not None:
207
+ run.char_pr_id_ref = character["identity"]["charPrIDRef"]
208
+ run.text = str(node["properties"].get("text") or "")
209
+ self.created[str(node["blueprintId"])] = CreatedBinding("run", run)
210
+
211
+ def _create_runs(self, paragraph: Any, child_ids: list[str], *, reuse_existing: bool) -> None:
212
+ run_ids = [child_id for child_id in child_ids if self.nodes[child_id]["kind"] == "run"]
213
+ existing = list(paragraph.runs) if reuse_existing else []
214
+ for index, run_id in enumerate(run_ids):
215
+ node = self.nodes[run_id]
216
+ if index < len(existing):
217
+ run = existing[index]
218
+ else:
219
+ character = self._dependency(node, "styleRefs")
220
+ char_ref = character["identity"]["charPrIDRef"] if character is not None else None
221
+ run = paragraph.add_run("", char_pr_id_ref=char_ref)
222
+ self._populate_run(run, node)
223
+ for run in existing[len(run_ids) :]:
224
+ # Newly created section/cell paragraphs may carry one structural
225
+ # control run. Preserve it only when it has non-text children.
226
+ if all(_local_name(child) in {"t", "tab"} for child in run.element):
227
+ paragraph.element.remove(run.element)
228
+
229
+ def _host_run(self, node_id: str, paragraph: Any) -> Any:
230
+ target_id = self.references.get((node_id, "hostRun"))
231
+ if target_id is not None:
232
+ try:
233
+ return self.created[target_id].native
234
+ except KeyError as exc:
235
+ raise AgentContractError("invariant_violation", "host run was not constructed", target=node_id) from exc
236
+ return paragraph.add_run("")
237
+
238
+ def _create_paragraph(
239
+ self,
240
+ node_id: str,
241
+ parent: Any,
242
+ *,
243
+ existing: Any | None = None,
244
+ root_view: HwpxAgentDocument | None = None,
245
+ root_parent: NodeRecord | None = None,
246
+ position: dict[str, Any] | None = None,
247
+ ) -> Any:
248
+ node = self.nodes[node_id]
249
+ if existing is not None:
250
+ paragraph = existing
251
+ paragraph.clear_text()
252
+ reuse_existing = True
253
+ elif hasattr(parent, "add_paragraph") and _local_name(parent.element) == "tc":
254
+ paragraph = parent.add_paragraph("")
255
+ paragraph.clear_text()
256
+ reuse_existing = True
257
+ else:
258
+ paragraph = parent.add_paragraph("", include_run=False, inherit_style=False)
259
+ reuse_existing = False
260
+ self._apply_paragraph_dependencies(paragraph, node)
261
+ if root_view is not None and root_parent is not None and position and position["mode"] != "append":
262
+ paragraph.element.getparent().remove(paragraph.element)
263
+ _insert_direct_child(root_view, root_parent, "paragraph", paragraph.element, position)
264
+ root_parent.native.mark_dirty()
265
+ self.created[node_id] = CreatedBinding("paragraph", paragraph)
266
+ child_ids = list(node["children"])
267
+ self._create_runs(paragraph, child_ids, reuse_existing=reuse_existing)
268
+ non_runs = [child_id for child_id in child_ids if self.nodes[child_id]["kind"] != "run"]
269
+ if not child_ids:
270
+ paragraph.text = str(node["properties"].get("text") or "")
271
+ for child_id in non_runs:
272
+ self._create_inline_or_table(child_id, paragraph)
273
+ return paragraph
274
+
275
+ def _create_table(self, node_id: str, paragraph: Any, host_run: Any) -> Any:
276
+ node = self.nodes[node_id]
277
+ properties = node["properties"]
278
+ table = paragraph.add_table(
279
+ int(properties["rowCount"]),
280
+ int(properties["columnCount"]),
281
+ width=_mm_units(properties.get("widthMm"), 7200 * int(properties["columnCount"])),
282
+ )
283
+ _move_to_host_run(paragraph, table, host_run)
284
+ if properties.get("caption") is not None:
285
+ _table_caption(table, str(properties["caption"] or ""))
286
+ pos = _direct_child(table.element, "pos")
287
+ if pos is not None and properties.get("alignment"):
288
+ pos.set("horzAlign", str(properties["alignment"]).upper())
289
+ self.created[node_id] = CreatedBinding("table", table)
290
+ row_ids = [child for child in node["children"] if self.nodes[child]["kind"] == "row"]
291
+ if len(row_ids) != len(table.rows):
292
+ raise AgentContractError("invariant_violation", "table row count does not match graph", target=node_id)
293
+ cell_layout = self._table_cell_layout(row_ids, int(properties["columnCount"]))
294
+ for cell_id, row_index, column_index, row_span, column_span in cell_layout:
295
+ if row_span > 1 or column_span > 1:
296
+ table.merge_cells(
297
+ row_index,
298
+ column_index,
299
+ row_index + row_span - 1,
300
+ column_index + column_span - 1,
301
+ )
302
+ for row_id, row in zip(row_ids, table.rows, strict=True):
303
+ self._bind_row(row_id, row)
304
+ for cell_id, row_index, column_index, _row_span, _column_span in cell_layout:
305
+ self._populate_cell(cell_id, table.cell(row_index, column_index))
306
+ return table
307
+
308
+ def _table_cell_layout(
309
+ self,
310
+ row_ids: list[str],
311
+ column_count: int,
312
+ ) -> list[tuple[str, int, int, int, int]]:
313
+ occupancy = [[False for _ in range(column_count)] for _ in row_ids]
314
+ layout: list[tuple[str, int, int, int, int]] = []
315
+ for row_index, row_id in enumerate(row_ids):
316
+ column_index = 0
317
+ cell_ids = [
318
+ child
319
+ for child in self.nodes[row_id]["children"]
320
+ if self.nodes[child]["kind"] == "cell"
321
+ ]
322
+ for cell_id in cell_ids:
323
+ while column_index < column_count and occupancy[row_index][column_index]:
324
+ column_index += 1
325
+ properties = self.nodes[cell_id]["properties"]
326
+ row_span = int(properties.get("rowSpan") or 1)
327
+ column_span = int(properties.get("columnSpan") or 1)
328
+ if (
329
+ column_index >= column_count
330
+ or row_index + row_span > len(row_ids)
331
+ or column_index + column_span > column_count
332
+ ):
333
+ raise AgentContractError(
334
+ "invariant_violation",
335
+ "table cell spans exceed the declared grid",
336
+ target=cell_id,
337
+ )
338
+ for occupied_row in range(row_index, row_index + row_span):
339
+ for occupied_column in range(column_index, column_index + column_span):
340
+ if occupancy[occupied_row][occupied_column]:
341
+ raise AgentContractError(
342
+ "invariant_violation",
343
+ "table cell spans overlap",
344
+ target=cell_id,
345
+ )
346
+ occupancy[occupied_row][occupied_column] = True
347
+ layout.append((cell_id, row_index, column_index, row_span, column_span))
348
+ column_index += column_span
349
+ if any(not occupied for row in occupancy for occupied in row):
350
+ raise AgentContractError(
351
+ "invariant_violation",
352
+ "table graph does not cover the declared grid",
353
+ )
354
+ return layout
355
+
356
+ def _bind_row(self, node_id: str, row: Any) -> None:
357
+ node = self.nodes[node_id]
358
+ self.created[node_id] = CreatedBinding("row", row)
359
+ height = node["properties"].get("heightMm")
360
+ if height is not None:
361
+ for cell in row.cells:
362
+ cell.set_size(height=_mm_units(height, cell.height))
363
+
364
+ def _populate_row(self, node_id: str, row: Any) -> None:
365
+ node = self.nodes[node_id]
366
+ self._bind_row(node_id, row)
367
+ cell_ids = [child for child in node["children"] if self.nodes[child]["kind"] == "cell"]
368
+ if len(cell_ids) != len(row.cells):
369
+ raise AgentContractError("invariant_violation", "table cell count does not match graph", target=node_id)
370
+ for cell_id, cell in zip(cell_ids, row.cells, strict=True):
371
+ self._populate_cell(cell_id, cell)
372
+
373
+ def _populate_cell(self, node_id: str, cell: Any) -> None:
374
+ node = self.nodes[node_id]
375
+ properties = node["properties"]
376
+ cell.set_span(int(properties.get("rowSpan") or 1), int(properties.get("columnSpan") or 1))
377
+ sublist = _direct_child(cell.element, "subList")
378
+ if sublist is not None and properties.get("verticalAlignment"):
379
+ sublist.set("vertAlign", str(properties["verticalAlignment"]).upper())
380
+ self.created[node_id] = CreatedBinding("cell", cell)
381
+ paragraph_ids = [child for child in node["children"] if self.nodes[child]["kind"] == "paragraph"]
382
+ existing = list(cell.paragraphs)
383
+ for index, paragraph_id in enumerate(paragraph_ids):
384
+ paragraph = existing[index] if index < len(existing) else None
385
+ self._create_paragraph(paragraph_id, cell, existing=paragraph)
386
+ for paragraph in existing[len(paragraph_ids) :]:
387
+ if sublist is not None:
388
+ sublist.remove(paragraph.element)
389
+ cell.table.mark_dirty()
390
+ if not paragraph_ids:
391
+ cell.text = str(properties.get("text") or "")
392
+
393
+ def _create_picture(self, node_id: str, paragraph: Any, host_run: Any) -> Any:
394
+ node = self.nodes[node_id]
395
+ resource = self._dependency(node, "resourceRefs")
396
+ if resource is None:
397
+ raise AgentContractError("invariant_violation", "picture has no mapped resource", target=node_id)
398
+ properties = node["properties"]
399
+ picture = paragraph.add_picture(
400
+ resource["identity"]["binaryItemIDRef"],
401
+ width=_mm_units(properties.get("widthMm"), 14400),
402
+ height=_mm_units(properties.get("heightMm"), 14400),
403
+ align=properties.get("alignment"),
404
+ treat_as_char=_bool(properties.get("treatAsChar"), True),
405
+ )
406
+ _move_to_host_run(paragraph, picture, host_run)
407
+ if properties.get("altText"):
408
+ picture.element.set("altText", str(properties["altText"]))
409
+ if properties.get("name"):
410
+ picture.element.set("name", str(properties["name"]))
411
+ self.created[node_id] = CreatedBinding("picture", picture)
412
+ return picture
413
+
414
+ def _create_shape(self, node_id: str, paragraph: Any, host_run: Any) -> Any:
415
+ node = self.nodes[node_id]
416
+ properties = node["properties"]
417
+ shape_type = str(properties.get("shapeType") or "")
418
+ kwargs = {
419
+ "width": _mm_units(properties.get("widthMm"), 14400),
420
+ "height": _mm_units(properties.get("heightMm"), 7200),
421
+ "line_color": str(properties.get("lineColor") or "#000000"),
422
+ "line_width": str(properties.get("lineWidth") or "283"),
423
+ "fill_color": properties.get("fillColor"),
424
+ "treat_as_char": True,
425
+ }
426
+ if shape_type == "rect":
427
+ shape = paragraph.add_rectangle(ratio=int(properties.get("ratio") or 0), **kwargs)
428
+ elif shape_type == "ellipse":
429
+ shape = paragraph.add_ellipse(**kwargs)
430
+ else:
431
+ raise AgentContractError("unsupported_content", "shape type is not replayable", target=node_id)
432
+ _move_to_host_run(paragraph, shape, host_run)
433
+ if properties.get("altText"):
434
+ shape.element.set("altText", str(properties["altText"]))
435
+ if properties.get("name"):
436
+ shape.element.set("name", str(properties["name"]))
437
+ self.created[node_id] = CreatedBinding("shape", shape)
438
+ return shape
439
+
440
+ def _create_note(self, node_id: str, paragraph: Any, host_run: Any) -> Any:
441
+ node = self.nodes[node_id]
442
+ text = str(node["properties"].get("text") or "")
443
+ note = paragraph.add_footnote(text) if node["kind"] == "footnote" else paragraph.add_endnote(text)
444
+ _move_to_host_run(paragraph, note, host_run)
445
+ self.created[node_id] = CreatedBinding(str(node["kind"]), note)
446
+ return note
447
+
448
+ def _create_form_field(self, node_id: str, paragraph: Any) -> None:
449
+ node = self.nodes[node_id]
450
+ properties = node["properties"]
451
+ field_type = str(properties.get("fieldType") or "ClickHere")
452
+ normalized_type = field_type.replace("_", "").replace("-", "").upper()
453
+ if normalized_type not in {"FORM", "CLICKHERE", "NURUMTUL", "누름틀"}:
454
+ raise AgentContractError("unsupported_content", "only FORM fields are replayable", target=node_id)
455
+ host_id = self.references.get((node_id, "hostRun"))
456
+ end_id = self.references.get((node_id, "endRun"))
457
+ if host_id is None or end_id is None:
458
+ raise AgentContractError("invariant_violation", "form field run references are incomplete", target=node_id)
459
+ host_run = self.created[host_id].native
460
+ end_run = self.created[end_id].native
461
+ begin_id = self._identity()
462
+ field_id = self._identity()
463
+ direction = str(properties.get("value") or "")[:4096]
464
+ command_payload = (
465
+ f"Direction:wstring:{len(direction)}:{direction} HelpState:wstring:0: "
466
+ )
467
+ ctrl = host_run.element.makeelement(f"{HP}ctrl", {})
468
+ begin = ctrl.makeelement(
469
+ f"{HP}fieldBegin",
470
+ {
471
+ "id": begin_id,
472
+ "fieldid": field_id,
473
+ "name": str(properties.get("name") or "")[:512],
474
+ "type": "CLICK_HERE" if normalized_type in {"FORM", "CLICKHERE"} else field_type,
475
+ "editable": "0" if properties.get("readOnly") else "1",
476
+ "dirty": "0",
477
+ "zorder": "-1",
478
+ "metaTag": "",
479
+ },
480
+ )
481
+ parameters = begin.makeelement(f"{HP}parameters", {"cnt": "3", "name": ""})
482
+ prop = parameters.makeelement(f"{HP}integerParam", {"name": "Prop"})
483
+ prop.text = "9"
484
+ command = parameters.makeelement(f"{HP}stringParam", {"name": "Command"})
485
+ command.text = f"Clickhere:set:{max(0, len(command_payload) - 1)}:{command_payload}"
486
+ direction_param = parameters.makeelement(f"{HP}stringParam", {"name": "Direction"})
487
+ direction_param.text = direction
488
+ parameters.extend((prop, command, direction_param))
489
+ begin.append(parameters)
490
+ ctrl.append(begin)
491
+ _remove_empty_text_placeholders(host_run)
492
+ host_run.element.append(ctrl)
493
+ end_ctrl = end_run.element.makeelement(f"{HP}ctrl", {"type": "FORM"})
494
+ end_ctrl.append(
495
+ end_ctrl.makeelement(
496
+ f"{HP}fieldEnd", {"beginIDRef": begin_id, "fieldid": field_id}
497
+ )
498
+ )
499
+ _remove_empty_text_placeholders(end_run)
500
+ end_run.element.append(end_ctrl)
501
+ paragraph.section.mark_dirty()
502
+ self.created[node_id] = CreatedBinding("form-field", None, begin_id)
503
+
504
+ def _create_inline_or_table(self, node_id: str, paragraph: Any) -> Any:
505
+ node = self.nodes[node_id]
506
+ kind = str(node["kind"])
507
+ if kind == "form-field":
508
+ self._create_form_field(node_id, paragraph)
509
+ return None
510
+ host_run = self._host_run(node_id, paragraph)
511
+ if kind == "table":
512
+ return self._create_table(node_id, paragraph, host_run)
513
+ if kind == "picture":
514
+ return self._create_picture(node_id, paragraph, host_run)
515
+ if kind == "shape":
516
+ return self._create_shape(node_id, paragraph, host_run)
517
+ if kind in {"footnote", "endnote"}:
518
+ return self._create_note(node_id, paragraph, host_run)
519
+ raise AgentContractError("unsupported_content", f"unsupported paragraph child: {kind}", target=node_id)
520
+
521
+ def _create_memo(self, node_id: str, section: Any) -> Any:
522
+ node = self.nodes[node_id]
523
+ properties = node["properties"]
524
+ attrs = {"author": str(properties["author"])} if properties.get("author") else None
525
+ memo = section.add_memo(str(properties.get("text") or ""), attributes=attrs)
526
+ self.created[node_id] = CreatedBinding("memo", memo)
527
+ return memo
528
+
529
+ def _create_section(self, node_id: str) -> Any:
530
+ node = self.nodes[node_id]
531
+ section = self.document.add_section()
532
+ properties = node["properties"]
533
+ size: dict[str, int] = {}
534
+ if properties.get("pageWidthMm") is not None:
535
+ size["width"] = _mm_units(properties["pageWidthMm"], 59528)
536
+ if properties.get("pageHeightMm") is not None:
537
+ size["height"] = _mm_units(properties["pageHeightMm"], 84189)
538
+ if size:
539
+ section.properties.set_page_size(width=size.get("width"), height=size.get("height"))
540
+ self.created[node_id] = CreatedBinding("section", section)
541
+ paragraph_ids = [child for child in node["children"] if self.nodes[child]["kind"] == "paragraph"]
542
+ existing = list(section.paragraphs)
543
+ for index, paragraph_id in enumerate(paragraph_ids):
544
+ paragraph = existing[index] if index < len(existing) else None
545
+ self._create_paragraph(paragraph_id, section, existing=paragraph)
546
+ for child_id in node["children"]:
547
+ if self.nodes[child_id]["kind"] == "memo":
548
+ self._create_memo(child_id, section)
549
+ return section
550
+
551
+ def create_root(
552
+ self,
553
+ parent: NodeRecord,
554
+ position: dict[str, Any],
555
+ view: HwpxAgentDocument,
556
+ ) -> dict[str, CreatedBinding]:
557
+ root_id = str(self.manifest["root"]["blueprintId"])
558
+ root = self.nodes[root_id]
559
+ kind = str(root["kind"])
560
+ if kind == "document":
561
+ self.created[root_id] = CreatedBinding("document", self.document)
562
+ for child_id in root["children"]:
563
+ self._create_section(child_id)
564
+ elif kind == "section":
565
+ if position["mode"] != "append":
566
+ raise AgentContractError("unsupported_operation", "section replay is append-only")
567
+ self._create_section(root_id)
568
+ elif kind == "paragraph":
569
+ self._create_paragraph(
570
+ root_id,
571
+ parent.native,
572
+ root_view=view,
573
+ root_parent=parent,
574
+ position=position,
575
+ )
576
+ elif kind == "run":
577
+ character = self._dependency(root, "styleRefs")
578
+ char_ref = character["identity"]["charPrIDRef"] if character is not None else None
579
+ run = parent.native.add_run(str(root["properties"].get("text") or ""), char_pr_id_ref=char_ref)
580
+ if position["mode"] != "append":
581
+ parent.native.element.remove(run.element)
582
+ _insert_direct_child(view, parent, "run", run.element, position)
583
+ self.created[root_id] = CreatedBinding("run", run)
584
+ elif kind in _INLINE_KINDS:
585
+ native = self._create_inline_or_table(root_id, parent.native)
586
+ if position["mode"] != "append":
587
+ _remove_inline_element(native.element, parent.native)
588
+ _insert_inline(view, parent, kind, native.element, position)
589
+ elif kind == "memo":
590
+ if position["mode"] != "append":
591
+ raise AgentContractError("unsupported_operation", "memo replay is append-only")
592
+ self._create_memo(root_id, parent.native)
593
+ elif kind == "row":
594
+ properties = root["properties"]
595
+ created_element = _add(
596
+ self.document,
597
+ view,
598
+ parent,
599
+ "row",
600
+ {"cellCount": int(properties["cellCount"]), "heightMm": properties.get("heightMm")},
601
+ position,
602
+ )
603
+ row = next(
604
+ (candidate for candidate in parent.native.rows if candidate.element is created_element),
605
+ None,
606
+ )
607
+ if row is None:
608
+ raise AgentContractError("not_found", "replayed row was not materialized", target=root_id)
609
+ self._populate_row(root_id, row)
610
+ else:
611
+ raise AgentContractError("unsupported_content", f"root kind is not replayable: {kind}")
612
+ return self.created
613
+
614
+
615
+ __all__ = [
616
+ "CreatedBinding",
617
+ "TypedNativeBridge",
618
+ "create_character_format",
619
+ "create_numbering",
620
+ "create_paragraph_style",
621
+ ]