python-hwpx 6.0.3__py3-none-any.whl → 6.2.1__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/_document/_legacy.py +16 -1
- hwpx/_document/highlight.py +164 -0
- hwpx/_document/layout.py +128 -11
- hwpx/_document/ns/__init__.py +1 -1
- hwpx/_document/ns/page.py +125 -1
- hwpx/_document/ns/parts.py +210 -4
- hwpx/_document/ns/shapes.py +182 -1
- hwpx/_document/ns/styles.py +269 -4
- hwpx/_document/ns/text.py +30 -1
- hwpx/_document/shapes.py +244 -1
- hwpx/capabilities.py +273 -2
- hwpx/data/contract_docs/support-matrix.md +56 -12
- hwpx/errors.py +59 -0
- hwpx/objects/__init__.py +2 -0
- hwpx/objects/highlight.py +41 -0
- hwpx/opc/package.py +123 -1
- hwpx/opc/relationships.py +6 -0
- hwpx/oxml/__init__.py +46 -2
- hwpx/oxml/_document_primitives.py +881 -12
- hwpx/oxml/body.py +488 -0
- hwpx/oxml/document_metadata.py +94 -0
- hwpx/oxml/document_parts.py +321 -106
- hwpx/oxml/drop_cap.py +218 -0
- hwpx/oxml/dutmal_compose.py +125 -0
- hwpx/oxml/field_marks.py +409 -0
- hwpx/oxml/header.py +349 -4
- hwpx/oxml/header_compat.py +317 -0
- hwpx/oxml/header_part.py +260 -123
- hwpx/oxml/history_part.py +162 -0
- hwpx/oxml/master_page.py +104 -0
- hwpx/oxml/master_page_authoring.py +153 -0
- hwpx/oxml/namespaces.py +8 -0
- hwpx/oxml/note_authoring.py +193 -0
- hwpx/oxml/numbering_kinds.py +98 -0
- hwpx/oxml/objects.py +934 -4
- hwpx/oxml/paragraph.py +325 -281
- hwpx/oxml/run.py +23 -0
- hwpx/oxml/section_format.py +79 -0
- hwpx/oxml/section_layout.py +133 -0
- hwpx/oxml/settings.py +155 -0
- hwpx/oxml/simple_parts.py +58 -4
- hwpx/oxml/table.py +253 -0
- hwpx/oxml/version_part.py +68 -0
- hwpx/table_patch.py +229 -30
- hwpx/tools/document_merge.py +1306 -0
- hwpx/tools/id_integrity.py +15 -0
- hwpx/tools/mail_merge.py +2 -2
- hwpx/tools/markdown_export.py +25 -2
- hwpx/tools/package_validator.py +83 -0
- hwpx/tools/text_extractor.py +31 -2
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/METADATA +1 -1
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/RECORD +57 -41
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/WHEEL +0 -0
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/entry_points.txt +0 -0
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx-6.0.3.dist-info → python_hwpx-6.2.1.dist-info}/top_level.txt +0 -0
hwpx/_document/_legacy.py
CHANGED
|
@@ -69,6 +69,7 @@ from . import tracked as _tracked
|
|
|
69
69
|
|
|
70
70
|
if TYPE_CHECKING:
|
|
71
71
|
from ..form_fit.policy import FitPolicy
|
|
72
|
+
from ..objects.binary_item import BinaryItem
|
|
72
73
|
from ..oxml import (
|
|
73
74
|
HwpxOxmlDocument,
|
|
74
75
|
HwpxOxmlHeader,
|
|
@@ -862,6 +863,8 @@ class _LegacyFacade:
|
|
|
862
863
|
border_color: str = "#BFBFBF",
|
|
863
864
|
border_width: str = "0.12 mm",
|
|
864
865
|
fill_color: str | None = None,
|
|
866
|
+
fill_image: "str | BinaryItem | Mapping[str, object] | None" = None,
|
|
867
|
+
fill_gradient: Mapping[str, object] | None = None,
|
|
865
868
|
active_borders: Sequence[str] | None = None,
|
|
866
869
|
border_type: str = "SOLID",
|
|
867
870
|
) -> str:
|
|
@@ -869,13 +872,25 @@ class _LegacyFacade:
|
|
|
869
872
|
|
|
870
873
|
``border_type`` selects the OWPML line style (``SOLID``, ``DASH``,
|
|
871
874
|
``DOT``, ``DOUBLE_SLIM``, ``WAVE``, …); values outside the OWPML
|
|
872
|
-
vocabulary are rejected.
|
|
875
|
+
vocabulary are rejected. ``fill_color``/``fill_image``/``fill_gradient``
|
|
876
|
+
are mutually exclusive (OWPML ``hc:fillBrush`` choice).
|
|
873
877
|
"""
|
|
874
878
|
|
|
879
|
+
from .ns.styles import _resolve_fill_gradient, _resolve_fill_image
|
|
880
|
+
|
|
881
|
+
given = sum(1 for v in (fill_color, fill_image, fill_gradient) if v is not None)
|
|
882
|
+
if given > 1:
|
|
883
|
+
raise HwpxValueError(
|
|
884
|
+
"fill_color/fill_image/fill_gradient are mutually exclusive",
|
|
885
|
+
code="style-border-fill-conflict",
|
|
886
|
+
suggestion="Pass exactly one of fill_color, fill_image, fill_gradient.",
|
|
887
|
+
)
|
|
875
888
|
return self._root.ensure_border_fill(
|
|
876
889
|
border_color=border_color,
|
|
877
890
|
border_width=border_width,
|
|
878
891
|
fill_color=fill_color,
|
|
892
|
+
fill_image=_resolve_fill_image(fill_image),
|
|
893
|
+
fill_gradient=_resolve_fill_gradient(fill_gradient),
|
|
879
894
|
active_borders=active_borders,
|
|
880
895
|
border_type=border_type,
|
|
881
896
|
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Highlight (``hp:markpenBegin``/``markpenEnd``) authoring and reading owner
|
|
3
|
+
behind the :class:`HwpxDocument` facade.
|
|
4
|
+
|
|
5
|
+
Real-corpus reversal (``hwpxlib_corpus/error__20251107__test*.hwpx``, the only
|
|
6
|
+
fixtures carrying this pair) plus the OWPML schema (``ParaList XML
|
|
7
|
+
schema.xml``) agree on the shape: both marks live inside a single ``hp:t``,
|
|
8
|
+
``markpenBegin`` carries an optional ``color``, ``markpenEnd`` carries no
|
|
9
|
+
attributes at all — pairing is positional (innermost open begin closes first),
|
|
10
|
+
not by id. :mod:`hwpx.tools.text_extractor` already reads this shape with a
|
|
11
|
+
per-``hp:t`` stack; :func:`list_highlights` exposes the same reading as a
|
|
12
|
+
public model instead of inline text markers.
|
|
13
|
+
|
|
14
|
+
Authoring is scoped to one run's text the same way ``doc.tracking.delete``
|
|
15
|
+
scopes tracked deletes: a match that only exists once inline markup pieces
|
|
16
|
+
are concatenated crosses a boundary this module refuses to split.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from ..errors import HwpxValueError
|
|
25
|
+
from ..objects.highlight import Highlight
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from hwpx.document import HwpxDocument
|
|
29
|
+
from ..oxml import HwpxOxmlParagraph
|
|
30
|
+
from ..oxml.body import TextSpan
|
|
31
|
+
|
|
32
|
+
#: OWPML ``hc:RGBColorType`` — ``#`` followed by exactly six hex digits.
|
|
33
|
+
_COLOR_RE = re.compile(r"#[0-9A-Fa-f]{6}")
|
|
34
|
+
|
|
35
|
+
#: Matches the real-corpus fixtures' brightest observed value; any
|
|
36
|
+
#: schema-valid hex is otherwise accepted.
|
|
37
|
+
DEFAULT_COLOR = "#FFFF00"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_color(color: str) -> str:
|
|
41
|
+
if not isinstance(color, str) or not _COLOR_RE.fullmatch(color):
|
|
42
|
+
raise HwpxValueError(
|
|
43
|
+
f"highlight color must be a 6-digit hex value like '#FFFF00', got {color!r}",
|
|
44
|
+
code="text-highlight-color-invalid",
|
|
45
|
+
context={"color": color},
|
|
46
|
+
suggestion="Pass a colour matching #RRGGBB (hex digits, either case).",
|
|
47
|
+
)
|
|
48
|
+
return color
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _paragraph_has_highlightable_text(paragraph: "HwpxOxmlParagraph", match: str) -> bool:
|
|
52
|
+
"""Return whether *match* lives inside one contiguous inline text piece.
|
|
53
|
+
|
|
54
|
+
Mirrors ``hwpx._document.tracked._paragraph_has_replaceable_text``: a
|
|
55
|
+
match that is only present after concatenating text across inline
|
|
56
|
+
markup (an existing highlight, a tracked-change mark, …) is real but
|
|
57
|
+
cannot be wrapped safely, so that case raises here instead of falling
|
|
58
|
+
through to a generic "not found".
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
crosses_inline_markup = False
|
|
62
|
+
for run in paragraph.runs:
|
|
63
|
+
model = run.to_model()
|
|
64
|
+
for span in model.text_spans:
|
|
65
|
+
if match not in span.text:
|
|
66
|
+
continue
|
|
67
|
+
if match in span.leading_text or any(
|
|
68
|
+
match in markup.trailing_text for markup in span.marks
|
|
69
|
+
):
|
|
70
|
+
return True
|
|
71
|
+
crosses_inline_markup = True
|
|
72
|
+
|
|
73
|
+
if crosses_inline_markup:
|
|
74
|
+
raise HwpxValueError(
|
|
75
|
+
"match text crosses inline markup and cannot be wrapped safely",
|
|
76
|
+
code="text-highlight-match-crosses-markup",
|
|
77
|
+
context={"match": match},
|
|
78
|
+
suggestion="Target a substring that lives inside a single run.",
|
|
79
|
+
)
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_highlight(
|
|
84
|
+
doc: "HwpxDocument",
|
|
85
|
+
paragraph: "HwpxOxmlParagraph",
|
|
86
|
+
match: str,
|
|
87
|
+
*,
|
|
88
|
+
color: str = DEFAULT_COLOR,
|
|
89
|
+
) -> Highlight:
|
|
90
|
+
"""Wrap the first occurrence of *match* in *paragraph* in markpen marks."""
|
|
91
|
+
|
|
92
|
+
if not match:
|
|
93
|
+
raise HwpxValueError(
|
|
94
|
+
"match text must be a non-empty string",
|
|
95
|
+
code="text-highlight-match-empty",
|
|
96
|
+
suggestion="Pass the substring to highlight.",
|
|
97
|
+
)
|
|
98
|
+
validated_color = _validate_color(color)
|
|
99
|
+
|
|
100
|
+
if not _paragraph_has_highlightable_text(paragraph, match):
|
|
101
|
+
raise HwpxValueError(
|
|
102
|
+
"match text was not found in the paragraph",
|
|
103
|
+
code="text-highlight-match-not-found",
|
|
104
|
+
context={"match": match, "paragraphText": paragraph.text},
|
|
105
|
+
suggestion="Inspect paragraph.text for the actual string.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
paragraph.add_highlight(color=validated_color, match=match)
|
|
109
|
+
return Highlight(text=match, color=validated_color, paragraph=paragraph)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _span_highlights(span: "TextSpan") -> list[tuple[str, "str | None"]]:
|
|
113
|
+
"""Yield (text, color) for every markpen pair closed within *span*.
|
|
114
|
+
|
|
115
|
+
A LIFO stack, same as ``text_extractor._render_text_element``'s
|
|
116
|
+
``highlight_stack`` — nested ``markpenBegin`` opens a new frame, the next
|
|
117
|
+
``markpenEnd`` closes the innermost one. A begin left open at the end of
|
|
118
|
+
the span (no matching end inside this ``hp:t``) is still reported, same
|
|
119
|
+
as that reader closing it defensively at ``hp:t`` end; an end with
|
|
120
|
+
nothing open is silently dropped rather than fabricated.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
results: list[tuple[str, "str | None"]] = []
|
|
124
|
+
stack: list[tuple["str | None", list[str]]] = []
|
|
125
|
+
|
|
126
|
+
for markup in span.marks:
|
|
127
|
+
element = markup.element
|
|
128
|
+
name = getattr(element, "name", None)
|
|
129
|
+
if name == "markpenBegin":
|
|
130
|
+
attributes = getattr(element, "attributes", None) or {}
|
|
131
|
+
stack.append((attributes.get("color"), []))
|
|
132
|
+
if markup.trailing_text:
|
|
133
|
+
stack[-1][1].append(markup.trailing_text)
|
|
134
|
+
continue
|
|
135
|
+
if name == "markpenEnd":
|
|
136
|
+
if stack:
|
|
137
|
+
mark_color, buffer = stack.pop()
|
|
138
|
+
results.append(("".join(buffer), mark_color))
|
|
139
|
+
if stack and markup.trailing_text:
|
|
140
|
+
stack[-1][1].append(markup.trailing_text)
|
|
141
|
+
continue
|
|
142
|
+
if stack and markup.trailing_text:
|
|
143
|
+
stack[-1][1].append(markup.trailing_text)
|
|
144
|
+
|
|
145
|
+
while stack:
|
|
146
|
+
mark_color, buffer = stack.pop()
|
|
147
|
+
results.append(("".join(buffer), mark_color))
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def list_highlights(doc: "HwpxDocument") -> tuple[Highlight, ...]:
|
|
152
|
+
"""Return every markpen highlight in the document, in document order."""
|
|
153
|
+
|
|
154
|
+
results: list[Highlight] = []
|
|
155
|
+
for paragraph in doc.paragraphs:
|
|
156
|
+
for run in paragraph.runs:
|
|
157
|
+
model = run.to_model()
|
|
158
|
+
for span in model.text_spans:
|
|
159
|
+
for text, color in _span_highlights(span):
|
|
160
|
+
results.append(Highlight(text=text, color=color, paragraph=paragraph))
|
|
161
|
+
return tuple(results)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["DEFAULT_COLOR", "add_highlight", "list_highlights"]
|
hwpx/_document/layout.py
CHANGED
|
@@ -15,6 +15,7 @@ from ..objects.results import (
|
|
|
15
15
|
ParagraphFormatResult,
|
|
16
16
|
Units,
|
|
17
17
|
)
|
|
18
|
+
from ..oxml._document_primitives import NEW_NUM_KINDS
|
|
18
19
|
from ..oxml.namespaces import HH
|
|
19
20
|
from ._units import _mm_to_hwp_units, _pt_to_hwp_units
|
|
20
21
|
|
|
@@ -119,16 +120,31 @@ def set_paragraph_format(
|
|
|
119
120
|
keep_with_next: bool | None = None,
|
|
120
121
|
keep_lines: bool | None = None,
|
|
121
122
|
page_break_before: bool | None = None,
|
|
123
|
+
column_break: bool | None = None,
|
|
122
124
|
bottom_border: bool = False,
|
|
123
125
|
border_color: str = "#BFBFBF",
|
|
124
126
|
border_width: str = "0.12 mm",
|
|
127
|
+
tab_stops: Sequence[Mapping[str, Any]] | None = None,
|
|
128
|
+
auto_tab_left: bool | None = None,
|
|
129
|
+
auto_tab_right: bool | None = None,
|
|
125
130
|
) -> ParagraphFormatResult:
|
|
126
131
|
"""Apply paragraph-level formatting using human units.
|
|
127
132
|
|
|
128
133
|
Millimetre inputs are converted to HWP units; paragraph spacing uses
|
|
129
134
|
points; line spacing is stored as a percent value. ``keep_with_next`` /
|
|
130
135
|
``keep_lines`` / ``page_break_before`` set the paragraph's keep-together
|
|
131
|
-
(``<hh:breakSetting>``) flags via a freshly minted paraPr.
|
|
136
|
+
(``<hh:breakSetting>``) flags via a freshly minted paraPr. ``column_break``
|
|
137
|
+
is a different mechanism -- ``hp:p``'s own ``columnBreak`` attribute, a
|
|
138
|
+
per-paragraph-instance forced break (not a shared paraPr style property
|
|
139
|
+
like ``page_break_before``) -- applied directly to each target paragraph.
|
|
140
|
+
|
|
141
|
+
``tab_stops`` is a sequence of ``{"pos_mm": ..., "type": "LEFT"|"RIGHT"|
|
|
142
|
+
"CENTER"|"DECIMAL", "leader": "NONE"|...}`` mappings (``type``/``leader``
|
|
143
|
+
default to the real-corpus-majority ``"LEFT"``/``"NONE"``) — order is
|
|
144
|
+
meaningful, matching how real multi-stop documents list them
|
|
145
|
+
position-ascending. Passing ``tab_stops``/``auto_tab_left``/
|
|
146
|
+
``auto_tab_right`` mints (or reuses — dedupe) a ``hh:tabPr`` and wires
|
|
147
|
+
the paragraph's ``tabPrIDRef`` to it.
|
|
132
148
|
"""
|
|
133
149
|
|
|
134
150
|
if not doc._root.headers:
|
|
@@ -181,6 +197,10 @@ def set_paragraph_format(
|
|
|
181
197
|
if page_break_before is not None:
|
|
182
198
|
break_setting["page_break_before"] = bool(page_break_before)
|
|
183
199
|
|
|
200
|
+
wants_tab_definition = (
|
|
201
|
+
tab_stops is not None or auto_tab_left is not None or auto_tab_right is not None
|
|
202
|
+
)
|
|
203
|
+
|
|
184
204
|
if (
|
|
185
205
|
alignment is None
|
|
186
206
|
and line_spacing_percent is None
|
|
@@ -188,6 +208,8 @@ def set_paragraph_format(
|
|
|
188
208
|
and heading is None
|
|
189
209
|
and not bottom_border
|
|
190
210
|
and not break_setting
|
|
211
|
+
and not wants_tab_definition
|
|
212
|
+
and column_break is None
|
|
191
213
|
):
|
|
192
214
|
raise HwpxValueError(
|
|
193
215
|
"at least one paragraph formatting option is required",
|
|
@@ -195,6 +217,28 @@ def set_paragraph_format(
|
|
|
195
217
|
suggestion="Pass alignment, line_spacing_percent, or another option to change.",
|
|
196
218
|
)
|
|
197
219
|
|
|
220
|
+
tab_pr_id: str | None = None
|
|
221
|
+
if wants_tab_definition:
|
|
222
|
+
converted_stops: list[dict[str, object]] = []
|
|
223
|
+
for index, stop in enumerate(tab_stops or ()):
|
|
224
|
+
if "pos_mm" not in stop or stop["pos_mm"] is None:
|
|
225
|
+
raise HwpxValueError(
|
|
226
|
+
f"tab_stops[{index}] is missing 'pos_mm'",
|
|
227
|
+
code="paragraph-tab-pos-invalid",
|
|
228
|
+
context={"index": index},
|
|
229
|
+
suggestion="각 tab stop은 'pos_mm'(mm, 0 이상)가 필요합니다.",
|
|
230
|
+
)
|
|
231
|
+
converted_stops.append({
|
|
232
|
+
"pos": _mm_to_hwp_units(float(stop["pos_mm"])),
|
|
233
|
+
"type": stop.get("type"),
|
|
234
|
+
"leader": stop.get("leader"),
|
|
235
|
+
})
|
|
236
|
+
tab_pr_id = header.ensure_tab_definition(
|
|
237
|
+
tab_stops=converted_stops,
|
|
238
|
+
auto_tab_left=bool(auto_tab_left),
|
|
239
|
+
auto_tab_right=bool(auto_tab_right),
|
|
240
|
+
)
|
|
241
|
+
|
|
198
242
|
border: dict[str, str] | None = None
|
|
199
243
|
if bottom_border:
|
|
200
244
|
border_fill_id = header.ensure_border_fill(
|
|
@@ -212,22 +256,40 @@ def set_paragraph_format(
|
|
|
212
256
|
"ignoreMargin": "0",
|
|
213
257
|
}
|
|
214
258
|
|
|
259
|
+
# column_break bypasses paraPr entirely (it's hp:p's own attribute, not
|
|
260
|
+
# a shared style) -- only mint a new paraPr when one of the *other*
|
|
261
|
+
# options actually needs it, so a column_break-only call doesn't churn
|
|
262
|
+
# a needless duplicate paraPr id.
|
|
263
|
+
wants_para_pr_change = (
|
|
264
|
+
alignment is not None
|
|
265
|
+
or line_spacing_percent is not None
|
|
266
|
+
or bool(margins)
|
|
267
|
+
or heading is not None
|
|
268
|
+
or bottom_border
|
|
269
|
+
or bool(break_setting)
|
|
270
|
+
or wants_tab_definition
|
|
271
|
+
)
|
|
272
|
+
|
|
215
273
|
targets = _resolve_paragraph_targets(doc,
|
|
216
274
|
paragraph_index=paragraph_index,
|
|
217
275
|
paragraph_indexes=paragraph_indexes,
|
|
218
276
|
)
|
|
219
277
|
formatted: list[int] = []
|
|
220
278
|
for index, paragraph in targets:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
279
|
+
if wants_para_pr_change:
|
|
280
|
+
para_pr_id = header.ensure_paragraph_format(
|
|
281
|
+
base_para_pr_id=paragraph.para_pr_id_ref,
|
|
282
|
+
alignment=alignment,
|
|
283
|
+
line_spacing_percent=line_spacing_percent,
|
|
284
|
+
margins=margins,
|
|
285
|
+
heading=heading,
|
|
286
|
+
border=border,
|
|
287
|
+
break_setting=break_setting or None,
|
|
288
|
+
tab_pr_id_ref=tab_pr_id,
|
|
289
|
+
)
|
|
290
|
+
paragraph.para_pr_id_ref = para_pr_id
|
|
291
|
+
if column_break is not None:
|
|
292
|
+
paragraph.column_break = column_break
|
|
231
293
|
formatted.append(index)
|
|
232
294
|
|
|
233
295
|
return ParagraphFormatResult(
|
|
@@ -767,6 +829,61 @@ def set_page_number(
|
|
|
767
829
|
)
|
|
768
830
|
|
|
769
831
|
|
|
832
|
+
def restart_page_number(
|
|
833
|
+
doc: "HwpxDocument",
|
|
834
|
+
paragraph: "HwpxOxmlParagraph",
|
|
835
|
+
*,
|
|
836
|
+
number: int = 1,
|
|
837
|
+
kind: str = "PAGE",
|
|
838
|
+
) -> "HwpxOxmlInlineObject":
|
|
839
|
+
"""Restart *kind*'s running count at *number* from *paragraph* onward.
|
|
840
|
+
|
|
841
|
+
Inserts ``<hp:ctrl><hp:newNum num="{number}" numType="{kind}"/></hp:ctrl>``
|
|
842
|
+
— a section-mid restart point, distinct from ``set_page_number`` (which
|
|
843
|
+
places the *display* field in a header/footer). ``kind`` defaults to
|
|
844
|
+
``"PAGE"``, the only value real corpus (hwpxlib_corpus) observes; the
|
|
845
|
+
other six (``FOOTNOTE``/``ENDNOTE``/``PICTURE``/``TABLE``/``EQUATION``/
|
|
846
|
+
``TOTAL_PAGE``) are schema-legal but unattested.
|
|
847
|
+
"""
|
|
848
|
+
|
|
849
|
+
normalized_kind = str(kind or "PAGE").upper()
|
|
850
|
+
if normalized_kind not in NEW_NUM_KINDS:
|
|
851
|
+
raise HwpxValueError(
|
|
852
|
+
f"unsupported kind {kind!r}",
|
|
853
|
+
code="page-new-num-kind-invalid",
|
|
854
|
+
context={"kind": normalized_kind, "allowed": sorted(NEW_NUM_KINDS)},
|
|
855
|
+
suggestion="Use one of: " + ", ".join(sorted(NEW_NUM_KINDS)),
|
|
856
|
+
)
|
|
857
|
+
return paragraph.add_new_num(number=number, kind=normalized_kind)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def hide_page_elements(
|
|
861
|
+
doc: "HwpxDocument",
|
|
862
|
+
paragraph: "HwpxOxmlParagraph",
|
|
863
|
+
*,
|
|
864
|
+
header: bool = False,
|
|
865
|
+
footer: bool = False,
|
|
866
|
+
master_page: bool = False,
|
|
867
|
+
border: bool = False,
|
|
868
|
+
fill: bool = False,
|
|
869
|
+
page_num: bool = False,
|
|
870
|
+
) -> "HwpxOxmlInlineObject":
|
|
871
|
+
"""Hide the named page elements from *paragraph*'s page onward.
|
|
872
|
+
|
|
873
|
+
Inserts ``<hp:ctrl><hp:pageHiding .../></hp:ctrl>`` (``ParaList XML
|
|
874
|
+
schema.xml:148-163`` — six independent booleans, all default unhidden).
|
|
875
|
+
"""
|
|
876
|
+
|
|
877
|
+
return paragraph.add_page_hiding(
|
|
878
|
+
header=header,
|
|
879
|
+
footer=footer,
|
|
880
|
+
master_page=master_page,
|
|
881
|
+
border=border,
|
|
882
|
+
fill=fill,
|
|
883
|
+
page_num=page_num,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
770
887
|
def remove_header(
|
|
771
888
|
doc: "HwpxDocument",
|
|
772
889
|
*,
|
hwpx/_document/ns/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
| 능력 영역 | 네임스페이스 |
|
|
11
11
|
|---|---|
|
|
12
|
-
| `shape-authoring`·`shape-escape-hatch`·`curve-objects`·`chart`·`equation` | `doc.shapes` |
|
|
12
|
+
| `shape-authoring`·`shape-escape-hatch`·`curve-objects`·`container-authoring`·`chart`·`equation` | `doc.shapes` |
|
|
13
13
|
| `picture`(BinData 절반) | `doc.media` |
|
|
14
14
|
| `redline` | `doc.tracking` |
|
|
15
15
|
| `memo`·`footnote-endnote` | `doc.notes` |
|
hwpx/_document/ns/page.py
CHANGED
|
@@ -27,7 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
from typing import TYPE_CHECKING, Any, Mapping, Sequence
|
|
28
28
|
|
|
29
29
|
from ...errors import HwpxValueError
|
|
30
|
-
from .._resolve import resolve_section
|
|
30
|
+
from .._resolve import resolve_paragraph, resolve_section
|
|
31
31
|
from ._base import _Namespace
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
|
|
|
37
37
|
PageMargins,
|
|
38
38
|
PageSize,
|
|
39
39
|
SectionGrid,
|
|
40
|
+
SectionTextDirection,
|
|
40
41
|
SectionVisibility,
|
|
41
42
|
)
|
|
42
43
|
from ...objects import PageSetup
|
|
@@ -44,6 +45,10 @@ if TYPE_CHECKING:
|
|
|
44
45
|
|
|
45
46
|
__all__ = ["PageNamespace"]
|
|
46
47
|
|
|
48
|
+
#: 6.12 트레인㊸ 갭③ — 스키마 선언 열거값(다른 어떤 값도 실코퍼스에 없음,
|
|
49
|
+
#: 실측 근거는 SectionTextDirection 독스트링 참조).
|
|
50
|
+
_TEXT_DIRECTIONS = frozenset({"HORIZONTAL", "VERTICAL", "VERTICALALL"})
|
|
51
|
+
|
|
47
52
|
|
|
48
53
|
class PageNamespace(_Namespace):
|
|
49
54
|
"""쪽 기하 — 용지·여백·단·머리말/꼬리말·쪽번호·격자·줄번호."""
|
|
@@ -339,6 +344,54 @@ class PageNamespace(_Namespace):
|
|
|
339
344
|
section=self._section(section, section_index, "set_page_number"),
|
|
340
345
|
)
|
|
341
346
|
|
|
347
|
+
def restart_page_number(
|
|
348
|
+
self,
|
|
349
|
+
paragraph: "HwpxOxmlParagraph | int",
|
|
350
|
+
*,
|
|
351
|
+
number: int = 1,
|
|
352
|
+
kind: str = "PAGE",
|
|
353
|
+
) -> "HwpxOxmlInlineObject":
|
|
354
|
+
"""*paragraph* 부터 *kind* 의 진행 번호를 *number* 로 재시작한다.
|
|
355
|
+
|
|
356
|
+
`set_page_number` 가 머리말/꼬리말의 *표시* 필드를 다루는 것과 달리,
|
|
357
|
+
이건 구역 중간의 재시작 지점(`hp:newNum`)을 문단에 심는다.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
from .. import layout as _layout
|
|
361
|
+
|
|
362
|
+
return _layout.restart_page_number(
|
|
363
|
+
self._doc,
|
|
364
|
+
resolve_paragraph(self._doc, paragraph, caller="doc.page.restart_page_number"),
|
|
365
|
+
number=number,
|
|
366
|
+
kind=kind,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def hide_page_elements(
|
|
370
|
+
self,
|
|
371
|
+
paragraph: "HwpxOxmlParagraph | int",
|
|
372
|
+
*,
|
|
373
|
+
header: bool = False,
|
|
374
|
+
footer: bool = False,
|
|
375
|
+
master_page: bool = False,
|
|
376
|
+
border: bool = False,
|
|
377
|
+
fill: bool = False,
|
|
378
|
+
page_num: bool = False,
|
|
379
|
+
) -> "HwpxOxmlInlineObject":
|
|
380
|
+
"""*paragraph* 가 속한 쪽부터 지정한 요소를 숨긴다(`hp:pageHiding`)."""
|
|
381
|
+
|
|
382
|
+
from .. import layout as _layout
|
|
383
|
+
|
|
384
|
+
return _layout.hide_page_elements(
|
|
385
|
+
self._doc,
|
|
386
|
+
resolve_paragraph(self._doc, paragraph, caller="doc.page.hide_page_elements"),
|
|
387
|
+
header=header,
|
|
388
|
+
footer=footer,
|
|
389
|
+
master_page=master_page,
|
|
390
|
+
border=border,
|
|
391
|
+
fill=fill,
|
|
392
|
+
page_num=page_num,
|
|
393
|
+
)
|
|
394
|
+
|
|
342
395
|
# -- 읽기: 현재 지면 상태 ----------------------------------------------
|
|
343
396
|
|
|
344
397
|
def size(
|
|
@@ -368,6 +421,33 @@ class PageNamespace(_Namespace):
|
|
|
368
421
|
# 저수준 API 는 ``HwpxOxmlSectionProperties`` 가 소유하고(Q3b), 여기서는 쪽
|
|
369
422
|
# 기하 축의 나머지와 같은 자리에 노출하기만 한다.
|
|
370
423
|
|
|
424
|
+
def master_page_refs(
|
|
425
|
+
self,
|
|
426
|
+
*,
|
|
427
|
+
section: "int | HwpxOxmlSection | None" = None,
|
|
428
|
+
section_index: int | None = None,
|
|
429
|
+
) -> tuple[str, ...]:
|
|
430
|
+
"""이 절이 참조하는 바탕쪽 id(``hp:masterPage/@idRef``) 목록."""
|
|
431
|
+
|
|
432
|
+
return self._section(
|
|
433
|
+
section, section_index, "master_page_refs"
|
|
434
|
+
).properties.master_page_refs
|
|
435
|
+
|
|
436
|
+
def set_master_page(
|
|
437
|
+
self,
|
|
438
|
+
master_page_id: str,
|
|
439
|
+
*,
|
|
440
|
+
section: "int | HwpxOxmlSection | None" = None,
|
|
441
|
+
section_index: int | None = None,
|
|
442
|
+
) -> None:
|
|
443
|
+
"""이 절이 바탕쪽(``doc.parts.add_master_page``가 만든 id)을
|
|
444
|
+
참조하도록 등록한다(6.13 트레인㊻). 이미 참조 중이면 아무 일도
|
|
445
|
+
안 한다(멱등)."""
|
|
446
|
+
|
|
447
|
+
self._section(
|
|
448
|
+
section, section_index, "set_master_page"
|
|
449
|
+
).properties.add_master_page_reference(master_page_id)
|
|
450
|
+
|
|
371
451
|
def grid(
|
|
372
452
|
self,
|
|
373
453
|
*,
|
|
@@ -395,6 +475,50 @@ class PageNamespace(_Namespace):
|
|
|
395
475
|
wonggoji_format=wonggoji_format,
|
|
396
476
|
)
|
|
397
477
|
|
|
478
|
+
def text_direction(
|
|
479
|
+
self,
|
|
480
|
+
*,
|
|
481
|
+
section: "int | HwpxOxmlSection | None" = None,
|
|
482
|
+
section_index: int | None = None,
|
|
483
|
+
) -> "SectionTextDirection":
|
|
484
|
+
"""글자 방향(가로쓰기/세로쓰기)과 머리말/꼬리말 세로쓰기 여부."""
|
|
485
|
+
|
|
486
|
+
return self._section(
|
|
487
|
+
section, section_index, "text_direction"
|
|
488
|
+
).properties.text_direction
|
|
489
|
+
|
|
490
|
+
def set_text_direction(
|
|
491
|
+
self,
|
|
492
|
+
direction: str | None = None,
|
|
493
|
+
*,
|
|
494
|
+
vertical_header_footer: bool | None = None,
|
|
495
|
+
section: "int | HwpxOxmlSection | None" = None,
|
|
496
|
+
section_index: int | None = None,
|
|
497
|
+
) -> None:
|
|
498
|
+
"""글자 방향(세로쓰기 포함)을 설정한다.
|
|
499
|
+
|
|
500
|
+
실코퍼스 67파일 전수에 `VERTICAL`/`VERTICALALL` 실사용 예가 없다
|
|
501
|
+
(74/74 `HORIZONTAL`) — 스키마 열거값 자체는 명확해 저작을 막지
|
|
502
|
+
않지만, 실한컴 렌더 검증 전까지는 "Create(experimental)"로만
|
|
503
|
+
표기한다(``docs/support-matrix.md`` 참조).
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
if direction is not None and direction not in _TEXT_DIRECTIONS:
|
|
507
|
+
raise HwpxValueError(
|
|
508
|
+
f"unsupported text direction: {direction}",
|
|
509
|
+
code="page-text-direction-unsupported",
|
|
510
|
+
context={
|
|
511
|
+
"requested": direction,
|
|
512
|
+
"supported": sorted(_TEXT_DIRECTIONS),
|
|
513
|
+
},
|
|
514
|
+
suggestion=f"Supported: {', '.join(sorted(_TEXT_DIRECTIONS))}",
|
|
515
|
+
)
|
|
516
|
+
self._section(
|
|
517
|
+
section, section_index, "set_text_direction"
|
|
518
|
+
).properties.set_text_direction(
|
|
519
|
+
direction, vertical_header_footer=vertical_header_footer
|
|
520
|
+
)
|
|
521
|
+
|
|
398
522
|
def visibility(
|
|
399
523
|
self,
|
|
400
524
|
*,
|