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,283 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Leaf helpers for table border-fill resolution, matching, and construction.
4
+
5
+ Private to the ops_services package: imported only by ``tables.py``. This module
6
+ holds no service class and depends solely on the standard library and the typed
7
+ ``upstream`` seam, so it can never participate in an import cycle.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import NamedTuple, Optional
14
+ from xml.etree import ElementTree as ET
15
+
16
+ from ..upstream import HH_NS, HwpxDocument
17
+
18
+
19
+ class _BorderFillSpec(NamedTuple):
20
+ border_type: str
21
+ width_value: str
22
+ diagonal_width_value: str
23
+ width_token: str
24
+ diagonal_width_token: str
25
+ edge_color: Optional[str]
26
+ diagonal_color: Optional[str]
27
+ fill_color: Optional[str]
28
+
29
+
30
+ def _normalize_border_length(value: Optional[str | float | int], default: str) -> str:
31
+ if value is None:
32
+ return default
33
+ if isinstance(value, (int, float)):
34
+ return f"{value:g} mm"
35
+ text = str(value).strip()
36
+ if not text:
37
+ return default
38
+ match = re.fullmatch(r"([0-9]+(?:\\.[0-9]+)?)\\s*([A-Za-z]+)?", text)
39
+ if match:
40
+ number, unit = match.groups()
41
+ unit = (unit or "mm").lower()
42
+ return f"{number} {unit}"
43
+ return text
44
+
45
+
46
+ def _normalize_length_token(value: Optional[str]) -> str:
47
+ if not value:
48
+ return ""
49
+ return re.sub(r"\s+", "", str(value)).lower()
50
+
51
+
52
+ def _resolve_border_fill_spec(
53
+ normalized_style: Optional[str],
54
+ normalized_border_color: Optional[str],
55
+ normalized_fill_color: Optional[str],
56
+ border_width: Optional[str | float | int],
57
+ ) -> _BorderFillSpec:
58
+ border_type = "NONE" if normalized_style == "none" else "SOLID"
59
+ if border_type == "NONE":
60
+ width_default = "0 mm"
61
+ diag_default = "0 mm"
62
+ else:
63
+ width_default = "0.12 mm"
64
+ diag_default = "0.1 mm"
65
+
66
+ width_value = _normalize_border_length(border_width, width_default)
67
+ if border_width is not None:
68
+ diagonal_width_value = _normalize_border_length(border_width, width_default)
69
+ else:
70
+ diagonal_width_value = _normalize_border_length(None, diag_default)
71
+ width_token = _normalize_length_token(width_value)
72
+ diagonal_width_token = _normalize_length_token(diagonal_width_value)
73
+
74
+ edge_color: Optional[str] = normalized_border_color
75
+ diagonal_color: Optional[str] = normalized_border_color
76
+ if border_type == "SOLID":
77
+ edge_color = normalized_border_color or "#000000"
78
+ diagonal_color = edge_color
79
+ # Non-solid borders retain the optional normalized color.
80
+ return _BorderFillSpec(
81
+ border_type=border_type,
82
+ width_value=width_value,
83
+ diagonal_width_value=diagonal_width_value,
84
+ width_token=width_token,
85
+ diagonal_width_token=diagonal_width_token,
86
+ edge_color=edge_color,
87
+ diagonal_color=diagonal_color,
88
+ fill_color=normalized_fill_color,
89
+ )
90
+
91
+
92
+ def _shortcut_border_fill_id(
93
+ document: HwpxDocument,
94
+ normalized_style: Optional[str],
95
+ normalized_border_color: Optional[str],
96
+ normalized_fill_color: Optional[str],
97
+ border_width: Optional[str | float | int],
98
+ ) -> Optional[str]:
99
+ if normalized_style == "none" and not any(
100
+ [normalized_border_color, normalized_fill_color, border_width]
101
+ ):
102
+ return "0"
103
+
104
+ if (
105
+ normalized_style in {None, "solid"}
106
+ and normalized_border_color is None
107
+ and normalized_fill_color is None
108
+ and border_width is None
109
+ ):
110
+ return document.oxml.ensure_basic_border_fill()
111
+
112
+ return None
113
+
114
+
115
+ def _border_fill_flags_match(existing: ET.Element) -> bool:
116
+ if (existing.get("threeD") or "0") != "0":
117
+ return False
118
+ if (existing.get("shadow") or "0") != "0":
119
+ return False
120
+ if (existing.get("centerLine") or "NONE").upper() != "NONE":
121
+ return False
122
+ if (existing.get("breakCellSeparateLine") or "0") != "0":
123
+ return False
124
+ return True
125
+
126
+
127
+ def _border_fill_slashes_match(existing: ET.Element) -> bool:
128
+ for slash_name in ("slash", "backSlash"):
129
+ slash = existing.find(f"{HH_NS}{slash_name}")
130
+ if slash is None:
131
+ return False
132
+ if (slash.get("type") or "NONE").upper() != "NONE":
133
+ return False
134
+ if slash.get("Crooked", "0") != "0":
135
+ return False
136
+ if slash.get("isCounter", "0") != "0":
137
+ return False
138
+ return True
139
+
140
+
141
+ def _border_fill_edges_match(
142
+ existing: ET.Element,
143
+ border_type: str,
144
+ width_token: str,
145
+ edge_color: Optional[str],
146
+ ) -> bool:
147
+ for child_name in ("leftBorder", "rightBorder", "topBorder", "bottomBorder"):
148
+ border_child = existing.find(f"{HH_NS}{child_name}")
149
+ if border_child is None:
150
+ return False
151
+ if (border_child.get("type") or "").upper() != border_type:
152
+ return False
153
+ if _normalize_length_token(border_child.get("width")) != width_token:
154
+ return False
155
+ if edge_color is not None:
156
+ if (border_child.get("color") or "").upper() != edge_color:
157
+ return False
158
+ else:
159
+ if border_child.get("color") not in (None, ""):
160
+ return False
161
+ return True
162
+
163
+
164
+ def _border_fill_diagonal_matches(
165
+ existing: ET.Element,
166
+ border_type: str,
167
+ diagonal_width_token: str,
168
+ diagonal_color: Optional[str],
169
+ ) -> bool:
170
+ diagonal_child = existing.find(f"{HH_NS}diagonal")
171
+ if diagonal_child is None:
172
+ return False
173
+ expected_diagonal_type = "SOLID" if border_type == "SOLID" else "NONE"
174
+ if (diagonal_child.get("type") or "").upper() != expected_diagonal_type:
175
+ return False
176
+ if _normalize_length_token(diagonal_child.get("width")) != diagonal_width_token:
177
+ return False
178
+ if diagonal_color is not None:
179
+ if (diagonal_child.get("color") or "").upper() != diagonal_color:
180
+ return False
181
+ else:
182
+ if diagonal_child.get("color") not in (None, ""):
183
+ return False
184
+ return True
185
+
186
+
187
+ def _border_fill_fill_matches(
188
+ existing: ET.Element, normalized_fill_color: Optional[str]
189
+ ) -> bool:
190
+ fill_brush = existing.find(f"{HH_NS}fillBrush")
191
+ if normalized_fill_color is None:
192
+ if fill_brush is not None:
193
+ return False
194
+ else:
195
+ if fill_brush is None:
196
+ return False
197
+ solid_brush = fill_brush.find(f"{HH_NS}solidBrush")
198
+ if solid_brush is None:
199
+ return False
200
+ if (solid_brush.get("type") or "SOLID").upper() != "SOLID":
201
+ return False
202
+ if (solid_brush.get("color") or "").upper() != normalized_fill_color:
203
+ return False
204
+ return True
205
+
206
+
207
+ def _border_fill_matches(existing: ET.Element, spec: _BorderFillSpec) -> bool:
208
+ return (
209
+ _border_fill_flags_match(existing)
210
+ and _border_fill_slashes_match(existing)
211
+ and _border_fill_edges_match(
212
+ existing, spec.border_type, spec.width_token, spec.edge_color
213
+ )
214
+ and _border_fill_diagonal_matches(
215
+ existing, spec.border_type, spec.diagonal_width_token, spec.diagonal_color
216
+ )
217
+ and _border_fill_fill_matches(existing, spec.fill_color)
218
+ )
219
+
220
+
221
+ def _find_matching_border_fill(
222
+ border_fills_element: ET.Element, spec: _BorderFillSpec
223
+ ) -> Optional[str]:
224
+ for candidate in border_fills_element.findall(f"{HH_NS}borderFill"):
225
+ identifier = candidate.get("id")
226
+ if not identifier:
227
+ continue
228
+ if _border_fill_matches(candidate, spec):
229
+ return identifier
230
+ return None
231
+
232
+
233
+ def _build_border_fill_element(
234
+ border_fills_element: ET.Element, new_id: str, spec: _BorderFillSpec
235
+ ) -> None:
236
+ border_fill_element = ET.SubElement(
237
+ border_fills_element,
238
+ f"{HH_NS}borderFill",
239
+ {
240
+ "id": new_id,
241
+ "threeD": "0",
242
+ "shadow": "0",
243
+ "centerLine": "NONE",
244
+ "breakCellSeparateLine": "0",
245
+ },
246
+ )
247
+
248
+ for slash_name in ("slash", "backSlash"):
249
+ ET.SubElement(
250
+ border_fill_element,
251
+ f"{HH_NS}{slash_name}",
252
+ {"type": "NONE", "Crooked": "0", "isCounter": "0"},
253
+ )
254
+
255
+ def append_border(
256
+ name: str, *, width: str, color: Optional[str], kind: str
257
+ ) -> None:
258
+ attrs = {"type": kind}
259
+ if width:
260
+ attrs["width"] = width
261
+ if color is not None:
262
+ attrs["color"] = color
263
+ ET.SubElement(border_fill_element, f"{HH_NS}{name}", attrs)
264
+
265
+ for side in ("leftBorder", "rightBorder", "topBorder", "bottomBorder"):
266
+ append_border(
267
+ side, width=spec.width_value, color=spec.edge_color, kind=spec.border_type
268
+ )
269
+
270
+ append_border(
271
+ "diagonal",
272
+ width=spec.diagonal_width_value,
273
+ color=spec.diagonal_color,
274
+ kind="SOLID" if spec.border_type == "SOLID" else "NONE",
275
+ )
276
+
277
+ if spec.fill_color is not None:
278
+ fill_brush = ET.SubElement(border_fill_element, f"{HH_NS}fillBrush")
279
+ ET.SubElement(
280
+ fill_brush,
281
+ f"{HH_NS}solidBrush",
282
+ {"type": "SOLID", "color": spec.fill_color, "alpha": "255"},
283
+ )
@@ -0,0 +1,55 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Explicit composition root for bounded ``HwpxOps`` services."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from .content_layout import ContentLayoutService
9
+ from .context import DocumentContext
10
+ from .form_fields import FormFieldService
11
+ from .media import MediaService
12
+ from .memo_style import MemoStyleService
13
+ from .package_validation import PackageValidationService
14
+ from .planning import PlanningService
15
+ from .preview_export import PreviewExportService
16
+ from .read_query import ReadQueryService
17
+ from .save_policy import SavePolicy
18
+ from .tables import TableService
19
+ from .transactions import TransactionService
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class HwpxOpsServices:
24
+ context: DocumentContext
25
+ save: SavePolicy
26
+ transactions: TransactionService
27
+ read_query: ReadQueryService
28
+ content_layout: ContentLayoutService
29
+ tables: TableService
30
+ form_fields: FormFieldService
31
+ media: MediaService
32
+ memo_style: MemoStyleService
33
+ preview_export: PreviewExportService
34
+ package_validation: PackageValidationService
35
+ planning: PlanningService
36
+
37
+
38
+ def build_hwpx_ops_services(context: DocumentContext) -> HwpxOpsServices:
39
+ save = SavePolicy(context)
40
+ transactions = TransactionService(context, save)
41
+ memo_style = MemoStyleService(context, save)
42
+ return HwpxOpsServices(
43
+ context=context,
44
+ save=save,
45
+ transactions=transactions,
46
+ read_query=ReadQueryService(context),
47
+ content_layout=ContentLayoutService(context, save, transactions, memo_style),
48
+ tables=TableService(context, save, memo_style),
49
+ form_fields=FormFieldService(context, save, transactions),
50
+ media=MediaService(context, save),
51
+ memo_style=memo_style,
52
+ preview_export=PreviewExportService(context),
53
+ package_validation=PackageValidationService(context),
54
+ planning=PlanningService(context),
55
+ )
@@ -0,0 +1,322 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Dict, Optional, Sequence
7
+
8
+ from ..upstream import (
9
+ HP_NS,
10
+ )
11
+
12
+ from .context import DocumentContext
13
+ from .save_policy import SavePolicy
14
+ from .transactions import TransactionService
15
+ from .memo_style import MemoStyleService
16
+
17
+ logger = logging.getLogger("hwpx_automation.hwpx_ops")
18
+
19
+
20
+ class ContentLayoutService:
21
+ def __init__(
22
+ self,
23
+ context: DocumentContext,
24
+ save: SavePolicy,
25
+ transactions: TransactionService,
26
+ styles: MemoStyleService,
27
+ ) -> None:
28
+ self._context = context
29
+ self._save = save
30
+ self._transactions = transactions
31
+ self._styles = styles
32
+
33
+ def replace_text_in_runs(
34
+ self,
35
+ path: str,
36
+ search: str,
37
+ replacement: str,
38
+ *,
39
+ style_filter: Optional[Dict[str, Any]] = None,
40
+ limit_per_run: Optional[int] = None,
41
+ dry_run: bool = False,
42
+ ) -> Dict[str, Any]:
43
+ document, resolved = self._context._open_document(path)
44
+ filter_args: Dict[str, Any] = {}
45
+ if style_filter:
46
+ if "colorHex" in style_filter and style_filter["colorHex"]:
47
+ filter_args["text_color"] = self._styles._normalize_color(
48
+ style_filter["colorHex"]
49
+ )
50
+ if "underline" in style_filter:
51
+ filter_args["underline_type"] = (
52
+ "SOLID" if style_filter["underline"] else "NONE"
53
+ )
54
+ if "charPrIDRef" in style_filter and style_filter["charPrIDRef"]:
55
+ filter_args["char_pr_id_ref"] = style_filter["charPrIDRef"]
56
+ replaced = document.replace_text_in_runs(
57
+ search,
58
+ replacement,
59
+ limit=limit_per_run,
60
+ **filter_args,
61
+ )
62
+ if not dry_run and replaced:
63
+ self._save._save_document(document, resolved)
64
+ return {"replacedCount": replaced}
65
+
66
+ def add_paragraph(
67
+ self,
68
+ path: str,
69
+ text: str = "",
70
+ *,
71
+ section_index: Optional[int] = None,
72
+ run_style: Optional[Dict[str, Any]] = None,
73
+ ) -> Dict[str, Any]:
74
+ document, resolved = self._context._open_document(path)
75
+ char_id = self._styles._ensure_char_style(document, run_style)
76
+ paragraph = document.add_paragraph(
77
+ text,
78
+ section_index=section_index,
79
+ char_pr_id_ref=char_id,
80
+ )
81
+ paragraphs = self._context._iter_paragraphs(document)
82
+ index = len(paragraphs) - 1
83
+ element_id = id(paragraph.element)
84
+ for idx, candidate in enumerate(paragraphs):
85
+ if id(candidate.element) == element_id:
86
+ index = idx
87
+ break
88
+ self._save._save_document(document, resolved)
89
+ return {"paragraphIndex": index}
90
+
91
+ def insert_paragraphs_bulk(
92
+ self,
93
+ path: str,
94
+ paragraphs: Sequence[str],
95
+ *,
96
+ section_index: Optional[int] = None,
97
+ run_style: Optional[Dict[str, Any]] = None,
98
+ dry_run: bool = False,
99
+ ) -> Dict[str, Any]:
100
+ if not paragraphs:
101
+ return {"added": 0}
102
+
103
+ if dry_run:
104
+ return {"added": len(paragraphs)}
105
+
106
+ document, resolved = self._context._open_document(path)
107
+ char_id = self._styles._ensure_char_style(document, run_style)
108
+ count = 0
109
+ for text in paragraphs:
110
+ document.add_paragraph(
111
+ text,
112
+ section_index=section_index,
113
+ char_pr_id_ref=char_id,
114
+ )
115
+ count += 1
116
+ self._save._save_document(document, resolved)
117
+ return {"added": count}
118
+
119
+ def set_paragraph_format(
120
+ self,
121
+ path: str,
122
+ *,
123
+ paragraph_index: Optional[int] = None,
124
+ paragraph_indexes: Optional[Sequence[int]] = None,
125
+ alignment: Optional[str] = None,
126
+ line_spacing_percent: Optional[float] = None,
127
+ indent_left_mm: Optional[float] = None,
128
+ indent_right_mm: Optional[float] = None,
129
+ first_line_indent_mm: Optional[float] = None,
130
+ spacing_before_pt: Optional[float] = None,
131
+ spacing_after_pt: Optional[float] = None,
132
+ outline_level: Optional[int] = None,
133
+ keep_with_next: Optional[bool] = None,
134
+ keep_lines: Optional[bool] = None,
135
+ page_break_before: Optional[bool] = None,
136
+ dry_run: bool = False,
137
+ ) -> Dict[str, Any]:
138
+ document, resolved = self._context._open_document(path)
139
+ result = document.set_paragraph_format(
140
+ paragraph_index=paragraph_index,
141
+ paragraph_indexes=paragraph_indexes,
142
+ alignment=alignment,
143
+ line_spacing_percent=line_spacing_percent,
144
+ indent_left_mm=indent_left_mm,
145
+ indent_right_mm=indent_right_mm,
146
+ first_line_indent_mm=first_line_indent_mm,
147
+ spacing_before_pt=spacing_before_pt,
148
+ spacing_after_pt=spacing_after_pt,
149
+ outline_level=outline_level,
150
+ keep_with_next=keep_with_next,
151
+ keep_lines=keep_lines,
152
+ page_break_before=page_break_before,
153
+ )
154
+ result.update({"ok": True, "filename": path})
155
+ return self._transactions._with_transaction_verification(
156
+ result, document, resolved, dry_run=dry_run
157
+ )
158
+
159
+ def set_page_setup(
160
+ self,
161
+ path: str,
162
+ *,
163
+ paper_size: Optional[str] = None,
164
+ width_mm: Optional[float] = None,
165
+ height_mm: Optional[float] = None,
166
+ orientation: Optional[str] = None,
167
+ margins_mm: Optional[Dict[str, float]] = None,
168
+ margin_left_mm: Optional[float] = None,
169
+ margin_right_mm: Optional[float] = None,
170
+ margin_top_mm: Optional[float] = None,
171
+ margin_bottom_mm: Optional[float] = None,
172
+ header_margin_mm: Optional[float] = None,
173
+ footer_margin_mm: Optional[float] = None,
174
+ gutter_mm: Optional[float] = None,
175
+ columns: Optional[int] = None,
176
+ column_gap_mm: Optional[float] = None,
177
+ section_index: Optional[int] = None,
178
+ dry_run: bool = False,
179
+ ) -> Dict[str, Any]:
180
+ document, resolved = self._context._open_document(path)
181
+ result = document.set_page_setup(
182
+ paper_size=paper_size,
183
+ width_mm=width_mm,
184
+ height_mm=height_mm,
185
+ orientation=orientation,
186
+ margins_mm=margins_mm,
187
+ margin_left_mm=margin_left_mm,
188
+ margin_right_mm=margin_right_mm,
189
+ margin_top_mm=margin_top_mm,
190
+ margin_bottom_mm=margin_bottom_mm,
191
+ header_margin_mm=header_margin_mm,
192
+ footer_margin_mm=footer_margin_mm,
193
+ gutter_mm=gutter_mm,
194
+ columns=columns,
195
+ column_gap_mm=column_gap_mm,
196
+ section_index=section_index,
197
+ )
198
+ result.update({"ok": True, "filename": path})
199
+ return self._transactions._with_transaction_verification(
200
+ result, document, resolved, dry_run=dry_run
201
+ )
202
+
203
+ def _header_footer_payload(
204
+ self,
205
+ wrapper: Any,
206
+ *,
207
+ kind: str,
208
+ page_type: str,
209
+ ) -> Dict[str, Any]:
210
+ element = getattr(wrapper, "element", None)
211
+ page_number_count = 0
212
+ if element is not None and hasattr(element, "iter"):
213
+ page_number_count = sum(1 for _ in element.iter(f"{HP_NS}pageNum"))
214
+ return {
215
+ "kind": kind,
216
+ "pageType": page_type,
217
+ "id": getattr(wrapper, "id", None),
218
+ "text": getattr(wrapper, "text", ""),
219
+ "pageNumberCount": page_number_count,
220
+ }
221
+
222
+ def set_header_footer(
223
+ self,
224
+ path: str,
225
+ *,
226
+ kind: str,
227
+ text: Optional[str] = None,
228
+ content: Optional[Sequence[Dict[str, Any]]] = None,
229
+ section_index: Optional[int] = None,
230
+ page_type: str = "BOTH",
231
+ dry_run: bool = False,
232
+ ) -> Dict[str, Any]:
233
+ document, resolved = self._context._open_document(path)
234
+ wrapper = document.set_header_footer(
235
+ kind=kind,
236
+ text=text,
237
+ content=content,
238
+ section_index=section_index,
239
+ page_type=page_type,
240
+ )
241
+ result = {
242
+ "ok": True,
243
+ "filename": path,
244
+ "headerFooter": self._header_footer_payload(
245
+ wrapper,
246
+ kind=kind,
247
+ page_type=page_type,
248
+ ),
249
+ }
250
+ return self._transactions._with_transaction_verification(
251
+ result, document, resolved, dry_run=dry_run
252
+ )
253
+
254
+ def set_page_number(
255
+ self,
256
+ path: str,
257
+ *,
258
+ target: str = "footer",
259
+ page_type: str = "BOTH",
260
+ format: str = "page",
261
+ align: str = "CENTER",
262
+ position: str = "BOTTOM_CENTER",
263
+ prefix: str = "",
264
+ suffix: str = "",
265
+ format_type: Optional[str] = None,
266
+ section_index: Optional[int] = None,
267
+ dry_run: bool = False,
268
+ ) -> Dict[str, Any]:
269
+ document, resolved = self._context._open_document(path)
270
+ wrapper = document.set_page_number(
271
+ target=target,
272
+ page_type=page_type,
273
+ format=format,
274
+ align=align,
275
+ position=position,
276
+ prefix=prefix,
277
+ suffix=suffix,
278
+ format_type=format_type,
279
+ section_index=section_index,
280
+ )
281
+ result = {
282
+ "ok": True,
283
+ "filename": path,
284
+ "target": target,
285
+ "format": format,
286
+ "headerFooter": self._header_footer_payload(
287
+ wrapper,
288
+ kind=target,
289
+ page_type=page_type,
290
+ ),
291
+ }
292
+ return self._transactions._with_transaction_verification(
293
+ result, document, resolved, dry_run=dry_run
294
+ )
295
+
296
+ def set_list_format(
297
+ self,
298
+ path: str,
299
+ *,
300
+ paragraph_index: Optional[int] = None,
301
+ paragraph_indexes: Optional[Sequence[int]] = None,
302
+ kind: str = "bullet",
303
+ level: int = 1,
304
+ bullet_char: Optional[str] = None,
305
+ number_format: Optional[str] = None,
306
+ start: Optional[int] = None,
307
+ dry_run: bool = False,
308
+ ) -> Dict[str, Any]:
309
+ document, resolved = self._context._open_document(path)
310
+ result = document.set_list_format(
311
+ paragraph_index=paragraph_index,
312
+ paragraph_indexes=paragraph_indexes,
313
+ kind=kind,
314
+ level=level,
315
+ bullet_char=bullet_char,
316
+ number_format=number_format,
317
+ start=start,
318
+ )
319
+ result.update({"ok": True, "filename": path})
320
+ return self._transactions._with_transaction_verification(
321
+ result, document, resolved, dry_run=dry_run
322
+ )