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,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""MCP Resource 직렬화 모델."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResourceModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid", frozen=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParagraphResourceEntry(ResourceModel):
|
|
16
|
+
paragraph_index: int = Field(alias="paragraphIndex")
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TableResourceEntry(ResourceModel):
|
|
21
|
+
table_index: int = Field(alias="tableIndex")
|
|
22
|
+
row_count: int = Field(alias="rowCount")
|
|
23
|
+
column_count: int = Field(alias="columnCount")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DocumentMetadataResource(ResourceModel):
|
|
27
|
+
handle_id: str = Field(alias="handleId")
|
|
28
|
+
locator: Dict[str, Any]
|
|
29
|
+
meta: Dict[str, Any]
|
|
30
|
+
section_count: int = Field(alias="sectionCount")
|
|
31
|
+
paragraph_count: int = Field(alias="paragraphCount")
|
|
32
|
+
header_count: int = Field(alias="headerCount")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DocumentParagraphsResource(ResourceModel):
|
|
36
|
+
handle_id: str = Field(alias="handleId")
|
|
37
|
+
paragraphs: List[ParagraphResourceEntry]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DocumentTablesResource(ResourceModel):
|
|
41
|
+
handle_id: str = Field(alias="handleId")
|
|
42
|
+
tables: List[TableResourceEntry]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
from xml.etree import ElementTree as ET
|
|
6
|
+
|
|
7
|
+
from ..upstream import repair_pathological_text_spacing
|
|
8
|
+
from .locations import iter_paragraph_locations, location_anchor
|
|
9
|
+
|
|
10
|
+
_HP_NS = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _replace_within_runs(runs: list[Any], find_text: str, replace_text: str) -> int:
|
|
14
|
+
replaced_total = 0
|
|
15
|
+
for run in runs:
|
|
16
|
+
run_text = run.text or ""
|
|
17
|
+
if not run_text or find_text not in run_text:
|
|
18
|
+
continue
|
|
19
|
+
expected = run_text.count(find_text)
|
|
20
|
+
if expected <= 0:
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
replaced = 0
|
|
24
|
+
if hasattr(run, "replace_text"):
|
|
25
|
+
try:
|
|
26
|
+
replaced = int(run.replace_text(find_text, replace_text))
|
|
27
|
+
except Exception: # noqa: BLE001
|
|
28
|
+
replaced = 0
|
|
29
|
+
|
|
30
|
+
if replaced <= 0:
|
|
31
|
+
run.text = run_text.replace(find_text, replace_text)
|
|
32
|
+
replaced = expected
|
|
33
|
+
|
|
34
|
+
replaced_total += replaced
|
|
35
|
+
return replaced_total
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _append_xml_text_node(run_element: Any) -> Any:
|
|
39
|
+
maker = getattr(run_element, "makeelement", None)
|
|
40
|
+
if callable(maker):
|
|
41
|
+
node = maker(f"{_HP_NS}t", {})
|
|
42
|
+
run_element.append(node)
|
|
43
|
+
return node
|
|
44
|
+
return ET.SubElement(run_element, f"{_HP_NS}t")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _xml_run_text(run_element: Any) -> str:
|
|
48
|
+
parts: list[str] = []
|
|
49
|
+
for text_node in run_element.findall(f"{_HP_NS}t"):
|
|
50
|
+
parts.append("".join(text_node.itertext()))
|
|
51
|
+
return "".join(parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _set_xml_run_text(run_element: Any, value: str) -> None:
|
|
55
|
+
text_nodes = list(run_element.findall(f"{_HP_NS}t"))
|
|
56
|
+
primary = text_nodes[0] if text_nodes else _append_xml_text_node(run_element)
|
|
57
|
+
for child in list(primary):
|
|
58
|
+
primary.remove(child)
|
|
59
|
+
primary.text = value
|
|
60
|
+
for node in text_nodes[1:]:
|
|
61
|
+
for child in list(node):
|
|
62
|
+
node.remove(child)
|
|
63
|
+
node.text = ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _clear_xml_paragraph_layout_cache(paragraph_element: Any) -> None:
|
|
67
|
+
for child in list(paragraph_element):
|
|
68
|
+
tag = getattr(child, "tag", "")
|
|
69
|
+
local_name = tag.rsplit("}", 1)[-1] if "}" in tag else tag
|
|
70
|
+
if local_name.lower() == "linesegarray":
|
|
71
|
+
paragraph_element.remove(child)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _replace_across_runs(runs: list[Any], find_text: str, replace_text: str) -> int:
|
|
75
|
+
if not runs or not find_text:
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
texts = [run.text or "" for run in runs]
|
|
79
|
+
merged = "".join(texts)
|
|
80
|
+
if find_text not in merged:
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
replaced_count = merged.count(find_text)
|
|
84
|
+
new_merged = merged.replace(find_text, replace_text)
|
|
85
|
+
runs[0].text = new_merged
|
|
86
|
+
for run in runs[1:]:
|
|
87
|
+
run.text = ""
|
|
88
|
+
|
|
89
|
+
return replaced_count
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _replace_in_runs(runs: list[Any], find_text: str, replace_text: str) -> int:
|
|
93
|
+
if not runs:
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
replaced = _replace_within_runs(runs, find_text, replace_text)
|
|
97
|
+
merged_after_simple = "".join(run.text or "" for run in runs)
|
|
98
|
+
if find_text in merged_after_simple:
|
|
99
|
+
replaced += _replace_across_runs(runs, find_text, replace_text)
|
|
100
|
+
return replaced
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _replace_within_xml_runs(run_elements: list[Any], find_text: str, replace_text: str) -> int:
|
|
104
|
+
replaced_total = 0
|
|
105
|
+
for run_element in run_elements:
|
|
106
|
+
run_text = _xml_run_text(run_element)
|
|
107
|
+
if not run_text or find_text not in run_text:
|
|
108
|
+
continue
|
|
109
|
+
replaced = run_text.count(find_text)
|
|
110
|
+
_set_xml_run_text(run_element, run_text.replace(find_text, replace_text))
|
|
111
|
+
replaced_total += replaced
|
|
112
|
+
return replaced_total
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _replace_across_xml_runs(run_elements: list[Any], find_text: str, replace_text: str) -> int:
|
|
116
|
+
if not run_elements or not find_text:
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
texts = [_xml_run_text(run_element) for run_element in run_elements]
|
|
120
|
+
merged = "".join(texts)
|
|
121
|
+
if find_text not in merged:
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
replaced_count = merged.count(find_text)
|
|
125
|
+
new_merged = merged.replace(find_text, replace_text)
|
|
126
|
+
_set_xml_run_text(run_elements[0], new_merged)
|
|
127
|
+
for run_element in run_elements[1:]:
|
|
128
|
+
_set_xml_run_text(run_element, "")
|
|
129
|
+
|
|
130
|
+
return replaced_count
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _replace_in_xml_runs(run_elements: list[Any], find_text: str, replace_text: str) -> int:
|
|
134
|
+
if not run_elements:
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
replaced = _replace_within_xml_runs(run_elements, find_text, replace_text)
|
|
138
|
+
merged_after_simple = "".join(_xml_run_text(run_element) for run_element in run_elements)
|
|
139
|
+
if find_text in merged_after_simple:
|
|
140
|
+
replaced += _replace_across_xml_runs(run_elements, find_text, replace_text)
|
|
141
|
+
return replaced
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def find_in_doc(doc: Any, text_to_find: str, match_case: bool = True, max_results: int = 50) -> dict:
|
|
145
|
+
if text_to_find == "":
|
|
146
|
+
raise ValueError("text_to_find는 빈 문자열일 수 없습니다.")
|
|
147
|
+
matches: list[dict] = []
|
|
148
|
+
needle = text_to_find if match_case else text_to_find.lower()
|
|
149
|
+
|
|
150
|
+
for resolved in iter_paragraph_locations(doc):
|
|
151
|
+
location = resolved.location
|
|
152
|
+
haystack_raw = resolved.paragraph.text or ""
|
|
153
|
+
haystack = haystack_raw if match_case else haystack_raw.lower()
|
|
154
|
+
cursor = 0
|
|
155
|
+
while True:
|
|
156
|
+
pos = haystack.find(needle, cursor)
|
|
157
|
+
if pos < 0:
|
|
158
|
+
break
|
|
159
|
+
context_start = max(0, pos - 20)
|
|
160
|
+
context_end = min(len(haystack_raw), pos + len(text_to_find) + 20)
|
|
161
|
+
match = {
|
|
162
|
+
"location": location,
|
|
163
|
+
"anchor": location_anchor(location, pos),
|
|
164
|
+
"position": pos,
|
|
165
|
+
"context": haystack_raw[context_start:context_end],
|
|
166
|
+
}
|
|
167
|
+
if location.get("kind") == "body_paragraph":
|
|
168
|
+
match["paragraph_index"] = location["paragraph_index"]
|
|
169
|
+
else:
|
|
170
|
+
match["table_index"] = location["table_index"]
|
|
171
|
+
match["row"] = location["row"]
|
|
172
|
+
match["col"] = location["col"]
|
|
173
|
+
match["cell_paragraph_index"] = location["cell_paragraph_index"]
|
|
174
|
+
matches.append(match)
|
|
175
|
+
if len(matches) >= max_results:
|
|
176
|
+
return {"matches": matches, "total_matches": len(matches)}
|
|
177
|
+
cursor = pos + max(1, len(text_to_find))
|
|
178
|
+
|
|
179
|
+
return {"matches": matches, "total_matches": len(matches)}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def replace_in_doc(doc: Any, find_text: str, replace_text: str) -> int:
|
|
183
|
+
if find_text == "":
|
|
184
|
+
raise ValueError("find_text는 빈 문자열일 수 없습니다.")
|
|
185
|
+
|
|
186
|
+
count = 0
|
|
187
|
+
try:
|
|
188
|
+
for para in doc.paragraphs:
|
|
189
|
+
runs = list(getattr(para, "runs", []))
|
|
190
|
+
before_run_texts = [run.text or "" for run in runs]
|
|
191
|
+
replaced = _replace_in_runs(runs, find_text, replace_text) if runs else 0
|
|
192
|
+
has_tables = bool(getattr(para, "tables", []))
|
|
193
|
+
if replaced == 0 and not has_tables:
|
|
194
|
+
text = para.text or ""
|
|
195
|
+
if find_text in text:
|
|
196
|
+
replaced = text.count(find_text)
|
|
197
|
+
para.text = text.replace(find_text, replace_text)
|
|
198
|
+
if replaced:
|
|
199
|
+
current_runs = list(getattr(para, "runs", []))
|
|
200
|
+
changed_runs = [
|
|
201
|
+
run
|
|
202
|
+
for index, run in enumerate(current_runs)
|
|
203
|
+
if (run.text or "")
|
|
204
|
+
and (index >= len(before_run_texts) or (run.text or "") != before_run_texts[index])
|
|
205
|
+
]
|
|
206
|
+
repair_pathological_text_spacing(doc, paragraph=para, runs=changed_runs)
|
|
207
|
+
count += replaced
|
|
208
|
+
|
|
209
|
+
for para in doc.paragraphs:
|
|
210
|
+
for table in getattr(para, "tables", []):
|
|
211
|
+
for row in table.rows:
|
|
212
|
+
for cell in row.cells:
|
|
213
|
+
cell_paragraphs = list(getattr(cell, "paragraphs", []))
|
|
214
|
+
if cell_paragraphs:
|
|
215
|
+
cell_replaced = 0
|
|
216
|
+
for cell_para in cell_paragraphs:
|
|
217
|
+
runs = list(getattr(cell_para, "runs", []))
|
|
218
|
+
before_run_texts = [run.text or "" for run in runs]
|
|
219
|
+
para_replaced = _replace_in_runs(runs, find_text, replace_text) if runs else 0
|
|
220
|
+
if para_replaced == 0:
|
|
221
|
+
text = cell_para.text or ""
|
|
222
|
+
if find_text in text:
|
|
223
|
+
para_replaced = text.count(find_text)
|
|
224
|
+
cell_para.text = text.replace(find_text, replace_text)
|
|
225
|
+
if para_replaced:
|
|
226
|
+
current_runs = list(getattr(cell_para, "runs", []))
|
|
227
|
+
changed_runs = [
|
|
228
|
+
run
|
|
229
|
+
for index, run in enumerate(current_runs)
|
|
230
|
+
if (run.text or "")
|
|
231
|
+
and (
|
|
232
|
+
index >= len(before_run_texts)
|
|
233
|
+
or (run.text or "") != before_run_texts[index]
|
|
234
|
+
)
|
|
235
|
+
]
|
|
236
|
+
repair_pathological_text_spacing(
|
|
237
|
+
doc,
|
|
238
|
+
paragraph=cell_para,
|
|
239
|
+
runs=changed_runs,
|
|
240
|
+
)
|
|
241
|
+
cell_replaced += para_replaced
|
|
242
|
+
count += cell_replaced
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
xml_replaced = 0
|
|
246
|
+
cell_element = getattr(cell, "element", None)
|
|
247
|
+
if cell_element is not None:
|
|
248
|
+
for cell_para_element in cell_element.findall(f".//{_HP_NS}p"):
|
|
249
|
+
run_elements = list(cell_para_element.findall(f"{_HP_NS}run"))
|
|
250
|
+
if not run_elements:
|
|
251
|
+
continue
|
|
252
|
+
para_replaced = _replace_in_xml_runs(run_elements, find_text, replace_text)
|
|
253
|
+
if para_replaced:
|
|
254
|
+
repair_pathological_text_spacing(
|
|
255
|
+
doc,
|
|
256
|
+
runs=[
|
|
257
|
+
run
|
|
258
|
+
for run in run_elements
|
|
259
|
+
if _xml_run_text(run)
|
|
260
|
+
],
|
|
261
|
+
)
|
|
262
|
+
_clear_xml_paragraph_layout_cache(cell_para_element)
|
|
263
|
+
xml_replaced += para_replaced
|
|
264
|
+
if xml_replaced:
|
|
265
|
+
count += xml_replaced
|
|
266
|
+
section = getattr(para, "section", None)
|
|
267
|
+
if section is not None and hasattr(section, "mark_dirty"):
|
|
268
|
+
section.mark_dirty()
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
text = cell.text or ""
|
|
272
|
+
if find_text in text:
|
|
273
|
+
replaced = text.count(find_text)
|
|
274
|
+
cell.text = text.replace(find_text, replace_text)
|
|
275
|
+
count += replaced
|
|
276
|
+
except Exception as e: # noqa: BLE001
|
|
277
|
+
raise RuntimeError(f"텍스트 치환 중 오류: {e}") from e
|
|
278
|
+
return count
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def batch_replace_in_doc(doc: Any, replacements: list[dict]) -> dict:
|
|
282
|
+
results: list[dict] = []
|
|
283
|
+
total = 0
|
|
284
|
+
for index, item in enumerate(replacements):
|
|
285
|
+
if not isinstance(item, dict):
|
|
286
|
+
raise ValueError(f"replacements[{index}]는 dict여야 합니다.")
|
|
287
|
+
|
|
288
|
+
found = str(item.get("find", ""))
|
|
289
|
+
repl = str(item.get("replace", ""))
|
|
290
|
+
if found == "":
|
|
291
|
+
raise ValueError(f"replacements[{index}].find는 빈 문자열일 수 없습니다.")
|
|
292
|
+
|
|
293
|
+
replaced = replace_in_doc(doc, found, repl)
|
|
294
|
+
total += replaced
|
|
295
|
+
results.append({"find": found, "replace": repl, "replaced_count": replaced})
|
|
296
|
+
return {"results": results, "total_replaced": total}
|