python-hwpx-automation 6.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..core.transactions import (
|
|
11
|
+
save_dry_run,
|
|
12
|
+
)
|
|
13
|
+
from ..upstream import (
|
|
14
|
+
HwpxDocument,
|
|
15
|
+
)
|
|
16
|
+
from hwpx.tools.id_integrity import check_id_integrity
|
|
17
|
+
|
|
18
|
+
from .context import DocumentContext
|
|
19
|
+
from .save_policy import SavePolicy
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("hwpx_automation.hwpx_ops")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MediaService:
|
|
25
|
+
def __init__(self, context: DocumentContext, save: SavePolicy) -> None:
|
|
26
|
+
self._context = context
|
|
27
|
+
self._save = save
|
|
28
|
+
|
|
29
|
+
def _decode_image_base64(self, image_base64: str) -> bytes:
|
|
30
|
+
try:
|
|
31
|
+
payload = base64.b64decode((image_base64 or "").strip(), validate=True)
|
|
32
|
+
except (binascii.Error, ValueError) as exc:
|
|
33
|
+
raise ValueError("invalid image_base64 payload") from exc
|
|
34
|
+
if not payload:
|
|
35
|
+
raise ValueError("image_base64 decoded to empty payload")
|
|
36
|
+
return payload
|
|
37
|
+
|
|
38
|
+
def _id_integrity_payload(self, document: HwpxDocument) -> Dict[str, Any]:
|
|
39
|
+
report = check_id_integrity(document)
|
|
40
|
+
return {
|
|
41
|
+
"ok": report.ok,
|
|
42
|
+
"dangling": [str(item) for item in report.dangling],
|
|
43
|
+
"orphanBinData": [str(item) for item in report.orphan_bin_data],
|
|
44
|
+
"ignored": [str(item) for item in report.ignored],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def add_shape(
|
|
48
|
+
self,
|
|
49
|
+
path: str,
|
|
50
|
+
*,
|
|
51
|
+
shape_type: str = "RECTANGLE",
|
|
52
|
+
section_index: Optional[int] = None,
|
|
53
|
+
dry_run: bool = True,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
document, resolved = self._context._open_document(path)
|
|
56
|
+
shape = document.add_shape(shape_type, section_index=section_index)
|
|
57
|
+
if not dry_run:
|
|
58
|
+
self._save._save_document(document, resolved)
|
|
59
|
+
return {"objectId": shape.element.get("id")}
|
|
60
|
+
|
|
61
|
+
def add_control(
|
|
62
|
+
self,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
control_type: str = "TEXTBOX",
|
|
66
|
+
section_index: Optional[int] = None,
|
|
67
|
+
dry_run: bool = True,
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
document, resolved = self._context._open_document(path)
|
|
70
|
+
control = document.add_control(
|
|
71
|
+
control_type=control_type, section_index=section_index
|
|
72
|
+
)
|
|
73
|
+
if not dry_run:
|
|
74
|
+
self._save._save_document(document, resolved)
|
|
75
|
+
return {"objectId": control.element.get("id")}
|
|
76
|
+
|
|
77
|
+
def insert_picture(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
image_base64: str,
|
|
81
|
+
*,
|
|
82
|
+
image_format: str = "png",
|
|
83
|
+
width: Optional[int] = None,
|
|
84
|
+
height: Optional[int] = None,
|
|
85
|
+
width_mm: Optional[float] = None,
|
|
86
|
+
height_mm: Optional[float] = None,
|
|
87
|
+
section_index: Optional[int] = None,
|
|
88
|
+
align: Optional[str] = None,
|
|
89
|
+
output: Optional[str] = None,
|
|
90
|
+
dry_run: bool = False,
|
|
91
|
+
) -> Dict[str, Any]:
|
|
92
|
+
document, resolved = self._context._open_document(path)
|
|
93
|
+
target = (
|
|
94
|
+
self._context._resolve_path(output, must_exist=False)
|
|
95
|
+
if output
|
|
96
|
+
else resolved
|
|
97
|
+
)
|
|
98
|
+
document.add_picture(
|
|
99
|
+
self._decode_image_base64(image_base64),
|
|
100
|
+
image_format,
|
|
101
|
+
width=width,
|
|
102
|
+
height=height,
|
|
103
|
+
width_mm=width_mm,
|
|
104
|
+
height_mm=height_mm,
|
|
105
|
+
section_index=section_index,
|
|
106
|
+
align=align,
|
|
107
|
+
)
|
|
108
|
+
picture_refs = document.picture_references()
|
|
109
|
+
result: Dict[str, Any] = {
|
|
110
|
+
"ok": True,
|
|
111
|
+
"dryRun": dry_run,
|
|
112
|
+
"filename": path,
|
|
113
|
+
"outputPath": str(target),
|
|
114
|
+
"picture": picture_refs[-1] if picture_refs else None,
|
|
115
|
+
"pictureReferences": picture_refs,
|
|
116
|
+
"idIntegrity": self._id_integrity_payload(document),
|
|
117
|
+
}
|
|
118
|
+
if dry_run:
|
|
119
|
+
result.update(save_dry_run(document, target))
|
|
120
|
+
return result
|
|
121
|
+
verification = self._save._save_transaction_document(document, target)
|
|
122
|
+
result["verificationReport"] = verification
|
|
123
|
+
result["openSafety"] = verification.get("openSafety")
|
|
124
|
+
if "visualComplete" in verification:
|
|
125
|
+
result["visualComplete"] = verification["visualComplete"]
|
|
126
|
+
if "backup" in verification:
|
|
127
|
+
result["backup"] = verification["backup"]
|
|
128
|
+
if "semanticDiff" in verification:
|
|
129
|
+
result["semanticDiff"] = verification["semanticDiff"]
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def replace_picture(
|
|
133
|
+
self,
|
|
134
|
+
path: str,
|
|
135
|
+
image_base64: str,
|
|
136
|
+
*,
|
|
137
|
+
image_format: str = "png",
|
|
138
|
+
picture_index: int = 0,
|
|
139
|
+
binary_item_id_ref: Optional[str] = None,
|
|
140
|
+
remove_orphaned: bool = True,
|
|
141
|
+
output: Optional[str] = None,
|
|
142
|
+
dry_run: bool = False,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
document, resolved = self._context._open_document(path)
|
|
145
|
+
target = (
|
|
146
|
+
self._context._resolve_path(output, must_exist=False)
|
|
147
|
+
if output
|
|
148
|
+
else resolved
|
|
149
|
+
)
|
|
150
|
+
replacement = document.replace_picture(
|
|
151
|
+
self._decode_image_base64(image_base64),
|
|
152
|
+
image_format,
|
|
153
|
+
picture_index=picture_index,
|
|
154
|
+
binary_item_id_ref=binary_item_id_ref,
|
|
155
|
+
remove_orphaned=remove_orphaned,
|
|
156
|
+
)
|
|
157
|
+
result: Dict[str, Any] = {
|
|
158
|
+
"ok": True,
|
|
159
|
+
"dryRun": dry_run,
|
|
160
|
+
"filename": path,
|
|
161
|
+
"outputPath": str(target),
|
|
162
|
+
"replacement": replacement,
|
|
163
|
+
"pictureReferences": document.picture_references(),
|
|
164
|
+
"idIntegrity": self._id_integrity_payload(document),
|
|
165
|
+
}
|
|
166
|
+
if dry_run:
|
|
167
|
+
result.update(save_dry_run(document, target))
|
|
168
|
+
return result
|
|
169
|
+
verification = self._save._save_transaction_document(document, target)
|
|
170
|
+
result["verificationReport"] = verification
|
|
171
|
+
result["openSafety"] = verification.get("openSafety")
|
|
172
|
+
if "visualComplete" in verification:
|
|
173
|
+
result["visualComplete"] = verification["visualComplete"]
|
|
174
|
+
if "backup" in verification:
|
|
175
|
+
result["backup"] = verification["backup"]
|
|
176
|
+
if "semanticDiff" in verification:
|
|
177
|
+
result["semanticDiff"] = verification["semanticDiff"]
|
|
178
|
+
return result
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import dataclasses
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, cast
|
|
11
|
+
from xml.etree import ElementTree as ET
|
|
12
|
+
|
|
13
|
+
from ..upstream import (
|
|
14
|
+
HP_NS,
|
|
15
|
+
HwpxDocument,
|
|
16
|
+
HwpxOxmlMemo,
|
|
17
|
+
HwpxOxmlParagraph,
|
|
18
|
+
HwpxOxmlRun,
|
|
19
|
+
ensure_char_style,
|
|
20
|
+
normalize_hex_color,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .context import DocumentContext
|
|
24
|
+
from .save_policy import SavePolicy
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("hwpx_automation.hwpx_ops")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Segment:
|
|
30
|
+
__slots__ = ("element", "attr", "text")
|
|
31
|
+
|
|
32
|
+
def __init__(self, element: ET.Element, attr: str, text: str) -> None:
|
|
33
|
+
self.element = element
|
|
34
|
+
self.attr = attr
|
|
35
|
+
self.text = text
|
|
36
|
+
|
|
37
|
+
def set(self, value: str) -> None:
|
|
38
|
+
self.text = value
|
|
39
|
+
if value:
|
|
40
|
+
setattr(self.element, self.attr, value)
|
|
41
|
+
else:
|
|
42
|
+
setattr(self.element, self.attr, "")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _gather_segments(element: ET.Element) -> List[_Segment]:
|
|
46
|
+
segments: List[_Segment] = []
|
|
47
|
+
|
|
48
|
+
def visit(node: ET.Element) -> None:
|
|
49
|
+
text_value = node.text or ""
|
|
50
|
+
segments.append(_Segment(node, "text", text_value))
|
|
51
|
+
for child in list(node):
|
|
52
|
+
visit(child)
|
|
53
|
+
tail_value = child.tail or ""
|
|
54
|
+
segments.append(_Segment(child, "tail", tail_value))
|
|
55
|
+
|
|
56
|
+
for text_node in element.findall(f"{HP_NS}t"):
|
|
57
|
+
visit(text_node)
|
|
58
|
+
return segments
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _slice_run(run_obj: HwpxOxmlRun, start: int, end: int) -> None:
|
|
62
|
+
segments = _gather_segments(run_obj.element)
|
|
63
|
+
if not segments:
|
|
64
|
+
return
|
|
65
|
+
total_length = sum(len(segment.text) for segment in segments)
|
|
66
|
+
start = max(0, min(start, total_length))
|
|
67
|
+
end = max(0, min(end, total_length))
|
|
68
|
+
if start >= end:
|
|
69
|
+
for segment in segments:
|
|
70
|
+
if segment.text:
|
|
71
|
+
segment.set("")
|
|
72
|
+
run_obj.paragraph.section.mark_dirty()
|
|
73
|
+
return
|
|
74
|
+
changed = False
|
|
75
|
+
offset = 0
|
|
76
|
+
for segment in segments:
|
|
77
|
+
seg_start = offset
|
|
78
|
+
seg_end = seg_start + len(segment.text)
|
|
79
|
+
offset = seg_end
|
|
80
|
+
if end <= seg_start or start >= seg_end:
|
|
81
|
+
if segment.text:
|
|
82
|
+
segment.set("")
|
|
83
|
+
changed = True
|
|
84
|
+
continue
|
|
85
|
+
local_start = max(start, seg_start) - seg_start
|
|
86
|
+
local_end = min(end, seg_end) - seg_start
|
|
87
|
+
new_value = segment.text[local_start:local_end]
|
|
88
|
+
if segment.text != new_value:
|
|
89
|
+
segment.set(new_value)
|
|
90
|
+
changed = True
|
|
91
|
+
if changed:
|
|
92
|
+
run_obj.paragraph.section.mark_dirty()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _split_run(
|
|
96
|
+
run_obj: HwpxOxmlRun, local_start: int, local_end: int, char_pr_id_ref: str
|
|
97
|
+
) -> None:
|
|
98
|
+
text_value = run_obj.text or ""
|
|
99
|
+
length = len(text_value)
|
|
100
|
+
if length == 0:
|
|
101
|
+
return
|
|
102
|
+
local_start = max(0, min(local_start, length))
|
|
103
|
+
local_end = max(0, min(local_end, length))
|
|
104
|
+
if local_start >= local_end:
|
|
105
|
+
return
|
|
106
|
+
if local_start == 0 and local_end == length:
|
|
107
|
+
run_obj.char_pr_id_ref = char_pr_id_ref
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
segments: List[Tuple[int, int, Optional[str]]] = []
|
|
111
|
+
original_char = run_obj.char_pr_id_ref
|
|
112
|
+
if local_start > 0:
|
|
113
|
+
segments.append((0, local_start, original_char))
|
|
114
|
+
segments.append((local_start, local_end, char_pr_id_ref))
|
|
115
|
+
if local_end < length:
|
|
116
|
+
segments.append((local_end, length, original_char))
|
|
117
|
+
|
|
118
|
+
parent = run_obj.paragraph.element
|
|
119
|
+
run_children = list(parent)
|
|
120
|
+
try:
|
|
121
|
+
index = run_children.index(run_obj.element)
|
|
122
|
+
except ValueError: # pragma: no cover - defensive branch
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
new_elements: List[ET.Element] = []
|
|
126
|
+
for seg_start, seg_end, char_id in segments:
|
|
127
|
+
if seg_start >= seg_end:
|
|
128
|
+
continue
|
|
129
|
+
element_copy = copy.deepcopy(run_obj.element)
|
|
130
|
+
segment_run = HwpxOxmlRun(element_copy, run_obj.paragraph)
|
|
131
|
+
_slice_run(segment_run, seg_start, seg_end)
|
|
132
|
+
if char_id is None:
|
|
133
|
+
segment_run.char_pr_id_ref = None
|
|
134
|
+
else:
|
|
135
|
+
segment_run.char_pr_id_ref = char_id
|
|
136
|
+
new_elements.append(element_copy)
|
|
137
|
+
|
|
138
|
+
if not new_elements:
|
|
139
|
+
parent.remove(run_obj.element)
|
|
140
|
+
run_obj.paragraph.section.mark_dirty()
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for offset, element in enumerate(new_elements):
|
|
144
|
+
parent.insert(index + offset, element)
|
|
145
|
+
parent.remove(run_obj.element)
|
|
146
|
+
run_obj.paragraph.section.mark_dirty()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _paragraph_length(paragraph: HwpxOxmlParagraph) -> int:
|
|
150
|
+
return sum(len(run.text or "") for run in paragraph.runs)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _apply_span(
|
|
154
|
+
paragraph: HwpxOxmlParagraph,
|
|
155
|
+
span_start: int,
|
|
156
|
+
span_end: int,
|
|
157
|
+
char_pr_id_ref: str,
|
|
158
|
+
) -> bool:
|
|
159
|
+
if span_start >= span_end:
|
|
160
|
+
return False
|
|
161
|
+
applied = False
|
|
162
|
+
cursor = span_start
|
|
163
|
+
while cursor < span_end:
|
|
164
|
+
runs = list(paragraph.runs)
|
|
165
|
+
offset = 0
|
|
166
|
+
target: Tuple[HwpxOxmlRun, int, int, int] | None = None
|
|
167
|
+
for candidate in runs:
|
|
168
|
+
text = candidate.text or ""
|
|
169
|
+
length = len(text)
|
|
170
|
+
run_start = offset
|
|
171
|
+
run_end = run_start + length
|
|
172
|
+
if run_end <= cursor:
|
|
173
|
+
offset = run_end
|
|
174
|
+
continue
|
|
175
|
+
if run_start >= span_end:
|
|
176
|
+
target = None
|
|
177
|
+
break
|
|
178
|
+
if length == 0:
|
|
179
|
+
offset = run_end
|
|
180
|
+
continue
|
|
181
|
+
target = (candidate, run_start, run_end, length)
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
if target is None:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
run_obj, run_start, run_end, length = target
|
|
188
|
+
local_start = max(0, cursor - run_start)
|
|
189
|
+
local_end = min(length, span_end - run_start)
|
|
190
|
+
if local_start >= local_end:
|
|
191
|
+
cursor = max(cursor + 1, run_end)
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
_split_run(run_obj, local_start, local_end, char_pr_id_ref)
|
|
195
|
+
applied = True
|
|
196
|
+
cursor = min(span_end, run_end)
|
|
197
|
+
|
|
198
|
+
return applied
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _span_bounds(span: Any) -> Tuple[int, int, int]:
|
|
202
|
+
if isinstance(span, dict):
|
|
203
|
+
paragraph_index = int(span.get("paragraph_index", -1))
|
|
204
|
+
start = int(span.get("start", 0))
|
|
205
|
+
end = int(span.get("end", 0))
|
|
206
|
+
else:
|
|
207
|
+
paragraph_index = int(
|
|
208
|
+
getattr(span, "paragraph_index", getattr(span, "paragraphIndex", -1))
|
|
209
|
+
)
|
|
210
|
+
start = int(getattr(span, "start", 0))
|
|
211
|
+
end = int(getattr(span, "end", 0))
|
|
212
|
+
return paragraph_index, start, end
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class MemoStyleService:
|
|
216
|
+
def __init__(self, context: DocumentContext, save: SavePolicy) -> None:
|
|
217
|
+
self._context = context
|
|
218
|
+
self._save = save
|
|
219
|
+
|
|
220
|
+
def _normalize_color(self, color: str | None) -> Optional[str]:
|
|
221
|
+
return normalize_hex_color(color, field_name="colorHex")
|
|
222
|
+
|
|
223
|
+
def _ensure_char_style(
|
|
224
|
+
self,
|
|
225
|
+
document: HwpxDocument,
|
|
226
|
+
run_style: Optional[Dict[str, Any]],
|
|
227
|
+
) -> Optional[str]:
|
|
228
|
+
if not run_style:
|
|
229
|
+
return None
|
|
230
|
+
bold = bool(run_style.get("bold", False))
|
|
231
|
+
italic = bool(run_style.get("italic", False))
|
|
232
|
+
underline = bool(run_style.get("underline", False))
|
|
233
|
+
color = self._normalize_color(run_style.get("colorHex"))
|
|
234
|
+
try:
|
|
235
|
+
return ensure_char_style(
|
|
236
|
+
document,
|
|
237
|
+
base_char_pr_id=None,
|
|
238
|
+
bold=bold,
|
|
239
|
+
italic=italic,
|
|
240
|
+
underline=underline,
|
|
241
|
+
color=color,
|
|
242
|
+
)
|
|
243
|
+
except (ValueError, RuntimeError) as exc:
|
|
244
|
+
message = str(exc)
|
|
245
|
+
if "document does not contain any headers" in message:
|
|
246
|
+
raise self._context._new_error("STYLE_HEADER_MISSING", message) from exc
|
|
247
|
+
if "char property does not expose an identifier" in message:
|
|
248
|
+
raise self._context._new_error(
|
|
249
|
+
"STYLE_CHAR_PROPERTY_ID_MISSING", message
|
|
250
|
+
) from exc
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
def find_runs_by_style(
|
|
254
|
+
self,
|
|
255
|
+
path: str,
|
|
256
|
+
*,
|
|
257
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
258
|
+
max_results: int = 200,
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
document, _ = self._context._open_document(path)
|
|
261
|
+
filter_args: Dict[str, Any] = {}
|
|
262
|
+
if filters:
|
|
263
|
+
if "colorHex" in filters and filters["colorHex"]:
|
|
264
|
+
filter_args["text_color"] = self._normalize_color(filters["colorHex"])
|
|
265
|
+
if "underline" in filters:
|
|
266
|
+
filter_args["underline_type"] = (
|
|
267
|
+
"SOLID" if filters["underline"] else "NONE"
|
|
268
|
+
)
|
|
269
|
+
if "charPrIDRef" in filters and filters["charPrIDRef"]:
|
|
270
|
+
filter_args["char_pr_id_ref"] = filters["charPrIDRef"]
|
|
271
|
+
runs = document.find_runs_by_style(**filter_args)
|
|
272
|
+
paragraph_index_map: Dict[int, int] = {}
|
|
273
|
+
paragraphs = self._context._iter_paragraphs(document)
|
|
274
|
+
for index, paragraph in enumerate(paragraphs):
|
|
275
|
+
paragraph_index_map[id(paragraph.element)] = index
|
|
276
|
+
results: List[Dict[str, Any]] = []
|
|
277
|
+
for run in runs[:max_results]:
|
|
278
|
+
paragraph = run.paragraph
|
|
279
|
+
para_index = paragraph_index_map.get(id(paragraph.element), -1)
|
|
280
|
+
style = {}
|
|
281
|
+
if run.style is not None:
|
|
282
|
+
style_data = run.style
|
|
283
|
+
if dataclasses.is_dataclass(style_data):
|
|
284
|
+
style = asdict(cast(Any, style_data))
|
|
285
|
+
results.append(
|
|
286
|
+
{
|
|
287
|
+
"text": run.text,
|
|
288
|
+
"paragraphIndex": para_index,
|
|
289
|
+
"charPrIDRef": run.char_pr_id_ref,
|
|
290
|
+
"style": style,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
return {"runs": results}
|
|
294
|
+
|
|
295
|
+
def add_memo(
|
|
296
|
+
self,
|
|
297
|
+
path: str,
|
|
298
|
+
text: str,
|
|
299
|
+
*,
|
|
300
|
+
section_index: Optional[int] = None,
|
|
301
|
+
author: str | None = None,
|
|
302
|
+
timestamp: str | None = None,
|
|
303
|
+
) -> Dict[str, Any]:
|
|
304
|
+
document, resolved = self._context._open_document(path)
|
|
305
|
+
memo = document.add_memo(
|
|
306
|
+
text,
|
|
307
|
+
section_index=section_index,
|
|
308
|
+
attributes={
|
|
309
|
+
"author": author or "",
|
|
310
|
+
"createDateTime": timestamp
|
|
311
|
+
or datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
self._save._save_document(document, resolved)
|
|
315
|
+
return {"memoId": memo.id}
|
|
316
|
+
|
|
317
|
+
def attach_memo_field(
|
|
318
|
+
self,
|
|
319
|
+
path: str,
|
|
320
|
+
paragraph_index: int,
|
|
321
|
+
memo_id: str,
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
document, resolved = self._context._open_document(path)
|
|
324
|
+
paragraphs = self._context._iter_paragraphs(document)
|
|
325
|
+
try:
|
|
326
|
+
paragraph = paragraphs[paragraph_index]
|
|
327
|
+
except IndexError as exc:
|
|
328
|
+
raise self._context._new_error(
|
|
329
|
+
"PARAGRAPH_INDEX_OUT_OF_RANGE",
|
|
330
|
+
"paragraphIndex out of range",
|
|
331
|
+
details={"paragraphIndex": paragraph_index},
|
|
332
|
+
) from exc
|
|
333
|
+
memo = self._find_memo(document, memo_id)
|
|
334
|
+
if memo is None:
|
|
335
|
+
raise self._context._new_error(
|
|
336
|
+
"MEMO_NOT_FOUND",
|
|
337
|
+
f"memo '{memo_id}' not found",
|
|
338
|
+
details={"memoId": memo_id},
|
|
339
|
+
)
|
|
340
|
+
field_id = document.attach_memo_field(paragraph, memo)
|
|
341
|
+
self._save._save_document(document, resolved)
|
|
342
|
+
return {"fieldId": field_id}
|
|
343
|
+
|
|
344
|
+
def add_memo_with_anchor(
|
|
345
|
+
self,
|
|
346
|
+
path: str,
|
|
347
|
+
*,
|
|
348
|
+
text: str,
|
|
349
|
+
section_index: Optional[int] = None,
|
|
350
|
+
memo_shape_id_ref: str | None = None,
|
|
351
|
+
) -> Dict[str, Any]:
|
|
352
|
+
document, resolved = self._context._open_document(path)
|
|
353
|
+
memo, paragraph, field_id = document.add_memo_with_anchor(
|
|
354
|
+
text,
|
|
355
|
+
section_index=section_index,
|
|
356
|
+
memo_shape_id_ref=memo_shape_id_ref,
|
|
357
|
+
)
|
|
358
|
+
paragraphs = self._context._iter_paragraphs(document)
|
|
359
|
+
paragraph_index = len(paragraphs) - 1
|
|
360
|
+
paragraph_element_id = id(paragraph.element)
|
|
361
|
+
for idx, candidate in enumerate(paragraphs):
|
|
362
|
+
if id(candidate.element) == paragraph_element_id:
|
|
363
|
+
paragraph_index = idx
|
|
364
|
+
break
|
|
365
|
+
self._save._save_document(document, resolved)
|
|
366
|
+
return {
|
|
367
|
+
"memoId": memo.id,
|
|
368
|
+
"paragraphIndex": paragraph_index,
|
|
369
|
+
"fieldId": field_id,
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def remove_memo(
|
|
373
|
+
self,
|
|
374
|
+
path: str,
|
|
375
|
+
memo_id: str,
|
|
376
|
+
*,
|
|
377
|
+
dry_run: bool = True,
|
|
378
|
+
) -> Dict[str, Any]:
|
|
379
|
+
document, resolved = self._context._open_document(path)
|
|
380
|
+
memo = self._find_memo(document, memo_id)
|
|
381
|
+
if memo is None:
|
|
382
|
+
return {"removed": False}
|
|
383
|
+
memo.remove()
|
|
384
|
+
if not dry_run:
|
|
385
|
+
self._save._save_document(document, resolved)
|
|
386
|
+
return {"removed": True}
|
|
387
|
+
|
|
388
|
+
def _find_memo(
|
|
389
|
+
self, document: HwpxDocument, memo_id: str
|
|
390
|
+
) -> Optional[HwpxOxmlMemo]:
|
|
391
|
+
for section in document.sections:
|
|
392
|
+
for memo in section.memos:
|
|
393
|
+
if memo.id == memo_id:
|
|
394
|
+
return memo
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def ensure_run_style(self, path: str, **run_style: Any) -> Dict[str, Any]:
|
|
398
|
+
document, _ = self._context._open_document(path)
|
|
399
|
+
char_id = self._ensure_char_style(document, run_style)
|
|
400
|
+
return {"charPrIDRef": char_id}
|
|
401
|
+
|
|
402
|
+
def list_styles_and_bullets(self, path: str) -> Dict[str, Any]:
|
|
403
|
+
document, _ = self._context._open_document(path)
|
|
404
|
+
styles = [
|
|
405
|
+
asdict(cast(Any, style))
|
|
406
|
+
for style in document.styles.values()
|
|
407
|
+
if dataclasses.is_dataclass(style)
|
|
408
|
+
]
|
|
409
|
+
bullets = [
|
|
410
|
+
asdict(cast(Any, bullet))
|
|
411
|
+
for bullet in document.bullets.values()
|
|
412
|
+
if dataclasses.is_dataclass(bullet)
|
|
413
|
+
]
|
|
414
|
+
return {"styles": styles, "bullets": bullets}
|
|
415
|
+
|
|
416
|
+
def apply_style_to_text_ranges(
|
|
417
|
+
self,
|
|
418
|
+
path: str,
|
|
419
|
+
spans: Sequence[Dict[str, int]],
|
|
420
|
+
char_pr_id_ref: str,
|
|
421
|
+
*,
|
|
422
|
+
dry_run: bool = True,
|
|
423
|
+
) -> Dict[str, Any]:
|
|
424
|
+
if not char_pr_id_ref:
|
|
425
|
+
raise ValueError("char_pr_id_ref must be provided")
|
|
426
|
+
|
|
427
|
+
document, resolved = self._context._open_document(path)
|
|
428
|
+
paragraphs = self._context._iter_paragraphs(document)
|
|
429
|
+
|
|
430
|
+
styled = 0
|
|
431
|
+
for span in spans:
|
|
432
|
+
paragraph_index, start, end = _span_bounds(span)
|
|
433
|
+
|
|
434
|
+
if paragraph_index < 0 or paragraph_index >= len(paragraphs):
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
start = max(0, start)
|
|
438
|
+
end = max(start, end)
|
|
439
|
+
if start >= end:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
paragraph = paragraphs[paragraph_index]
|
|
443
|
+
total_length = _paragraph_length(paragraph)
|
|
444
|
+
if total_length == 0 or start >= total_length:
|
|
445
|
+
continue
|
|
446
|
+
clamped_end = min(end, total_length)
|
|
447
|
+
|
|
448
|
+
if _apply_span(paragraph, start, clamped_end, char_pr_id_ref):
|
|
449
|
+
styled += 1
|
|
450
|
+
|
|
451
|
+
if not dry_run and styled:
|
|
452
|
+
self._save._save_document(document, resolved)
|
|
453
|
+
|
|
454
|
+
return {"styledSpans": styled}
|
|
455
|
+
|
|
456
|
+
def apply_style_to_paragraphs(
|
|
457
|
+
self,
|
|
458
|
+
path: str,
|
|
459
|
+
paragraph_indexes: Sequence[int],
|
|
460
|
+
char_pr_id_ref: str,
|
|
461
|
+
*,
|
|
462
|
+
dry_run: bool = True,
|
|
463
|
+
) -> Dict[str, Any]:
|
|
464
|
+
document, resolved = self._context._open_document(path)
|
|
465
|
+
paragraphs = self._context._iter_paragraphs(document)
|
|
466
|
+
updated = 0
|
|
467
|
+
for index in paragraph_indexes:
|
|
468
|
+
if index < 0 or index >= len(paragraphs):
|
|
469
|
+
continue
|
|
470
|
+
paragraph = paragraphs[index]
|
|
471
|
+
paragraph.char_pr_id_ref = char_pr_id_ref
|
|
472
|
+
for run in paragraph.runs:
|
|
473
|
+
run.char_pr_id_ref = char_pr_id_ref
|
|
474
|
+
updated += 1
|
|
475
|
+
if not dry_run and updated:
|
|
476
|
+
self._save._save_document(document, resolved)
|
|
477
|
+
return {"updated": updated}
|