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,660 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Layout and style automation handlers for the optional MCP adapter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..office.authoring.style_profile import (
|
|
9
|
+
apply_style_profile_to_plan as apply_hwpx_style_profile_to_plan,
|
|
10
|
+
compare_style_profiles as compare_hwpx_style_profiles,
|
|
11
|
+
extract_style_profile as extract_hwpx_style_profile,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..core.content import (
|
|
15
|
+
add_memo_to_doc,
|
|
16
|
+
remove_memo_from_doc,
|
|
17
|
+
)
|
|
18
|
+
from ..core.document import open_doc
|
|
19
|
+
from ..core.formatting import (
|
|
20
|
+
create_style_in_doc,
|
|
21
|
+
format_text_range,
|
|
22
|
+
list_styles_in_doc,
|
|
23
|
+
)
|
|
24
|
+
from ..core.locations import location_from_anchor
|
|
25
|
+
from ..upstream import (
|
|
26
|
+
HP_NS,
|
|
27
|
+
)
|
|
28
|
+
from ..utils.helpers import resolve_path
|
|
29
|
+
from ._shared import (
|
|
30
|
+
_revision_guard,
|
|
31
|
+
_save_doc_verification,
|
|
32
|
+
_with_dry_run_verification,
|
|
33
|
+
_with_save_verification,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_style_profile(filename: str) -> dict:
|
|
38
|
+
"""참조 HWPX의 페이지·폰트·표 프로파일을 plan 적용용 JSON으로 추출합니다."""
|
|
39
|
+
if extract_hwpx_style_profile is None:
|
|
40
|
+
raise RuntimeError("installed python-hwpx does not provide style profile tools")
|
|
41
|
+
return extract_hwpx_style_profile(resolve_path(filename))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def apply_style_profile_to_plan(
|
|
45
|
+
document_plan: dict,
|
|
46
|
+
style_profile: dict = None,
|
|
47
|
+
reference_filename: str = None,
|
|
48
|
+
overwrite: bool = True,
|
|
49
|
+
) -> dict:
|
|
50
|
+
"""style_profile 또는 reference HWPX 서식을 document_plan v2에 적용합니다."""
|
|
51
|
+
if apply_hwpx_style_profile_to_plan is None:
|
|
52
|
+
raise RuntimeError("installed python-hwpx does not provide style profile tools")
|
|
53
|
+
profile = style_profile
|
|
54
|
+
if profile is None:
|
|
55
|
+
if not reference_filename:
|
|
56
|
+
raise ValueError("provide style_profile or reference_filename")
|
|
57
|
+
if extract_hwpx_style_profile is None:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
"installed python-hwpx does not provide style profile extraction"
|
|
60
|
+
)
|
|
61
|
+
profile = extract_hwpx_style_profile(resolve_path(reference_filename))
|
|
62
|
+
return {
|
|
63
|
+
"document_plan": apply_hwpx_style_profile_to_plan(
|
|
64
|
+
document_plan or {},
|
|
65
|
+
profile,
|
|
66
|
+
overwrite=overwrite,
|
|
67
|
+
),
|
|
68
|
+
"style_profile": profile,
|
|
69
|
+
"next_tool": "create_document_from_plan",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compare_style_profiles(
|
|
74
|
+
reference_filename: str = None,
|
|
75
|
+
candidate_filename: str = None,
|
|
76
|
+
reference_profile: dict = None,
|
|
77
|
+
candidate_profile: dict = None,
|
|
78
|
+
margin_tolerance_mm: float = 1.0,
|
|
79
|
+
table_weight_tolerance: float = 0.10,
|
|
80
|
+
) -> dict:
|
|
81
|
+
"""참조/후보 HWPX 또는 style_profile의 페이지·표 프로파일 유사성을 비교합니다."""
|
|
82
|
+
if compare_hwpx_style_profiles is None:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
"installed python-hwpx does not provide style profile comparison"
|
|
85
|
+
)
|
|
86
|
+
reference = reference_profile or (
|
|
87
|
+
resolve_path(reference_filename) if reference_filename else None
|
|
88
|
+
)
|
|
89
|
+
candidate = candidate_profile or (
|
|
90
|
+
resolve_path(candidate_filename) if candidate_filename else None
|
|
91
|
+
)
|
|
92
|
+
if reference is None or candidate is None:
|
|
93
|
+
raise ValueError("provide reference/candidate filenames or profiles")
|
|
94
|
+
return compare_hwpx_style_profiles(
|
|
95
|
+
reference,
|
|
96
|
+
candidate,
|
|
97
|
+
margin_tolerance_mm=margin_tolerance_mm,
|
|
98
|
+
table_weight_tolerance=table_weight_tolerance,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _toc_format_guard(filename: str, path: str) -> dict | None:
|
|
103
|
+
if str(path).lower().endswith((".hwpx", ".hwpxml")):
|
|
104
|
+
return None
|
|
105
|
+
return {
|
|
106
|
+
"ok": False,
|
|
107
|
+
"error": "native TOC/cross-reference tools support HWPX only",
|
|
108
|
+
"filename": filename,
|
|
109
|
+
"path": path,
|
|
110
|
+
"errors": [
|
|
111
|
+
{"code": "unsupported_format", "message": "HWPX(.hwpx)만 지원합니다"}
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def add_toc(
|
|
117
|
+
filename: str,
|
|
118
|
+
level: int = 2,
|
|
119
|
+
leader: int = 3,
|
|
120
|
+
hyperlink: bool = False,
|
|
121
|
+
at_index: int = 0,
|
|
122
|
+
dry_run: bool = False,
|
|
123
|
+
) -> dict:
|
|
124
|
+
"""개요 스타일 제목들로 한컴 네이티브 차례(TABLEOFCONTENTS 필드)를 삽입합니다.
|
|
125
|
+
|
|
126
|
+
고정 텍스트 목차가 아니라 한컴이 인식·재계산하는 필드입니다. dirty=1로
|
|
127
|
+
방출되므로 한컴이 처음 여는 순간 항목·스타일·쪽번호를 스스로 재계산합니다
|
|
128
|
+
(방출 시점의 쪽번호는 추정치 — 응답의 cachedPagesAreEstimates 참조).
|
|
129
|
+
본문 문단은 본문(스타일 1) 등 비수집 스타일이어야 항목으로 끌려가지
|
|
130
|
+
않습니다(바탕글=스타일 0은 수집 대상)."""
|
|
131
|
+
from hwpx.tools.toc_author import add_native_toc
|
|
132
|
+
|
|
133
|
+
path = resolve_path(filename)
|
|
134
|
+
guard = _toc_format_guard(filename, path)
|
|
135
|
+
if guard is not None:
|
|
136
|
+
return guard
|
|
137
|
+
doc = open_doc(path)
|
|
138
|
+
try:
|
|
139
|
+
summary = add_native_toc(
|
|
140
|
+
doc, at_index=at_index, level=level, leader=leader, hyperlink=hyperlink
|
|
141
|
+
)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
return {
|
|
144
|
+
"ok": False,
|
|
145
|
+
"error": str(exc),
|
|
146
|
+
"hint": "개요 1~10 스타일의 제목 문단이 필요합니다 (add_heading 사용)",
|
|
147
|
+
"errors": [{"code": "no_outline_headings", "message": str(exc)}],
|
|
148
|
+
}
|
|
149
|
+
if dry_run:
|
|
150
|
+
return _with_dry_run_verification(
|
|
151
|
+
{"ok": True, "dryRun": True, **summary}, doc, path
|
|
152
|
+
)
|
|
153
|
+
verification = _save_doc_verification(doc, path)
|
|
154
|
+
return _with_save_verification({"ok": True, **summary}, verification)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def add_cross_reference(
|
|
158
|
+
filename: str,
|
|
159
|
+
paragraph_index: int,
|
|
160
|
+
target_heading_text: str,
|
|
161
|
+
cached_page: int = 1,
|
|
162
|
+
dry_run: bool = False,
|
|
163
|
+
) -> dict:
|
|
164
|
+
"""지정 문단 끝에 특정 제목의 '쪽 번호' 상호참조(CROSSREF 필드)를 추가합니다.
|
|
165
|
+
|
|
166
|
+
캐시 쪽번호는 추정치여도 됩니다 — 한컴이 문서를 열거나 편집/저장할 때
|
|
167
|
+
자동으로 재계산합니다(실측 의미론)."""
|
|
168
|
+
from hwpx.tools.toc_author import add_page_crossref
|
|
169
|
+
|
|
170
|
+
path = resolve_path(filename)
|
|
171
|
+
guard = _toc_format_guard(filename, path)
|
|
172
|
+
if guard is not None:
|
|
173
|
+
return guard
|
|
174
|
+
doc = open_doc(path)
|
|
175
|
+
paragraphs = [p for s in doc.sections for p in s.paragraphs]
|
|
176
|
+
if not (0 <= paragraph_index < len(paragraphs)):
|
|
177
|
+
return {
|
|
178
|
+
"ok": False,
|
|
179
|
+
"error": f"paragraph_index {paragraph_index} out of range (total {len(paragraphs)})",
|
|
180
|
+
"errors": [
|
|
181
|
+
{
|
|
182
|
+
"code": "paragraph_index_out_of_range",
|
|
183
|
+
"message": str(paragraph_index),
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
needle = target_heading_text.strip()
|
|
188
|
+
target = next((p for p in paragraphs if (p.text or "").strip() == needle), None)
|
|
189
|
+
if target is None:
|
|
190
|
+
target = next(
|
|
191
|
+
(p for p in paragraphs if needle and needle in (p.text or "")), None
|
|
192
|
+
)
|
|
193
|
+
if target is None:
|
|
194
|
+
return {
|
|
195
|
+
"ok": False,
|
|
196
|
+
"error": f"target heading not found: {target_heading_text!r}",
|
|
197
|
+
"errors": [{"code": "target_not_found", "message": target_heading_text}],
|
|
198
|
+
}
|
|
199
|
+
result = add_page_crossref(
|
|
200
|
+
doc, paragraphs[paragraph_index], target, cached_page=cached_page
|
|
201
|
+
)
|
|
202
|
+
if dry_run:
|
|
203
|
+
return _with_dry_run_verification(
|
|
204
|
+
{"ok": True, "dryRun": True, **result}, doc, path
|
|
205
|
+
)
|
|
206
|
+
verification = _save_doc_verification(doc, path)
|
|
207
|
+
return _with_save_verification({"ok": True, **result}, verification)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def verify_toc(
|
|
211
|
+
filename: str,
|
|
212
|
+
refresh: bool = False,
|
|
213
|
+
verify_render: bool = False,
|
|
214
|
+
) -> dict:
|
|
215
|
+
"""네이티브 차례/상호참조의 캐시 쪽번호를 검증합니다.
|
|
216
|
+
|
|
217
|
+
verify_render=True면 실제 한컴 렌더로 캐시 vs 실제 페이지를 대조해
|
|
218
|
+
toc_correctness_ratio를 산출합니다(오라클 없으면 정직하게 unverified).
|
|
219
|
+
refresh=True면 검증 전에 한컴을 열어 dirty 필드를 재계산·저장합니다
|
|
220
|
+
(macOS GUI 오라클 필요; dirty 재생성 직후 같은 세션 렌더는 이 한컴
|
|
221
|
+
빌드가 크래시하므로 refresh와 render는 별도 세션으로 수행됩니다)."""
|
|
222
|
+
from hwpx.tools.toc_fidelity import structural_report, toc_verify
|
|
223
|
+
|
|
224
|
+
path = resolve_path(filename)
|
|
225
|
+
guard = _toc_format_guard(filename, path)
|
|
226
|
+
if guard is not None:
|
|
227
|
+
return guard
|
|
228
|
+
|
|
229
|
+
oracle = None
|
|
230
|
+
refreshed = None
|
|
231
|
+
if refresh or verify_render:
|
|
232
|
+
try:
|
|
233
|
+
from ..office.rendering import resolve_hancom_oracle
|
|
234
|
+
|
|
235
|
+
oracle = resolve_hancom_oracle()
|
|
236
|
+
except Exception: # pragma: no cover - oracle stack unavailable
|
|
237
|
+
oracle = None
|
|
238
|
+
if refresh:
|
|
239
|
+
refresher = getattr(oracle, "refresh_document", None)
|
|
240
|
+
refreshed = bool(refresher and refresher(path))
|
|
241
|
+
|
|
242
|
+
if verify_render and oracle is not None and oracle.available():
|
|
243
|
+
# Core matches headings but does not read PDFs; the imaging stack is
|
|
244
|
+
# this layer's, so the extractor is injected alongside the oracle.
|
|
245
|
+
from ..office.form_fill.fit.wordbox import extract_word_boxes
|
|
246
|
+
|
|
247
|
+
report = toc_verify(path, oracle=oracle, extract=extract_word_boxes)
|
|
248
|
+
else:
|
|
249
|
+
structural = structural_report(path)
|
|
250
|
+
report = {
|
|
251
|
+
"structural": structural,
|
|
252
|
+
"render_checked": False,
|
|
253
|
+
"toc_correctness_ratio": None,
|
|
254
|
+
"stale_entries": [],
|
|
255
|
+
"crossref_correctness_ratio": None,
|
|
256
|
+
"verdict": (
|
|
257
|
+
"stale_detected_structurally"
|
|
258
|
+
if not structural["internally_consistent"]
|
|
259
|
+
else "unverified"
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
if refreshed is not None:
|
|
263
|
+
report["refreshed"] = refreshed
|
|
264
|
+
report["ok"] = report["verdict"] in ("verified", "unverified")
|
|
265
|
+
return report
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def set_paragraph_format(
|
|
269
|
+
filename: str,
|
|
270
|
+
paragraph_index: int | None = None,
|
|
271
|
+
paragraph_indexes: list[int] | None = None,
|
|
272
|
+
alignment: str | None = None,
|
|
273
|
+
line_spacing_percent: float | None = None,
|
|
274
|
+
indent_left_mm: float | None = None,
|
|
275
|
+
indent_right_mm: float | None = None,
|
|
276
|
+
first_line_indent_mm: float | None = None,
|
|
277
|
+
spacing_before_pt: float | None = None,
|
|
278
|
+
spacing_after_pt: float | None = None,
|
|
279
|
+
outline_level: int | None = None,
|
|
280
|
+
keep_with_next: bool | None = None,
|
|
281
|
+
keep_lines: bool | None = None,
|
|
282
|
+
page_break_before: bool | None = None,
|
|
283
|
+
dry_run: bool = False,
|
|
284
|
+
expected_revision: str = None,
|
|
285
|
+
) -> dict:
|
|
286
|
+
"""기존 문단의 정렬, 줄간격(%), 들여쓰기(mm), 문단 간격(pt), 개요 수준을 변경합니다.
|
|
287
|
+
|
|
288
|
+
keep_with_next/keep_lines/page_break_before로 문단의 keep-together(다음 문단과
|
|
289
|
+
붙임·문단 줄바꿈 금지·앞에서 쪽 나눔) 플래그를 설정합니다 — 시험지 조판 등에서
|
|
290
|
+
한 문항이 단/쪽 경계에서 잘리지 않게 할 때 씁니다.
|
|
291
|
+
"""
|
|
292
|
+
path = resolve_path(filename)
|
|
293
|
+
guard = _revision_guard(path, expected_revision)
|
|
294
|
+
if guard is not None:
|
|
295
|
+
return guard
|
|
296
|
+
doc = open_doc(path)
|
|
297
|
+
result = doc.set_paragraph_format(
|
|
298
|
+
paragraph_index=paragraph_index,
|
|
299
|
+
paragraph_indexes=paragraph_indexes,
|
|
300
|
+
alignment=alignment,
|
|
301
|
+
line_spacing_percent=line_spacing_percent,
|
|
302
|
+
indent_left_mm=indent_left_mm,
|
|
303
|
+
indent_right_mm=indent_right_mm,
|
|
304
|
+
first_line_indent_mm=first_line_indent_mm,
|
|
305
|
+
spacing_before_pt=spacing_before_pt,
|
|
306
|
+
spacing_after_pt=spacing_after_pt,
|
|
307
|
+
outline_level=outline_level,
|
|
308
|
+
keep_with_next=keep_with_next,
|
|
309
|
+
keep_lines=keep_lines,
|
|
310
|
+
page_break_before=page_break_before,
|
|
311
|
+
)
|
|
312
|
+
result["filename"] = filename
|
|
313
|
+
if dry_run:
|
|
314
|
+
return _with_dry_run_verification(result, doc, path)
|
|
315
|
+
verification = _save_doc_verification(doc, path)
|
|
316
|
+
return _with_save_verification(result, verification)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def set_page_setup(
|
|
320
|
+
filename: str,
|
|
321
|
+
paper_size: str | None = None,
|
|
322
|
+
width_mm: float | None = None,
|
|
323
|
+
height_mm: float | None = None,
|
|
324
|
+
orientation: str | None = None,
|
|
325
|
+
margins_mm: dict[str, float] | None = None,
|
|
326
|
+
margin_left_mm: float | None = None,
|
|
327
|
+
margin_right_mm: float | None = None,
|
|
328
|
+
margin_top_mm: float | None = None,
|
|
329
|
+
margin_bottom_mm: float | None = None,
|
|
330
|
+
header_margin_mm: float | None = None,
|
|
331
|
+
footer_margin_mm: float | None = None,
|
|
332
|
+
gutter_mm: float | None = None,
|
|
333
|
+
columns: int | None = None,
|
|
334
|
+
column_gap_mm: float | None = None,
|
|
335
|
+
section_index: int | None = None,
|
|
336
|
+
dry_run: bool = False,
|
|
337
|
+
expected_revision: str = None,
|
|
338
|
+
) -> dict:
|
|
339
|
+
"""용지/방향/여백을 mm 단위로 설정하고 선택적으로 단 구성을 적용합니다."""
|
|
340
|
+
path = resolve_path(filename)
|
|
341
|
+
guard = _revision_guard(path, expected_revision)
|
|
342
|
+
if guard is not None:
|
|
343
|
+
return guard
|
|
344
|
+
doc = open_doc(path)
|
|
345
|
+
result = doc.set_page_setup(
|
|
346
|
+
paper_size=paper_size,
|
|
347
|
+
width_mm=width_mm,
|
|
348
|
+
height_mm=height_mm,
|
|
349
|
+
orientation=orientation,
|
|
350
|
+
margins_mm=margins_mm,
|
|
351
|
+
margin_left_mm=margin_left_mm,
|
|
352
|
+
margin_right_mm=margin_right_mm,
|
|
353
|
+
margin_top_mm=margin_top_mm,
|
|
354
|
+
margin_bottom_mm=margin_bottom_mm,
|
|
355
|
+
header_margin_mm=header_margin_mm,
|
|
356
|
+
footer_margin_mm=footer_margin_mm,
|
|
357
|
+
gutter_mm=gutter_mm,
|
|
358
|
+
columns=columns,
|
|
359
|
+
column_gap_mm=column_gap_mm,
|
|
360
|
+
section_index=section_index,
|
|
361
|
+
)
|
|
362
|
+
result["filename"] = filename
|
|
363
|
+
if dry_run:
|
|
364
|
+
return _with_dry_run_verification(result, doc, path)
|
|
365
|
+
verification = _save_doc_verification(doc, path)
|
|
366
|
+
return _with_save_verification(result, verification)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _header_footer_payload(
|
|
370
|
+
wrapper: Any, *, kind: str, page_type: str
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
element = getattr(wrapper, "element", None)
|
|
373
|
+
page_number_count = 0
|
|
374
|
+
if element is not None and hasattr(element, "iter"):
|
|
375
|
+
page_number_count = sum(1 for _ in element.iter(f"{HP_NS}pageNum"))
|
|
376
|
+
return {
|
|
377
|
+
"kind": kind,
|
|
378
|
+
"pageType": page_type,
|
|
379
|
+
"id": getattr(wrapper, "id", None),
|
|
380
|
+
"text": getattr(wrapper, "text", ""),
|
|
381
|
+
"pageNumberCount": page_number_count,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def set_header_footer(
|
|
386
|
+
filename: str,
|
|
387
|
+
kind: str,
|
|
388
|
+
text: str | None = None,
|
|
389
|
+
content: list[dict[str, Any]] | None = None,
|
|
390
|
+
section_index: int | None = None,
|
|
391
|
+
page_type: str = "BOTH",
|
|
392
|
+
dry_run: bool = False,
|
|
393
|
+
expected_revision: str = None,
|
|
394
|
+
) -> dict:
|
|
395
|
+
"""머리글 또는 바닥글을 텍스트나 rich content spec으로 추가/수정합니다."""
|
|
396
|
+
path = resolve_path(filename)
|
|
397
|
+
guard = _revision_guard(path, expected_revision)
|
|
398
|
+
if guard is not None:
|
|
399
|
+
return guard
|
|
400
|
+
doc = open_doc(path)
|
|
401
|
+
wrapper = doc.set_header_footer(
|
|
402
|
+
kind=kind,
|
|
403
|
+
text=text,
|
|
404
|
+
content=content,
|
|
405
|
+
section_index=section_index,
|
|
406
|
+
page_type=page_type,
|
|
407
|
+
)
|
|
408
|
+
result = {
|
|
409
|
+
"filename": filename,
|
|
410
|
+
"headerFooter": _header_footer_payload(wrapper, kind=kind, page_type=page_type),
|
|
411
|
+
}
|
|
412
|
+
if dry_run:
|
|
413
|
+
return _with_dry_run_verification(result, doc, path)
|
|
414
|
+
verification = _save_doc_verification(doc, path)
|
|
415
|
+
return _with_save_verification(result, verification)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def set_page_number(
|
|
419
|
+
filename: str,
|
|
420
|
+
target: str = "footer",
|
|
421
|
+
page_type: str = "BOTH",
|
|
422
|
+
format: str = "page",
|
|
423
|
+
align: str = "CENTER",
|
|
424
|
+
position: str = "BOTTOM_CENTER",
|
|
425
|
+
prefix: str = "",
|
|
426
|
+
suffix: str = "",
|
|
427
|
+
format_type: str | None = None,
|
|
428
|
+
section_index: int | None = None,
|
|
429
|
+
dry_run: bool = False,
|
|
430
|
+
expected_revision: str = None,
|
|
431
|
+
) -> dict:
|
|
432
|
+
"""머리글/바닥글에 자동 쪽번호 필드를 추가하거나 수정합니다."""
|
|
433
|
+
path = resolve_path(filename)
|
|
434
|
+
guard = _revision_guard(path, expected_revision)
|
|
435
|
+
if guard is not None:
|
|
436
|
+
return guard
|
|
437
|
+
doc = open_doc(path)
|
|
438
|
+
wrapper = doc.set_page_number(
|
|
439
|
+
target=target,
|
|
440
|
+
page_type=page_type,
|
|
441
|
+
format=format,
|
|
442
|
+
align=align,
|
|
443
|
+
position=position,
|
|
444
|
+
prefix=prefix,
|
|
445
|
+
suffix=suffix,
|
|
446
|
+
format_type=format_type,
|
|
447
|
+
section_index=section_index,
|
|
448
|
+
)
|
|
449
|
+
result = {
|
|
450
|
+
"filename": filename,
|
|
451
|
+
"target": target,
|
|
452
|
+
"format": format,
|
|
453
|
+
"headerFooter": _header_footer_payload(
|
|
454
|
+
wrapper, kind=target, page_type=page_type
|
|
455
|
+
),
|
|
456
|
+
}
|
|
457
|
+
if dry_run:
|
|
458
|
+
return _with_dry_run_verification(result, doc, path)
|
|
459
|
+
verification = _save_doc_verification(doc, path)
|
|
460
|
+
return _with_save_verification(result, verification)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def set_list_format(
|
|
464
|
+
filename: str,
|
|
465
|
+
paragraph_index: int | None = None,
|
|
466
|
+
paragraph_indexes: list[int] | None = None,
|
|
467
|
+
kind: str = "bullet",
|
|
468
|
+
level: int = 1,
|
|
469
|
+
bullet_char: str | None = None,
|
|
470
|
+
number_format: str | None = None,
|
|
471
|
+
start: int | None = None,
|
|
472
|
+
dry_run: bool = False,
|
|
473
|
+
expected_revision: str = None,
|
|
474
|
+
) -> dict:
|
|
475
|
+
"""기존 문단에 불릿 또는 번호 목록 서식을 적용합니다."""
|
|
476
|
+
path = resolve_path(filename)
|
|
477
|
+
guard = _revision_guard(path, expected_revision)
|
|
478
|
+
if guard is not None:
|
|
479
|
+
return guard
|
|
480
|
+
doc = open_doc(path)
|
|
481
|
+
result = doc.set_list_format(
|
|
482
|
+
paragraph_index=paragraph_index,
|
|
483
|
+
paragraph_indexes=paragraph_indexes,
|
|
484
|
+
kind=kind,
|
|
485
|
+
level=level,
|
|
486
|
+
bullet_char=bullet_char,
|
|
487
|
+
number_format=number_format,
|
|
488
|
+
start=start,
|
|
489
|
+
)
|
|
490
|
+
result["filename"] = filename
|
|
491
|
+
if dry_run:
|
|
492
|
+
return _with_dry_run_verification(result, doc, path)
|
|
493
|
+
verification = _save_doc_verification(doc, path)
|
|
494
|
+
return _with_save_verification(result, verification)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def add_memo(
|
|
498
|
+
filename: str,
|
|
499
|
+
paragraph_index: int | None = None,
|
|
500
|
+
text: str = "",
|
|
501
|
+
location: dict[str, Any] | None = None,
|
|
502
|
+
dry_run: bool = False,
|
|
503
|
+
expected_revision: str = None,
|
|
504
|
+
) -> dict:
|
|
505
|
+
"""본문 문단 또는 표 셀 문단에 메모를 추가합니다. dry_run=True이면 원본을 저장하지 않습니다."""
|
|
506
|
+
path = resolve_path(filename)
|
|
507
|
+
guard = _revision_guard(path, expected_revision)
|
|
508
|
+
if guard is not None:
|
|
509
|
+
return guard
|
|
510
|
+
doc = open_doc(path)
|
|
511
|
+
result = add_memo_to_doc(doc, paragraph_index, text, location=location)
|
|
512
|
+
if result["location"].get("kind") == "body_paragraph":
|
|
513
|
+
result["paragraph_index"] = result["location"]["paragraph_index"]
|
|
514
|
+
if dry_run:
|
|
515
|
+
return _with_dry_run_verification(result, doc, path)
|
|
516
|
+
verification = _save_doc_verification(doc, path)
|
|
517
|
+
return _with_save_verification(result, verification)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def add_memo_by_anchor(
|
|
521
|
+
filename: str,
|
|
522
|
+
anchor: dict[str, Any] | str,
|
|
523
|
+
text: str,
|
|
524
|
+
dry_run: bool = False,
|
|
525
|
+
expected_revision: str = None,
|
|
526
|
+
) -> dict:
|
|
527
|
+
"""find_text가 반환한 anchor로 메모 위치를 지정해 메모를 추가합니다."""
|
|
528
|
+
return add_memo(
|
|
529
|
+
filename,
|
|
530
|
+
text=text,
|
|
531
|
+
location=location_from_anchor(anchor),
|
|
532
|
+
dry_run=dry_run,
|
|
533
|
+
expected_revision=expected_revision,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def remove_memo(
|
|
538
|
+
filename: str,
|
|
539
|
+
paragraph_index: int | None = None,
|
|
540
|
+
location: dict[str, Any] | None = None,
|
|
541
|
+
dry_run: bool = False,
|
|
542
|
+
expected_revision: str = None,
|
|
543
|
+
) -> dict:
|
|
544
|
+
"""본문 문단 또는 표 셀 문단의 메모를 제거합니다. dry_run=True이면 원본을 저장하지 않습니다."""
|
|
545
|
+
path = resolve_path(filename)
|
|
546
|
+
guard = _revision_guard(path, expected_revision)
|
|
547
|
+
if guard is not None:
|
|
548
|
+
return guard
|
|
549
|
+
doc = open_doc(path)
|
|
550
|
+
result = remove_memo_from_doc(doc, paragraph_index, location=location)
|
|
551
|
+
if result["location"].get("kind") == "body_paragraph":
|
|
552
|
+
result["paragraph_index"] = result["location"]["paragraph_index"]
|
|
553
|
+
if dry_run:
|
|
554
|
+
return _with_dry_run_verification(result, doc, path)
|
|
555
|
+
verification = _save_doc_verification(doc, path)
|
|
556
|
+
return _with_save_verification(result, verification)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def format_text(
|
|
560
|
+
filename: str,
|
|
561
|
+
paragraph_index: int,
|
|
562
|
+
start_pos: int,
|
|
563
|
+
end_pos: int,
|
|
564
|
+
bold: bool = None,
|
|
565
|
+
italic: bool = None,
|
|
566
|
+
underline: bool = None,
|
|
567
|
+
font_size: float = None,
|
|
568
|
+
font_name: str = None,
|
|
569
|
+
color: str = None,
|
|
570
|
+
dry_run: bool = False,
|
|
571
|
+
expected_revision: str = None,
|
|
572
|
+
) -> dict:
|
|
573
|
+
"""지정 범위 텍스트 서식을 변경합니다. dry_run=True이면 원본을 저장하지 않습니다."""
|
|
574
|
+
path = resolve_path(filename)
|
|
575
|
+
guard = _revision_guard(path, expected_revision)
|
|
576
|
+
if guard is not None:
|
|
577
|
+
return guard
|
|
578
|
+
doc = open_doc(path)
|
|
579
|
+
format_text_range(
|
|
580
|
+
doc,
|
|
581
|
+
paragraph_index,
|
|
582
|
+
start_pos,
|
|
583
|
+
end_pos,
|
|
584
|
+
bold=bold,
|
|
585
|
+
italic=italic,
|
|
586
|
+
underline=underline,
|
|
587
|
+
font_size=font_size,
|
|
588
|
+
font_name=font_name,
|
|
589
|
+
color=color,
|
|
590
|
+
)
|
|
591
|
+
result = {
|
|
592
|
+
"formatted": True,
|
|
593
|
+
"paragraph_index": paragraph_index,
|
|
594
|
+
"range": [start_pos, end_pos],
|
|
595
|
+
}
|
|
596
|
+
if dry_run:
|
|
597
|
+
return _with_dry_run_verification(result, doc, path)
|
|
598
|
+
verification = _save_doc_verification(doc, path)
|
|
599
|
+
return _with_save_verification(result, verification)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def create_custom_style(
|
|
603
|
+
filename: str,
|
|
604
|
+
style_name: str,
|
|
605
|
+
bold: bool = None,
|
|
606
|
+
italic: bool = None,
|
|
607
|
+
font_size: float = None,
|
|
608
|
+
font_name: str = None,
|
|
609
|
+
color: str = None,
|
|
610
|
+
dry_run: bool = False,
|
|
611
|
+
expected_revision: str = None,
|
|
612
|
+
) -> dict:
|
|
613
|
+
"""문서에 커스텀 스타일을 생성합니다. dry_run=True이면 원본을 저장하지 않습니다."""
|
|
614
|
+
path = resolve_path(filename)
|
|
615
|
+
guard = _revision_guard(path, expected_revision)
|
|
616
|
+
if guard is not None:
|
|
617
|
+
return guard
|
|
618
|
+
doc = open_doc(path)
|
|
619
|
+
result = create_style_in_doc(
|
|
620
|
+
doc,
|
|
621
|
+
style_name,
|
|
622
|
+
bold=bold,
|
|
623
|
+
italic=italic,
|
|
624
|
+
font_size=font_size,
|
|
625
|
+
font_name=font_name,
|
|
626
|
+
color=color,
|
|
627
|
+
)
|
|
628
|
+
if dry_run:
|
|
629
|
+
return _with_dry_run_verification(result, doc, path)
|
|
630
|
+
verification = _save_doc_verification(doc, path)
|
|
631
|
+
return _with_save_verification(result, verification)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def list_styles(filename: str) -> dict:
|
|
635
|
+
"""문서에 정의된 스타일 목록을 조회합니다."""
|
|
636
|
+
path = resolve_path(filename)
|
|
637
|
+
doc = open_doc(path)
|
|
638
|
+
styles = list_styles_in_doc(doc)
|
|
639
|
+
return {"styles": styles, "count": len(styles)}
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
__all__ = [
|
|
643
|
+
"list_styles",
|
|
644
|
+
"create_custom_style",
|
|
645
|
+
"set_paragraph_format",
|
|
646
|
+
"set_list_format",
|
|
647
|
+
"format_text",
|
|
648
|
+
"extract_style_profile",
|
|
649
|
+
"apply_style_profile_to_plan",
|
|
650
|
+
"compare_style_profiles",
|
|
651
|
+
"set_page_setup",
|
|
652
|
+
"set_header_footer",
|
|
653
|
+
"set_page_number",
|
|
654
|
+
"add_toc",
|
|
655
|
+
"add_cross_reference",
|
|
656
|
+
"verify_toc",
|
|
657
|
+
"add_memo",
|
|
658
|
+
"add_memo_by_anchor",
|
|
659
|
+
"remove_memo",
|
|
660
|
+
]
|