python-hwpx 5.6.0__py3-none-any.whl → 5.7.0__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/fields.py CHANGED
@@ -386,6 +386,121 @@ def add_form_field(
386
386
  )
387
387
 
388
388
 
389
+ _CHECK_BOX_TAG = f"{_HP}checkBtn"
390
+
391
+
392
+ def _iter_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
393
+ """Every ``<hp:checkBtn>`` in document order, with its owning element."""
394
+
395
+ found: list[dict[str, Any]] = []
396
+ index = 0
397
+ for section_index, section in enumerate(doc.sections):
398
+ for element in section.element.iter(_CHECK_BOX_TAG):
399
+ found.append(
400
+ {
401
+ "index": index,
402
+ "sectionIndex": section_index,
403
+ "name": element.get("name", ""),
404
+ "caption": element.get("caption", ""),
405
+ "checked": element.get("value", "").upper() == "CHECKED",
406
+ "value": element.get("value", ""),
407
+ "_element": element,
408
+ "_section": section,
409
+ }
410
+ )
411
+ index += 1
412
+ return found
413
+
414
+
415
+ def list_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
416
+ """Return check-box form objects in document order."""
417
+
418
+ return [
419
+ {key: value for key, value in match.items() if not key.startswith("_")}
420
+ for match in _iter_check_boxes(doc)
421
+ ]
422
+
423
+
424
+ def add_check_box(
425
+ doc: "HwpxDocument",
426
+ caption: str,
427
+ *,
428
+ checked: bool = False,
429
+ name: str | None = None,
430
+ paragraph: Any | None = None,
431
+ section: Any | None = None,
432
+ section_index: int | None = None,
433
+ ) -> dict[str, Any]:
434
+ """Create a check-box form object and return its payload.
435
+
436
+ The emitted XML follows the check-box contract measured against real
437
+ Hancom (specs/060): ``value`` selects ☑/□ and the ``<hp:formCharPr>`` child
438
+ is mandatory — without it Hancom refuses the document. The created object is
439
+ re-read through the standard :func:`list_check_boxes` reader, so creation
440
+ fails loudly if the ordinary consumer would not see it.
441
+ """
442
+
443
+ if not str(caption).strip():
444
+ raise ValueError("check box caption must be a non-empty string")
445
+ if paragraph is None:
446
+ paragraph = doc.add_paragraph(
447
+ "", section=section, section_index=section_index, include_run=False,
448
+ )
449
+
450
+ control = paragraph.add_check_box(caption, checked=checked, name=name)
451
+ _clear_form_field_layout_cache(paragraph.element)
452
+
453
+ created_name = control.element.get("name", "")
454
+ for match in _iter_check_boxes(doc):
455
+ if match["_element"] is control.element:
456
+ return {
457
+ key: value for key, value in match.items() if not key.startswith("_")
458
+ }
459
+ raise RuntimeError(
460
+ f"created check box {created_name!r} was not recognized by the standard reader"
461
+ )
462
+
463
+
464
+ def set_check_box(
465
+ doc: "HwpxDocument",
466
+ checked: bool,
467
+ *,
468
+ index: int | None = None,
469
+ name: str | None = None,
470
+ ) -> dict[str, Any]:
471
+ """Set a check box's state, selecting it by ``index`` or ``name``.
472
+
473
+ Exactly one selector is required — an ambiguous or missing selector is a
474
+ typed refusal, never a guess.
475
+ """
476
+
477
+ if (index is None) == (name is None):
478
+ raise ValueError("provide exactly one of index or name")
479
+ matches = _iter_check_boxes(doc)
480
+ if index is not None:
481
+ chosen = [m for m in matches if m["index"] == index]
482
+ if not chosen:
483
+ raise ValueError(f"check box index not found: {index}")
484
+ else:
485
+ wanted = str(name).strip()
486
+ chosen = [m for m in matches if m["name"] == wanted]
487
+ if not chosen:
488
+ raise ValueError(f"check box name not found: {wanted}")
489
+ if len(chosen) > 1:
490
+ raise ValueError(
491
+ f"check box name is ambiguous ({len(chosen)} matches): {wanted}"
492
+ )
493
+ match = chosen[0]
494
+ match["_element"].set("value", "CHECKED" if checked else "UNCHECKED")
495
+ match["_section"].mark_dirty()
496
+ return {
497
+ key: value
498
+ for key, value in {**match, "checked": bool(checked),
499
+ "value": "CHECKED" if checked else "UNCHECKED"}.items()
500
+ if not key.startswith("_")
501
+ }
502
+
503
+
389
504
  def _select_form_field(
390
505
  doc: "HwpxDocument",
391
506
  matches: Sequence[dict[str, Any]],
hwpx/document.py CHANGED
@@ -1462,6 +1462,68 @@ class HwpxDocument:
1462
1462
  section_index=section_index,
1463
1463
  )
1464
1464
 
1465
+ def list_check_boxes(self) -> list[dict[str, Any]]:
1466
+ """Return check-box form objects (체크박스) in document order.
1467
+
1468
+ Each entry carries ``index``/``name``/``caption``/``checked``.
1469
+ """
1470
+
1471
+ return _fields.list_check_boxes(self)
1472
+
1473
+ def add_check_box(
1474
+ self,
1475
+ caption: str,
1476
+ *,
1477
+ checked: bool = False,
1478
+ name: str | None = None,
1479
+ paragraph: HwpxOxmlParagraph | None = None,
1480
+ section: HwpxOxmlSection | None = None,
1481
+ section_index: int | None = None,
1482
+ ) -> dict[str, Any]:
1483
+ """Create a check-box form object (체크박스). **Experimental contract.**
1484
+
1485
+ Real Hancom draws ☑ when *checked* and □ otherwise, with *caption* beside
1486
+ the box and present in the rendered text layer. The created object is
1487
+ read back through :meth:`list_check_boxes` with no special-casing.
1488
+
1489
+ Note:
1490
+ Korean government forms specify a text ``[ ]`` + √ convention rather
1491
+ than this form object (시행규칙 별표 4 제10호), so this primitive is
1492
+ for forms that genuinely use Hancom check boxes — not for 공문서.
1493
+
1494
+ Args:
1495
+ caption: Label drawn beside the box (non-empty).
1496
+ checked: Initial state.
1497
+ name: Object name; generated when omitted.
1498
+ paragraph: Target paragraph (e.g. a table cell). A new paragraph is
1499
+ appended to *section* when omitted.
1500
+ """
1501
+
1502
+ return _fields.add_check_box(
1503
+ self,
1504
+ caption,
1505
+ checked=checked,
1506
+ name=name,
1507
+ paragraph=paragraph,
1508
+ section=section,
1509
+ section_index=section_index,
1510
+ )
1511
+
1512
+ def set_check_box(
1513
+ self,
1514
+ checked: bool,
1515
+ *,
1516
+ index: int | None = None,
1517
+ name: str | None = None,
1518
+ ) -> dict[str, Any]:
1519
+ """Set a check box's state, selecting it by ``index`` or ``name``.
1520
+
1521
+ Exactly one selector is required; an ambiguous name is refused rather
1522
+ than guessed.
1523
+ """
1524
+
1525
+ return _fields.set_check_box(self, checked, index=index, name=name)
1526
+
1465
1527
 
1466
1528
  def add_chart(
1467
1529
  self,
hwpx/oxml/paragraph.py CHANGED
@@ -1147,6 +1147,91 @@ class HwpxOxmlParagraph:
1147
1147
  self.section.mark_dirty()
1148
1148
  return HwpxOxmlInlineObject(ctrl1, self)
1149
1149
 
1150
+ def add_check_box(
1151
+ self,
1152
+ caption: str,
1153
+ *,
1154
+ checked: bool = False,
1155
+ name: str | None = None,
1156
+ width: int = 9921,
1157
+ height: int = 1984,
1158
+ char_pr_id_ref: str | int | None = None,
1159
+ ) -> HwpxOxmlInlineObject:
1160
+ """Insert a check-box form object (체크박스) at the end of this paragraph.
1161
+
1162
+ Emits the real-Hancom ``<hp:checkBtn>`` shape. Unlike the click-here
1163
+ field this is **not** wrapped in ``<hp:ctrl>`` — it is a direct child of
1164
+ the run, beside the ``<hp:t>`` node.
1165
+
1166
+ Hancom refuses to open a document whose ``checkBtn`` lacks
1167
+ ``<hp:formCharPr>`` (measured: the same file with the element present
1168
+ opens, without it Hancom reports a read error and produces nothing), so
1169
+ the child block is always emitted. ``caption`` is drawn next to the box
1170
+ and appears in the rendered text layer; ``checked`` selects the
1171
+ ``CHECKED``/``UNCHECKED`` value that draws ☑ or □.
1172
+
1173
+ Args:
1174
+ caption: Label drawn beside the box.
1175
+ checked: Initial state.
1176
+ name: Object name. Defaults to a generated ``CheckBox<N>``.
1177
+ width: Box + caption width in HWPUNIT.
1178
+ height: Box height in HWPUNIT.
1179
+ """
1180
+
1181
+ run = self._create_run_for_object(char_pr_id_ref=char_pr_id_ref)
1182
+ check = _append_child(run, f"{_HP}checkBtn", {
1183
+ "caption": _sanitize_text(caption),
1184
+ "value": "CHECKED" if checked else "UNCHECKED",
1185
+ "radioGroupName": "",
1186
+ "triState": "0",
1187
+ "backStyle": "OPAQUE",
1188
+ "name": _sanitize_text(name) if name else f"CheckBox{_object_id()}",
1189
+ "foreColor": "#000000",
1190
+ "backColor": "#FFFFFF",
1191
+ "groupName": "",
1192
+ "tabStop": "1",
1193
+ "editable": "1",
1194
+ "tabOrder": "0",
1195
+ "enabled": "1",
1196
+ "borderTypeIDRef": "0",
1197
+ "drawFrame": "1",
1198
+ "printable": "1",
1199
+ "command": "",
1200
+ })
1201
+ _append_child(check, f"{_HP}formCharPr", {
1202
+ "charPrIDRef": str(char_pr_id_ref if char_pr_id_ref is not None else 0),
1203
+ "followContext": "0",
1204
+ "autoSz": "0",
1205
+ "wordWrap": "0",
1206
+ })
1207
+ _append_child(check, f"{_HP}sz", {
1208
+ "width": str(int(width)),
1209
+ "widthRelTo": "ABSOLUTE",
1210
+ "height": str(int(height)),
1211
+ "heightRelTo": "ABSOLUTE",
1212
+ "protect": "0",
1213
+ })
1214
+ _append_child(check, f"{_HP}pos", {
1215
+ "treatAsChar": "1",
1216
+ "affectLSpacing": "0",
1217
+ "flowWithText": "1",
1218
+ "allowOverlap": "1",
1219
+ "holdAnchorAndSO": "0",
1220
+ "vertRelTo": "PARA",
1221
+ "horzRelTo": "COLUMN",
1222
+ "vertAlign": "TOP",
1223
+ "horzAlign": "LEFT",
1224
+ "vertOffset": "0",
1225
+ "horzOffset": "0",
1226
+ })
1227
+ _append_child(check, f"{_HP}outMargin", {
1228
+ "left": "0", "right": "0", "top": "0", "bottom": "0",
1229
+ })
1230
+ _append_child(run, f"{_HP}t", {})
1231
+
1232
+ self.section.mark_dirty()
1233
+ return HwpxOxmlInlineObject(check, self)
1234
+
1150
1235
  @property
1151
1236
  def bookmarks(self) -> list[str]:
1152
1237
  """Return the names of all bookmarks in this paragraph."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 5.6.0
3
+ Version: 5.7.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -1,7 +1,7 @@
1
1
  hwpx/__init__.py,sha256=DqoPNdD-4-oP4b_56P5ADABnyRGcDZiHch__S-urI4s,16642
2
2
  hwpx/body_patch.py,sha256=XfwrTMThS9vPLA62PDnEleCGuvYv61URxelbD_LHe8w,25730
3
3
  hwpx/capabilities.py,sha256=NZIuzerbKhdS6A4Ls9bpIrLOMQWoJ6JcICD4HrosJxQ,16597
4
- hwpx/document.py,sha256=XV8T-ucd0F-Uup6XBUe_ZLliSNuImq7MgGhefIwMQD0,67298
4
+ hwpx/document.py,sha256=ylIo6R3k67aRHgPzVDtQrUQF_gt-D2FHpQ3pNPckm9c,69441
5
5
  hwpx/errors.py,sha256=l3QK2Izwkpm25ar7QnNQvyg0yosoLfQt7kW5wn5MCG0,3033
6
6
  hwpx/experimental.py,sha256=Jvc7WAi3eMFRKYubqwtIWi9oU0OjuqNOfottWkaP2RQ,2113
7
7
  hwpx/mutation_report.py,sha256=6hurhDdgGiONeLIeWZJT8lwLtWJZ1VHp1l6G0zy0mQo,19409
@@ -12,7 +12,7 @@ hwpx/table_patch.py,sha256=nl7UT-Qf5-KV25Wdxm4ve9ZxX6FfijibymPXZcJJIjQ,83729
12
12
  hwpx/templates.py,sha256=28bYqeJVeDb1Cq8G9NZG9Mhnu4K2GamAKC4QhxvUZyA,1187
13
13
  hwpx/_document/__init__.py,sha256=REiNqMbuk_TS4NVC27FIo1rUt3Z8nhJrTBGOgNtxBRg,90
14
14
  hwpx/_document/_units.py,sha256=qyC8YtnvV2VlSGltJntDaIMF_sHy_XJZCwwe-g080TQ,386
15
- hwpx/_document/fields.py,sha256=6VB7FV69cWcHLONj_Bn_DUGH5k9IMn94SRhRCia6wvI,22935
15
+ hwpx/_document/fields.py,sha256=xOpTKXedTlpSg82G2_zPVQ0I6N6d6PQSAyQvZMtHF5A,26935
16
16
  hwpx/_document/layout.py,sha256=_fNHVqI2zFUn6L1-JsdyO8Z4UShmIi9G29Y5YJtQtKw,23553
17
17
  hwpx/_document/media.py,sha256=kamsFJZ4K9aoKhjyztMHC64MVFR_ZQ50rDVgouZqxFA,11592
18
18
  hwpx/_document/memos.py,sha256=_tscEhNfPe9-iAkcEtJGjwHTN42_lx4stFoPrgJxk3o,7481
@@ -61,7 +61,7 @@ hwpx/oxml/memo.py,sha256=jTtZIrR7LwWn_vvFN6Fr9yX2eSJfMPh8WnNW1DhD2Rs,10283
61
61
  hwpx/oxml/namespaces.py,sha256=c7JfdOdJbzrhyHvjbxoeeeloRE3xPuoB7v3YI8SmKDk,6524
62
62
  hwpx/oxml/numbering.py,sha256=9a0ARGW1DkKsTF7mEEXM4FA5loQmzIBuqE4APA8UFIQ,669
63
63
  hwpx/oxml/objects.py,sha256=8ErDXWvkjLm3FTsx0Z_IovUXbiv2uc-j0B5Y2-OtihA,20864
64
- hwpx/oxml/paragraph.py,sha256=btJlX6WScB9gjKRu2uAs1qAdEQYar2BuKdSwNfdRm9U,53408
64
+ hwpx/oxml/paragraph.py,sha256=q7uxcMMTSslRR3WpaEK5waFV8boGOMZA2Hx2uU87_cU,56601
65
65
  hwpx/oxml/parser.py,sha256=pIfyNdW3WdFkCcE8JFY7hn1fobRbMngzfhVZICvWeVs,2937
66
66
  hwpx/oxml/run.py,sha256=F939J1W6zQjr-JQ1Z8-nKh2s7NQ_0Okov4QJAEyw_5M,15503
67
67
  hwpx/oxml/schema.py,sha256=ElR3_IIhhPPZEqJtNKMNCHA-VFVLdhL454W_4spuvlc,1284
@@ -114,10 +114,10 @@ hwpx/tools/toc_fidelity.py,sha256=rvoKH8QJ65WNVrxS0d-i2AtODvBy-4O8NRudfQ244v4,19
114
114
  hwpx/tools/validator.py,sha256=U856izL9NcJZOiKDYoCpwOSaFNQ2Un8Jn4pzL20A96Q,7100
115
115
  hwpx/tools/_schemas/header.xsd,sha256=mJXuFMuHGT1JnFFaluUpYUglwjMCNlfbFCRVM26eHXE,664
116
116
  hwpx/tools/_schemas/section.xsd,sha256=MgvavVHG05RDfUnVPxVU10H4FQOja5ON04_m9Uk_m7E,522
117
- python_hwpx-5.6.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
118
- python_hwpx-5.6.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
119
- python_hwpx-5.6.0.dist-info/METADATA,sha256=t4MwjQ7lPJtRM5DNDihCB4NO7PVFb820vQBllRPte18,13162
120
- python_hwpx-5.6.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
121
- python_hwpx-5.6.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
122
- python_hwpx-5.6.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
123
- python_hwpx-5.6.0.dist-info/RECORD,,
117
+ python_hwpx-5.7.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
118
+ python_hwpx-5.7.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
119
+ python_hwpx-5.7.0.dist-info/METADATA,sha256=L4W3jUCNCTx15HU552LRUGTpIK5CuIFJegAlB6MEGCg,13162
120
+ python_hwpx-5.7.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
121
+ python_hwpx-5.7.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
122
+ python_hwpx-5.7.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
123
+ python_hwpx-5.7.0.dist-info/RECORD,,