python-hwpx-automation 6.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Search and context utilities for hardened tools."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Callable, Iterable, List, Sequence
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SearchHit:
13
+ node_id: str
14
+ paragraph_index: int
15
+ start: int
16
+ end: int
17
+ match: str
18
+ context: str
19
+
20
+
21
+ def _iter_substring_matches(text: str, pattern: str) -> Iterable[tuple[int, int, str]]:
22
+ start = 0
23
+ length = len(pattern)
24
+ if length == 0:
25
+ return
26
+ while True:
27
+ index = text.find(pattern, start)
28
+ if index == -1:
29
+ break
30
+ yield index, index + length, pattern
31
+ start = index + length
32
+
33
+
34
+ def search_paragraphs(
35
+ paragraphs: Sequence[str],
36
+ *,
37
+ pattern: str,
38
+ limit: int,
39
+ use_regex: bool,
40
+ node_resolver: Callable[[int], str],
41
+ context_radius: int = 20,
42
+ ) -> List[SearchHit]:
43
+ hits: List[SearchHit] = []
44
+ if not pattern:
45
+ return hits
46
+
47
+ matcher = None
48
+ if use_regex:
49
+ matcher = re.compile(pattern)
50
+
51
+ for index, paragraph in enumerate(paragraphs):
52
+ matches: Iterable[tuple[int, int, str]]
53
+ if matcher is not None:
54
+ matches = ((m.start(), m.end(), m.group(0)) for m in matcher.finditer(paragraph))
55
+ else:
56
+ matches = _iter_substring_matches(paragraph, pattern)
57
+ for start, end, match_text in matches:
58
+ context = paragraph[max(0, start - context_radius) : min(len(paragraph), end + context_radius)]
59
+ hits.append(
60
+ SearchHit(
61
+ node_id=node_resolver(index),
62
+ paragraph_index=index,
63
+ start=start,
64
+ end=end,
65
+ match=match_text,
66
+ context=context,
67
+ )
68
+ )
69
+ if len(hits) >= limit:
70
+ return hits
71
+ return hits
72
+
73
+
74
+ def window_for_paragraph(
75
+ paragraphs: Sequence[str],
76
+ index: int,
77
+ *,
78
+ radius: int,
79
+ ) -> dict:
80
+ radius = max(1, min(radius, 3))
81
+ focus = paragraphs[index] if 0 <= index < len(paragraphs) else ""
82
+ before = list(paragraphs[max(0, index - radius) : index])
83
+ after = list(paragraphs[index + 1 : index + 1 + radius])
84
+ return {
85
+ "before": before,
86
+ "focus": focus,
87
+ "after": after,
88
+ }
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class SessionLifecyclePolicy:
93
+ """문서 handle 레지스트리 수명 정책."""
94
+
95
+ registry_scope: str = "process"
96
+ request_scope: str = "request"
97
+ eviction: tuple[str, ...] = (
98
+ "close_document_handle 호출 시",
99
+ "프로세스 종료 시",
100
+ )
101
+
102
+ def as_dict(self) -> dict:
103
+ return {
104
+ "registryScope": self.registry_scope,
105
+ "requestScope": self.request_scope,
106
+ "eviction": list(self.eviction),
107
+ }
108
+
109
+
110
+ def default_session_lifecycle_policy() -> SessionLifecyclePolicy:
111
+ return SessionLifecyclePolicy()
@@ -0,0 +1,53 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Lightweight helpers for representing logical diffs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Dict, List, Sequence
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ParagraphMatch:
12
+ """Capture a single pattern match inside a paragraph."""
13
+
14
+ paragraph_index: int
15
+ start: int
16
+ end: int
17
+ text: str
18
+
19
+ def context(self, paragraph: str, radius: int = 20) -> str:
20
+ left = max(self.start - radius, 0)
21
+ right = min(self.end + radius, len(paragraph))
22
+ snippet = paragraph[left:right]
23
+ return snippet
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ParagraphDiff:
28
+ """A replace operation scoped to a paragraph."""
29
+
30
+ paragraph_index: int
31
+ start: int
32
+ end: int
33
+ before: str
34
+ after: str
35
+
36
+ def apply(self, paragraphs: Sequence[str]) -> List[str]:
37
+ updated = list(paragraphs)
38
+ if self.paragraph_index >= len(updated):
39
+ raise ValueError("paragraph index out of range")
40
+ original = updated[self.paragraph_index]
41
+ if original[self.start : self.end] != self.before:
42
+ raise ValueError("paragraph content changed before apply")
43
+ updated[self.paragraph_index] = (
44
+ original[: self.start] + self.after + original[self.end :]
45
+ )
46
+ return updated
47
+
48
+ def as_preview(self) -> Dict[str, object]:
49
+ return {
50
+ "paragraphIndex": self.paragraph_index,
51
+ "before": self.before,
52
+ "after": self.after,
53
+ }
@@ -0,0 +1,37 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import os
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from ..configuration import env_value
8
+ from ..storage import LocalDocumentStorage, require_hwpx_editor_open_safe
9
+ from ..upstream import HwpxDocument, blank_document_template_bytes, open_document
10
+
11
+
12
+ def _local_storage() -> LocalDocumentStorage:
13
+ return LocalDocumentStorage(
14
+ auto_backup=env_value("AUTOBACKUP", "1") == "1",
15
+ )
16
+
17
+
18
+ def open_doc(path: str) -> HwpxDocument:
19
+ if not os.path.exists(path):
20
+ raise FileNotFoundError(f"파일을 찾을 수 없습니다: {path}")
21
+ require_hwpx_editor_open_safe(Path(path), role="local HWPX open")
22
+ return open_document(path)
23
+
24
+
25
+ def save_doc(doc: HwpxDocument, path: str, *, quality: Any = None) -> dict[str, Any]:
26
+ storage = _local_storage()
27
+ target = storage.resolve_output_path(path)
28
+ return storage.save_document(doc, target, quality=quality)
29
+
30
+
31
+ def create_blank(path: str, title=None, author=None) -> dict[str, Any]:
32
+ source = BytesIO(blank_document_template_bytes())
33
+ doc = open_document(source)
34
+ try:
35
+ return save_doc(doc, path)
36
+ finally:
37
+ doc.close()
@@ -0,0 +1,513 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Text formatting helpers backed by python-hwpx character/style definitions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from copy import deepcopy
7
+ from typing import Any
8
+ from xml.etree import ElementTree as ET
9
+
10
+ from ..upstream import (
11
+ HH_NS,
12
+ HP_NS,
13
+ HwpxDocument,
14
+ HwpxOxmlParagraph,
15
+ HwpxOxmlRun,
16
+ append_xml_child,
17
+ char_style_matches,
18
+ default_base_style_id,
19
+ document_styles_element,
20
+ element_font_refs,
21
+ element_style_flags,
22
+ ensure_char_style,
23
+ ensure_font_face_refs,
24
+ iter_unique_styles,
25
+ next_numeric_style_id,
26
+ normalize_font_size,
27
+ normalize_hex_color,
28
+ resolve_style_id as upstream_resolve_style_id,
29
+ run_style_flags,
30
+ run_style_font_refs,
31
+ run_style_height,
32
+ style_element_by_id,
33
+ update_styles_item_count,
34
+ )
35
+
36
+ _FONT_REF_KEYS = (
37
+ "hangul",
38
+ "latin",
39
+ "hanja",
40
+ "japanese",
41
+ "other",
42
+ "symbol",
43
+ "user",
44
+ )
45
+ _FONT_FACE_LANGS = {
46
+ "HANGUL": "hangul",
47
+ "LATIN": "latin",
48
+ "HANJA": "hanja",
49
+ "JAPANESE": "japanese",
50
+ "OTHER": "other",
51
+ "SYMBOL": "symbol",
52
+ "USER": "user",
53
+ }
54
+ _DEFAULT_TEXT_COLOR = "#000000"
55
+ _DEFAULT_CHAR_HEIGHT = "1000"
56
+
57
+
58
+ def _append_child(parent: ET.Element, tag: str, attrs: dict[str, str] | None = None) -> ET.Element:
59
+ return append_xml_child(parent, tag, attrs)
60
+
61
+
62
+ def _iter_unique_styles(doc: HwpxDocument):
63
+ yield from iter_unique_styles(doc)
64
+
65
+
66
+ def _normalize_color(color: str | None) -> str | None:
67
+ return normalize_hex_color(color)
68
+
69
+
70
+ def _normalize_font_size(font_size: float | None) -> str | None:
71
+ return normalize_font_size(font_size)
72
+
73
+
74
+ def _run_style_flags(run_style: Any) -> tuple[bool, bool, bool]:
75
+ return run_style_flags(run_style)
76
+
77
+
78
+ def _run_style_height(run_style: Any) -> str | None:
79
+ return run_style_height(run_style)
80
+
81
+
82
+ def _run_style_font_refs(run_style: Any) -> dict[str, str]:
83
+ return run_style_font_refs(run_style)
84
+
85
+
86
+ def _element_flags(element: ET.Element) -> tuple[bool, bool, bool]:
87
+ return element_style_flags(element)
88
+
89
+
90
+ def _element_font_refs(element: ET.Element) -> dict[str, str]:
91
+ return element_font_refs(element)
92
+
93
+
94
+ def _char_style_matches(
95
+ run_style: Any,
96
+ *,
97
+ flags: tuple[bool, bool, bool],
98
+ color: str,
99
+ height: str,
100
+ font_refs: dict[str, str],
101
+ ) -> bool:
102
+ return char_style_matches(run_style, flags=flags, color=color, height=height, font_refs=font_refs)
103
+
104
+
105
+ def _ref_list_element(header: Any) -> ET.Element:
106
+ element = header.element.find(f"{HH_NS}refList")
107
+ if element is None:
108
+ raise RuntimeError("document header does not expose a refList element")
109
+ return element
110
+
111
+
112
+ def _fontfaces_element(header: Any) -> ET.Element:
113
+ ref_list = _ref_list_element(header)
114
+ fontfaces = ref_list.find(f"{HH_NS}fontfaces")
115
+ if fontfaces is None:
116
+ raise RuntimeError("document header does not expose fontfaces")
117
+ return fontfaces
118
+
119
+
120
+ def _find_fontface_bucket(fontfaces: ET.Element, lang: str) -> ET.Element | None:
121
+ for bucket in fontfaces.findall(f"{HH_NS}fontface"):
122
+ if (bucket.get("lang") or "").upper() == lang:
123
+ return bucket
124
+ return None
125
+
126
+
127
+ def _next_font_id(bucket: ET.Element) -> str:
128
+ numeric_ids: list[int] = []
129
+ for child in bucket.findall(f"{HH_NS}font"):
130
+ raw_id = child.get("id")
131
+ if raw_id is None:
132
+ continue
133
+ try:
134
+ numeric_ids.append(int(raw_id))
135
+ except ValueError:
136
+ continue
137
+ return "0" if not numeric_ids else str(max(numeric_ids) + 1)
138
+
139
+
140
+ def _ensure_font_face_refs(header: Any, font_name: str) -> dict[str, str]:
141
+ return ensure_font_face_refs(header, font_name)
142
+
143
+
144
+ def resolve_style_id(doc: HwpxDocument, style: str | None) -> str | None:
145
+ return upstream_resolve_style_id(doc, style)
146
+
147
+
148
+ def _ensure_char_style(
149
+ doc: HwpxDocument,
150
+ *,
151
+ base_char_pr_id: str | int | None,
152
+ bold: bool | None = None,
153
+ italic: bool | None = None,
154
+ underline: bool | None = None,
155
+ font_size: float | None = None,
156
+ font_name: str | None = None,
157
+ color: str | None = None,
158
+ ) -> str:
159
+ return ensure_char_style(
160
+ doc,
161
+ base_char_pr_id=base_char_pr_id,
162
+ bold=bold,
163
+ italic=italic,
164
+ underline=underline,
165
+ font_size=font_size,
166
+ font_name=font_name,
167
+ color=color,
168
+ )
169
+
170
+
171
+ class _Segment:
172
+ __slots__ = ("element", "attr", "text")
173
+
174
+ def __init__(self, element: ET.Element, attr: str, text: str) -> None:
175
+ self.element = element
176
+ self.attr = attr
177
+ self.text = text
178
+
179
+ def set(self, value: str) -> None:
180
+ self.text = value
181
+ if value:
182
+ setattr(self.element, self.attr, value)
183
+ else:
184
+ setattr(self.element, self.attr, "")
185
+
186
+
187
+ def _gather_segments(run_element: ET.Element) -> list[_Segment]:
188
+ segments: list[_Segment] = []
189
+
190
+ def visit(node: ET.Element) -> None:
191
+ segments.append(_Segment(node, "text", node.text or ""))
192
+ for child in list(node):
193
+ visit(child)
194
+ segments.append(_Segment(child, "tail", child.tail or ""))
195
+
196
+ for text_node in run_element.findall(f"{HP_NS}t"):
197
+ visit(text_node)
198
+ return segments
199
+
200
+
201
+ def _slice_run(run_obj: HwpxOxmlRun, start: int, end: int) -> None:
202
+ segments = _gather_segments(run_obj.element)
203
+ if not segments:
204
+ return
205
+
206
+ total_length = sum(len(segment.text) for segment in segments)
207
+ start = max(0, min(start, total_length))
208
+ end = max(0, min(end, total_length))
209
+ if start >= end:
210
+ for segment in segments:
211
+ if segment.text:
212
+ segment.set("")
213
+ run_obj.paragraph.section.mark_dirty()
214
+ return
215
+
216
+ changed = False
217
+ offset = 0
218
+ for segment in segments:
219
+ seg_start = offset
220
+ seg_end = seg_start + len(segment.text)
221
+ offset = seg_end
222
+ if end <= seg_start or start >= seg_end:
223
+ if segment.text:
224
+ segment.set("")
225
+ changed = True
226
+ continue
227
+
228
+ local_start = max(start, seg_start) - seg_start
229
+ local_end = min(end, seg_end) - seg_start
230
+ new_value = segment.text[local_start:local_end]
231
+ if segment.text != new_value:
232
+ segment.set(new_value)
233
+ changed = True
234
+
235
+ if changed:
236
+ run_obj.paragraph.section.mark_dirty()
237
+
238
+
239
+ def _split_run(run_obj: HwpxOxmlRun, local_start: int, local_end: int, char_pr_id_ref: str) -> None:
240
+ text_value = run_obj.text or ""
241
+ length = len(text_value)
242
+ if length == 0:
243
+ return
244
+
245
+ local_start = max(0, min(local_start, length))
246
+ local_end = max(0, min(local_end, length))
247
+ if local_start >= local_end:
248
+ return
249
+
250
+ if local_start == 0 and local_end == length:
251
+ run_obj.char_pr_id_ref = char_pr_id_ref
252
+ return
253
+
254
+ segments: list[tuple[int, int, str | None]] = []
255
+ original_char = run_obj.char_pr_id_ref
256
+ if local_start > 0:
257
+ segments.append((0, local_start, original_char))
258
+ segments.append((local_start, local_end, char_pr_id_ref))
259
+ if local_end < length:
260
+ segments.append((local_end, length, original_char))
261
+
262
+ parent = run_obj.paragraph.element
263
+ run_children = list(parent)
264
+ try:
265
+ index = run_children.index(run_obj.element)
266
+ except ValueError:
267
+ return
268
+
269
+ new_elements: list[ET.Element] = []
270
+ for seg_start, seg_end, char_id in segments:
271
+ if seg_start >= seg_end:
272
+ continue
273
+ element_copy = deepcopy(run_obj.element)
274
+ segment_run = HwpxOxmlRun(element_copy, run_obj.paragraph)
275
+ _slice_run(segment_run, seg_start, seg_end)
276
+ segment_run.char_pr_id_ref = char_id
277
+ new_elements.append(element_copy)
278
+
279
+ if not new_elements:
280
+ parent.remove(run_obj.element)
281
+ run_obj.paragraph.section.mark_dirty()
282
+ return
283
+
284
+ for offset, element in enumerate(new_elements):
285
+ parent.insert(index + offset, element)
286
+ parent.remove(run_obj.element)
287
+ run_obj.paragraph.section.mark_dirty()
288
+
289
+
290
+ def _paragraph_length(paragraph: HwpxOxmlParagraph) -> int:
291
+ return sum(len(run.text or "") for run in paragraph.runs)
292
+
293
+
294
+ def _style_element_by_id(styles_element: ET.Element, style_id: str) -> ET.Element | None:
295
+ return style_element_by_id(styles_element, style_id)
296
+
297
+
298
+ def _next_style_id(styles_element: ET.Element) -> str:
299
+ return next_numeric_style_id(styles_element)
300
+
301
+
302
+ def _update_styles_item_count(styles_element: ET.Element) -> None:
303
+ update_styles_item_count(styles_element)
304
+
305
+
306
+ def _default_base_style_id(doc: HwpxDocument) -> str:
307
+ return default_base_style_id(doc)
308
+ for candidate in ("1", "본문", "Body", "0", "바탕글", "Normal"):
309
+ try:
310
+ resolved = resolve_style_id(doc, candidate)
311
+ except ValueError:
312
+ continue
313
+ if resolved is not None:
314
+ return resolved
315
+
316
+ for style in _iter_unique_styles(doc):
317
+ raw_id = getattr(style, "raw_id", None)
318
+ if raw_id:
319
+ return raw_id
320
+ if getattr(style, "id", None) is not None:
321
+ return str(style.id)
322
+
323
+ raise RuntimeError("document does not contain any styles")
324
+
325
+
326
+ def format_text_range(
327
+ doc: HwpxDocument,
328
+ paragraph_index: int,
329
+ start_pos: int,
330
+ end_pos: int,
331
+ bold: bool = None,
332
+ italic: bool = None,
333
+ underline: bool = None,
334
+ font_size: float = None,
335
+ font_name: str = None,
336
+ color: str = None,
337
+ ) -> None:
338
+ """Apply character formatting to a text span within a paragraph."""
339
+ if all(value is None for value in (bold, italic, underline, font_size, font_name, color)):
340
+ return
341
+ if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
342
+ raise ValueError(f"invalid paragraph_index: {paragraph_index}")
343
+ if start_pos < 0 or end_pos < 0 or end_pos < start_pos:
344
+ raise ValueError("start_pos/end_pos are out of range")
345
+ if start_pos == end_pos:
346
+ return
347
+
348
+ paragraph = doc.paragraphs[paragraph_index]
349
+ total_length = _paragraph_length(paragraph)
350
+ if total_length == 0 or start_pos >= total_length:
351
+ return
352
+ span_end = min(end_pos, total_length)
353
+
354
+ cursor = start_pos
355
+ while cursor < span_end:
356
+ runs = list(paragraph.runs)
357
+ offset = 0
358
+ target: tuple[HwpxOxmlRun, int, int, int] | None = None
359
+ for candidate in runs:
360
+ text = candidate.text or ""
361
+ length = len(text)
362
+ run_start = offset
363
+ run_end = run_start + length
364
+ offset = run_end
365
+ if length == 0 or run_end <= cursor:
366
+ continue
367
+ if run_start >= span_end:
368
+ break
369
+ target = (candidate, run_start, run_end, length)
370
+ break
371
+
372
+ if target is None:
373
+ break
374
+
375
+ run_obj, run_start, run_end, length = target
376
+ local_start = max(0, cursor - run_start)
377
+ local_end = min(length, span_end - run_start)
378
+ if local_start >= local_end:
379
+ cursor = max(cursor + 1, run_end)
380
+ continue
381
+
382
+ char_id = _ensure_char_style(
383
+ doc,
384
+ base_char_pr_id=run_obj.char_pr_id_ref,
385
+ bold=bold,
386
+ italic=italic,
387
+ underline=underline,
388
+ font_size=font_size,
389
+ font_name=font_name,
390
+ color=color,
391
+ )
392
+ _split_run(run_obj, local_start, local_end, char_id)
393
+ cursor = min(span_end, run_end)
394
+
395
+
396
+ def create_style_in_doc(
397
+ doc: HwpxDocument,
398
+ style_name: str,
399
+ bold: bool = None,
400
+ italic: bool = None,
401
+ font_size: float = None,
402
+ font_name: str = None,
403
+ color: str = None,
404
+ ) -> dict[str, Any]:
405
+ """Create a paragraph style backed by a stable upstream charPr definition."""
406
+ name = (style_name or "").strip()
407
+ if not name:
408
+ raise ValueError("style_name cannot be empty")
409
+ if not doc.headers:
410
+ raise RuntimeError("document does not contain any headers to host styles")
411
+
412
+ header = doc.headers[0]
413
+ styles_element = document_styles_element(doc)
414
+
415
+ for style in _iter_unique_styles(doc):
416
+ if name not in {style.name, style.eng_name}:
417
+ continue
418
+ style_id = getattr(style, "raw_id", None) or str(style.id)
419
+ char_ref = style.char_pr_id_ref
420
+ para_ref = style.para_pr_id_ref
421
+ return {
422
+ "style_id": style_id,
423
+ "style_name": style.name or name,
424
+ "created": False,
425
+ "char_pr_id_ref": None if char_ref is None else str(char_ref),
426
+ "para_pr_id_ref": None if para_ref is None else str(para_ref),
427
+ }
428
+
429
+ base_style_id = _default_base_style_id(doc)
430
+ base_style = doc.style(base_style_id)
431
+ if base_style is None:
432
+ raise RuntimeError(f"failed to resolve base style id: {base_style_id}")
433
+
434
+ base_style_element = _style_element_by_id(styles_element, base_style_id)
435
+ if base_style_element is None:
436
+ raise RuntimeError(f"failed to locate base style element: {base_style_id}")
437
+
438
+ base_char_ref = getattr(base_style, "char_pr_id_ref", None)
439
+ char_ref: str | None
440
+ if any(value is not None for value in (bold, italic, font_size, font_name, color)):
441
+ char_ref = _ensure_char_style(
442
+ doc,
443
+ base_char_pr_id=None if base_char_ref is None else str(base_char_ref),
444
+ bold=bold,
445
+ italic=italic,
446
+ underline=None,
447
+ font_size=font_size,
448
+ font_name=font_name,
449
+ color=color,
450
+ )
451
+ else:
452
+ char_ref = None if base_char_ref is None else str(base_char_ref)
453
+
454
+ style_id = _next_style_id(styles_element)
455
+ new_style = deepcopy(base_style_element)
456
+ new_style.set("id", style_id)
457
+ new_style.set("name", name)
458
+ new_style.set("engName", name)
459
+ new_style.set("nextStyleIDRef", style_id)
460
+ if char_ref is not None:
461
+ new_style.set("charPrIDRef", char_ref)
462
+ styles_element.append(new_style)
463
+ _update_styles_item_count(styles_element)
464
+ header.mark_dirty()
465
+
466
+ return {
467
+ "style_id": style_id,
468
+ "style_name": name,
469
+ "created": True,
470
+ "char_pr_id_ref": char_ref,
471
+ "para_pr_id_ref": new_style.get("paraPrIDRef"),
472
+ }
473
+
474
+
475
+ def list_styles_in_doc(doc: HwpxDocument) -> list[dict[str, str | None]]:
476
+ """Return styles defined in the document."""
477
+ styles_element = document_styles_element(doc)
478
+
479
+ styles: list[dict[str, str | None]] = []
480
+ for style_element in styles_element.findall(f"{HH_NS}style"):
481
+ styles.append(
482
+ {
483
+ "id": style_element.get("id"),
484
+ "name": style_element.get("name"),
485
+ "eng_name": style_element.get("engName"),
486
+ "type": style_element.get("type"),
487
+ "para_pr_id_ref": style_element.get("paraPrIDRef"),
488
+ "char_pr_id_ref": style_element.get("charPrIDRef"),
489
+ }
490
+ )
491
+ return styles
492
+
493
+
494
+ def outline_style_levels(doc: HwpxDocument) -> dict[str, int]:
495
+ """스타일 id → 개요 수준 매핑을 반환한다 ("개요 N"/"Outline N" 문단 스타일)."""
496
+ levels: dict[str, int] = {}
497
+ for style in list_styles_in_doc(doc):
498
+ if (style.get("type") or "").upper() == "CHAR":
499
+ continue
500
+ for label in (style.get("name") or "", style.get("eng_name") or ""):
501
+ for prefix in ("개요 ", "Outline "):
502
+ if label.startswith(prefix):
503
+ try:
504
+ level = int(label[len(prefix):].strip())
505
+ except ValueError:
506
+ continue
507
+ style_id = style.get("id")
508
+ if style_id is not None:
509
+ levels[str(style_id)] = min(6, max(1, level))
510
+ break
511
+ if str(style.get("id")) in levels:
512
+ break
513
+ return levels