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,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
|