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,539 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import math
7
+ import re as _re
8
+ from typing import Any, Dict, List, Optional, Sequence, TypedDict
9
+ from xml.etree import ElementTree as ET
10
+
11
+ from ..upstream import (
12
+ HH_NS,
13
+ HP_NS,
14
+ HwpxDocument,
15
+ HwpxOxmlTable,
16
+ default_cell_width,
17
+ )
18
+
19
+ from ._border_fill import (
20
+ _build_border_fill_element,
21
+ _find_matching_border_fill,
22
+ _resolve_border_fill_spec,
23
+ _shortcut_border_fill_id,
24
+ )
25
+ from .context import DocumentContext
26
+ from .save_policy import SavePolicy
27
+ from .memo_style import MemoStyleService
28
+
29
+ logger = logging.getLogger("hwpx_automation.hwpx_ops")
30
+
31
+
32
+ class _CellAddressingKwargs(TypedDict, total=False):
33
+ """Optional addressing flags forwarded to ``HwpxOxmlTable.set_cell_text``.
34
+
35
+ Restricting the mapping to these two keys keeps ``**kwargs`` from being read
36
+ as also supplying ``fit`` (a ``FitPolicy | None`` parameter), which a bare
37
+ ``Dict[str, bool]`` would imply.
38
+ """
39
+
40
+ logical: bool
41
+ split_merged: bool
42
+
43
+
44
+ _CELL_TEXT_ILLEGAL = _re.compile(r"[\x00-\x08\x09\x0b\x0c\x0d\x0e-\x1f\ufffe\uffff]")
45
+
46
+
47
+ def _sanitize_cell_text(value: str) -> str:
48
+ """Remove characters illegal inside HWPML <hp:t> nodes.
49
+
50
+ Tab (U+0009) is stripped - it must live in a separate cell column,
51
+ not be concatenated with the text. \r is stripped; \n is kept.
52
+ Logs a warning when anything is actually removed.
53
+ """
54
+ cleaned = _CELL_TEXT_ILLEGAL.sub("", value)
55
+ if cleaned != value:
56
+ logger.warning(
57
+ "cell text contained illegal characters and was sanitised",
58
+ extra={"original_len": len(value), "cleaned_len": len(cleaned)},
59
+ )
60
+ return cleaned
61
+
62
+
63
+ _DEFAULT_CELL_WIDTH = default_cell_width()
64
+
65
+ _AUTO_FIT_CHAR_UNIT = max(360, _DEFAULT_CELL_WIDTH // 10)
66
+ _AUTO_FIT_PADDING_CHARS = 2
67
+ _AUTO_FIT_MIN_COLUMN_WIDTH = max(
68
+ _AUTO_FIT_CHAR_UNIT * (_AUTO_FIT_PADDING_CHARS + 1), _DEFAULT_CELL_WIDTH // 2
69
+ )
70
+ _AUTO_FIT_MAX_COLUMN_WIDTH = _DEFAULT_CELL_WIDTH * 12
71
+
72
+
73
+ DEFAULT_PAGING_PARAGRAPH_LIMIT = 200
74
+
75
+
76
+ class TableService:
77
+ def __init__(
78
+ self, context: DocumentContext, save: SavePolicy, styles: MemoStyleService
79
+ ) -> None:
80
+ self._context = context
81
+ self._save = save
82
+ self._styles = styles
83
+
84
+ def _auto_fit_table_columns(self, table: HwpxOxmlTable) -> List[int]:
85
+ column_count = table.column_count
86
+ if column_count <= 0:
87
+ return []
88
+
89
+ char_requirements: List[float] = [0.0] * column_count
90
+ for position in table.iter_grid():
91
+ if not position.is_anchor:
92
+ continue
93
+ text = position.cell.text or ""
94
+ lines = text.splitlines()
95
+ if not lines:
96
+ lines = [text]
97
+ longest = max(len(line) for line in lines)
98
+ span = max(1, position.col_span)
99
+ per_column = longest / span if span else float(longest)
100
+ for offset in range(span):
101
+ column_index = position.column + offset
102
+ if 0 <= column_index < column_count:
103
+ char_requirements[column_index] = max(
104
+ char_requirements[column_index],
105
+ per_column,
106
+ )
107
+
108
+ column_widths: List[int] = []
109
+ for requirement in char_requirements:
110
+ width = int(
111
+ math.ceil((requirement + _AUTO_FIT_PADDING_CHARS) * _AUTO_FIT_CHAR_UNIT)
112
+ )
113
+ width = max(width, _AUTO_FIT_MIN_COLUMN_WIDTH)
114
+ width = min(width, _AUTO_FIT_MAX_COLUMN_WIDTH)
115
+ column_widths.append(width)
116
+
117
+ total_width = sum(column_widths)
118
+ if total_width <= 0:
119
+ column_widths = [
120
+ max(_AUTO_FIT_MIN_COLUMN_WIDTH, _AUTO_FIT_CHAR_UNIT)
121
+ ] * column_count
122
+ total_width = sum(column_widths)
123
+
124
+ size_element = table.element.find(f"{HP_NS}sz")
125
+ if size_element is not None:
126
+ size_element.set("width", str(total_width))
127
+
128
+ for position in table.iter_grid():
129
+ if not position.is_anchor:
130
+ continue
131
+ span = max(1, position.col_span)
132
+ start = position.column
133
+ width_value = 0
134
+ for offset in range(span):
135
+ column_index = start + offset
136
+ if 0 <= column_index < column_count:
137
+ width_value += column_widths[column_index]
138
+ if width_value <= 0:
139
+ continue
140
+ cell_size = position.cell.element.find(f"{HP_NS}cellSz")
141
+ if cell_size is not None:
142
+ cell_size.set("width", str(width_value))
143
+
144
+ table.mark_dirty()
145
+ return column_widths
146
+
147
+ def _ensure_table_border_fill(
148
+ self,
149
+ document: HwpxDocument,
150
+ *,
151
+ border_style: Optional[str] = None,
152
+ border_color: Optional[str] = None,
153
+ border_width: Optional[str | float | int] = None,
154
+ fill_color: Optional[str] = None,
155
+ ) -> str:
156
+ normalized_style = (border_style or "").strip().lower() or None
157
+ if normalized_style not in {None, "solid", "none"}:
158
+ raise ValueError(f"Unsupported border style: {border_style}")
159
+
160
+ normalized_border_color = self._styles._normalize_color(border_color)
161
+ normalized_fill_color = self._styles._normalize_color(fill_color)
162
+
163
+ shortcut = _shortcut_border_fill_id(
164
+ document,
165
+ normalized_style,
166
+ normalized_border_color,
167
+ normalized_fill_color,
168
+ border_width,
169
+ )
170
+ if shortcut is not None:
171
+ return shortcut
172
+
173
+ if not document.headers:
174
+ raise self._context._new_error(
175
+ "STYLE_BORDER_FILL_HEADER_MISSING",
176
+ "document does not contain any headers to host border fills",
177
+ )
178
+
179
+ header = document.headers[0]
180
+ spec = _resolve_border_fill_spec(
181
+ normalized_style,
182
+ normalized_border_color,
183
+ normalized_fill_color,
184
+ border_width,
185
+ )
186
+
187
+ ref_list = header.element.find(f"{HH_NS}refList")
188
+ if ref_list is None:
189
+ ref_list = ET.SubElement(header.element, f"{HH_NS}refList")
190
+ header.mark_dirty()
191
+
192
+ border_fills_element = ref_list.find(f"{HH_NS}borderFills")
193
+ if border_fills_element is None:
194
+ border_fills_element = ET.SubElement(
195
+ ref_list, f"{HH_NS}borderFills", {"itemCnt": "0"}
196
+ )
197
+ header.mark_dirty()
198
+
199
+ matched = _find_matching_border_fill(border_fills_element, spec)
200
+ if matched is not None:
201
+ return matched
202
+
203
+ # Upstream still does not expose a public border-fill creation API.
204
+ # Keep the private helper usage isolated here until python-hwpx offers one.
205
+ if not hasattr(header, "_allocate_border_fill_id"):
206
+ raise self._context._new_error(
207
+ "STYLE_ID_ALLOCATOR_MISSING",
208
+ "header does not expose ID allocation helpers",
209
+ )
210
+
211
+ new_id = header._allocate_border_fill_id(border_fills_element)
212
+ _build_border_fill_element(border_fills_element, new_id, spec)
213
+
214
+ if hasattr(header, "_update_border_fills_item_count"):
215
+ header._update_border_fills_item_count(border_fills_element)
216
+ else:
217
+ count = len(border_fills_element.findall(f"{HH_NS}borderFill"))
218
+ border_fills_element.set("itemCnt", str(count))
219
+ header.mark_dirty()
220
+ return new_id
221
+
222
+ def add_table(
223
+ self,
224
+ path: str,
225
+ rows: int,
226
+ cols: int,
227
+ *,
228
+ section_index: Optional[int] = None,
229
+ border_style: str | None = None,
230
+ border_color: Optional[str] = None,
231
+ border_width: Optional[str | float | int] = None,
232
+ fill_color: Optional[str] = None,
233
+ auto_fit: bool = False,
234
+ ) -> Dict[str, Any]:
235
+ document, resolved = self._context._open_document(path)
236
+ border_fill = self._ensure_table_border_fill(
237
+ document,
238
+ border_style=border_style,
239
+ border_color=border_color,
240
+ border_width=border_width,
241
+ fill_color=fill_color,
242
+ )
243
+ table = document.add_table(
244
+ rows,
245
+ cols,
246
+ section_index=section_index,
247
+ border_fill_id_ref=border_fill,
248
+ )
249
+ if auto_fit:
250
+ self._auto_fit_table_columns(table)
251
+ tables = self._context._iter_tables(document)
252
+ element_id = id(table.element)
253
+ index = len(tables) - 1
254
+ for idx, candidate in enumerate(tables):
255
+ if id(candidate.element) == element_id:
256
+ index = idx
257
+ break
258
+ self._save._save_document(document, resolved)
259
+ return {"tableIndex": index, "cellCount": rows * cols}
260
+
261
+ def set_table_border_fill(
262
+ self,
263
+ path: str,
264
+ table_index: int,
265
+ *,
266
+ border_style: str | None = None,
267
+ border_color: Optional[str] = None,
268
+ border_width: Optional[str | float | int] = None,
269
+ fill_color: Optional[str] = None,
270
+ ) -> Dict[str, Any]:
271
+ document, resolved = self._context._open_document(path)
272
+ tables = self._context._iter_tables(document)
273
+ try:
274
+ table = tables[table_index]
275
+ except IndexError as exc:
276
+ raise self._context._new_error(
277
+ "TABLE_INDEX_OUT_OF_RANGE",
278
+ "tableIndex out of range",
279
+ details={"tableIndex": table_index},
280
+ ) from exc
281
+
282
+ border_fill = self._ensure_table_border_fill(
283
+ document,
284
+ border_style=border_style,
285
+ border_color=border_color,
286
+ border_width=border_width,
287
+ fill_color=fill_color,
288
+ )
289
+
290
+ table.element.set("borderFillIDRef", border_fill)
291
+ anchor_elements: set[int] = set()
292
+ for position in table.iter_grid():
293
+ if getattr(position, "is_anchor", False):
294
+ cell_element = position.cell.element
295
+ cell_element.set("borderFillIDRef", border_fill)
296
+ anchor_elements.add(id(cell_element))
297
+
298
+ table.mark_dirty()
299
+ self._save._save_document(document, resolved)
300
+ return {"borderFillIDRef": border_fill, "anchorCells": len(anchor_elements)}
301
+
302
+ def get_table_cell_map(
303
+ self,
304
+ path: str,
305
+ table_index: int,
306
+ ) -> Dict[str, Any]:
307
+ document, _ = self._context._open_document(path)
308
+ tables = self._context._iter_tables(document)
309
+ try:
310
+ table = tables[table_index]
311
+ except IndexError as exc:
312
+ raise self._context._new_error(
313
+ "TABLE_INDEX_OUT_OF_RANGE",
314
+ "tableIndex out of range",
315
+ details={"tableIndex": table_index},
316
+ ) from exc
317
+
318
+ grid_positions = table.get_cell_map()
319
+ serialized: List[List[Dict[str, Any]]] = []
320
+ for row in grid_positions:
321
+ row_payload: List[Dict[str, Any]] = []
322
+ for position in row:
323
+ anchor_row, anchor_col = position.anchor
324
+ row_span, col_span = position.span
325
+ cell_text: Optional[str] = None
326
+ cell = position.cell
327
+ if cell is not None:
328
+ cell_text = cell.text
329
+ row_payload.append(
330
+ {
331
+ "row": position.row,
332
+ "column": position.column,
333
+ "anchor": {"row": anchor_row, "column": anchor_col},
334
+ "rowSpan": row_span,
335
+ "colSpan": col_span,
336
+ "text": cell_text,
337
+ }
338
+ )
339
+ serialized.append(row_payload)
340
+ row_count = len(serialized)
341
+ column_count = len(serialized[0]) if serialized else 0
342
+ return {"grid": serialized, "rowCount": row_count, "columnCount": column_count}
343
+
344
+ def set_table_cell_text(
345
+ self,
346
+ path: str,
347
+ table_index: int,
348
+ row: int,
349
+ col: int,
350
+ text: str,
351
+ *,
352
+ dry_run: bool = False,
353
+ logical: Optional[bool] = None,
354
+ split_merged: Optional[bool] = None,
355
+ auto_fit: bool = False,
356
+ ) -> Dict[str, Any]:
357
+ document, resolved = self._context._open_document(path)
358
+ tables = self._context._iter_tables(document)
359
+ try:
360
+ table = tables[table_index]
361
+ except IndexError as exc:
362
+ raise self._context._new_error(
363
+ "TABLE_INDEX_OUT_OF_RANGE",
364
+ "tableIndex out of range",
365
+ details={"tableIndex": table_index},
366
+ ) from exc
367
+ kwargs: _CellAddressingKwargs = {}
368
+ if logical is not None:
369
+ kwargs["logical"] = logical
370
+ if split_merged is not None:
371
+ kwargs["split_merged"] = split_merged
372
+ guidance = (
373
+ "failed to update table cell; check indexes, enable logical addressing, "
374
+ "or split merged cells first"
375
+ )
376
+ try:
377
+ table.set_cell_text(row, col, _sanitize_cell_text(text), **kwargs)
378
+ except (IndexError, ValueError) as exc:
379
+ raise self._context._new_error(
380
+ "TABLE_CELL_OPERATION_FAILED", f"{guidance}: {exc}"
381
+ ) from exc
382
+ if auto_fit and not dry_run:
383
+ self._auto_fit_table_columns(table)
384
+ if not dry_run:
385
+ self._save._save_document(document, resolved)
386
+ return {"ok": True}
387
+
388
+ def replace_table_region(
389
+ self,
390
+ path: str,
391
+ table_index: int,
392
+ start_row: int,
393
+ start_col: int,
394
+ values: Sequence[Sequence[str]],
395
+ *,
396
+ dry_run: bool = False,
397
+ logical: Optional[bool] = None,
398
+ split_merged: Optional[bool] = None,
399
+ auto_fit: bool = False,
400
+ ) -> Dict[str, Any]:
401
+ document, resolved = self._context._open_document(path)
402
+ tables = self._context._iter_tables(document)
403
+ try:
404
+ table = tables[table_index]
405
+ except IndexError as exc:
406
+ raise self._context._new_error(
407
+ "TABLE_INDEX_OUT_OF_RANGE",
408
+ "tableIndex out of range",
409
+ details={"tableIndex": table_index},
410
+ ) from exc
411
+ kwargs: _CellAddressingKwargs = {}
412
+ if logical is not None:
413
+ kwargs["logical"] = logical
414
+ if split_merged is not None:
415
+ kwargs["split_merged"] = split_merged
416
+ guidance = (
417
+ "failed to update table cell; check indexes, enable logical addressing, "
418
+ "or split merged cells first"
419
+ )
420
+ updated = 0
421
+ for row_offset, row_values in enumerate(values):
422
+ for col_offset, cell_text in enumerate(row_values):
423
+ logical_row = start_row + row_offset
424
+ logical_col = start_col + col_offset
425
+ try:
426
+ table.set_cell_text(
427
+ logical_row,
428
+ logical_col,
429
+ _sanitize_cell_text(cell_text),
430
+ **kwargs,
431
+ )
432
+ except (IndexError, ValueError) as exc:
433
+ message = (
434
+ f"{guidance} while writing cell ({logical_row}, {logical_col})"
435
+ )
436
+ raise self._context._new_error(
437
+ "TABLE_CELL_OPERATION_FAILED",
438
+ f"{message}: {exc}",
439
+ details={"row": logical_row, "col": logical_col},
440
+ ) from exc
441
+ updated += 1
442
+ if auto_fit and not dry_run and updated > 0:
443
+ self._auto_fit_table_columns(table)
444
+ if not dry_run:
445
+ self._save._save_document(document, resolved)
446
+ return {"updatedCells": updated}
447
+
448
+ def split_table_cell(
449
+ self,
450
+ path: str,
451
+ table_index: int,
452
+ row: int,
453
+ col: int,
454
+ ) -> Dict[str, Any]:
455
+ document, resolved = self._context._open_document(path)
456
+ tables = self._context._iter_tables(document)
457
+ try:
458
+ table = tables[table_index]
459
+ except IndexError as exc:
460
+ raise self._context._new_error(
461
+ "TABLE_INDEX_OUT_OF_RANGE",
462
+ "tableIndex out of range",
463
+ details={"tableIndex": table_index},
464
+ ) from exc
465
+ try:
466
+ target = table.cell(row, col)
467
+ except (IndexError, ValueError) as exc:
468
+ raise self._context._new_error(
469
+ "TABLE_CELL_INDEX_OUT_OF_RANGE",
470
+ "table cell coordinates out of range; enable logical addressing to verify merged grids",
471
+ details={"row": row, "col": col},
472
+ ) from exc
473
+ anchor_row, anchor_col = target.address
474
+ span_row, span_col = target.span
475
+ changed = span_row > 1 or span_col > 1
476
+ guidance = "failed to split merged cell; check indexes or split manually if logical addressing shows overlaps"
477
+ try:
478
+ table.split_merged_cell(row, col)
479
+ except (IndexError, ValueError) as exc:
480
+ raise self._context._new_error(
481
+ "TABLE_CELL_OPERATION_FAILED", f"{guidance}: {exc}"
482
+ ) from exc
483
+ if changed:
484
+ self._save._save_document(document, resolved)
485
+ return {
486
+ "startRow": anchor_row,
487
+ "startCol": anchor_col,
488
+ "rowSpan": span_row,
489
+ "colSpan": span_col,
490
+ }
491
+
492
+ def copy_table_between_documents(
493
+ self,
494
+ source_path: str,
495
+ source_table_index: int,
496
+ target_path: str,
497
+ *,
498
+ target_section_index: Optional[int] = None,
499
+ auto_fit: bool = False,
500
+ ) -> Dict[str, Any]:
501
+ source_map = self.get_table_cell_map(source_path, source_table_index)
502
+ row_count = int(source_map["rowCount"])
503
+ column_count = int(source_map["columnCount"])
504
+ if row_count <= 0 or column_count <= 0:
505
+ raise self._context._new_error(
506
+ "TABLE_EMPTY",
507
+ "복사할 표 셀이 비어 있습니다.",
508
+ details={"tableIndex": source_table_index},
509
+ )
510
+
511
+ values: List[List[str]] = []
512
+ for row in source_map["grid"]:
513
+ row_values: List[str] = []
514
+ for cell in row:
515
+ row_values.append((cell.get("text") or "").strip())
516
+ values.append(row_values)
517
+
518
+ created = self.add_table(
519
+ target_path,
520
+ rows=row_count,
521
+ cols=column_count,
522
+ section_index=target_section_index,
523
+ auto_fit=auto_fit,
524
+ )
525
+ target_table_index = int(created["tableIndex"])
526
+ updated = self.replace_table_region(
527
+ target_path,
528
+ table_index=target_table_index,
529
+ start_row=0,
530
+ start_col=0,
531
+ values=values,
532
+ auto_fit=auto_fit,
533
+ )
534
+ return {
535
+ "targetTableIndex": target_table_index,
536
+ "copiedCells": updated["updatedCells"],
537
+ "rowCount": row_count,
538
+ "columnCount": column_count,
539
+ }