python-hwpx 5.4.0__py3-none-any.whl → 5.5.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/oxml/memo.py CHANGED
@@ -14,7 +14,6 @@ from ._document_primitives import (
14
14
  _default_sublist_attributes,
15
15
  _element_local_name,
16
16
  _memo_id,
17
- _paragraph_id,
18
17
  _sanitize_text,
19
18
  )
20
19
 
@@ -213,14 +212,35 @@ class HwpxOxmlNote:
213
212
 
214
213
  @text.setter
215
214
  def text(self, value: str) -> None:
216
- """Replace the note body text."""
215
+ """Replace the note body text.
216
+
217
+ The gold-shaped body (각주/미주 style paragraph with the leading
218
+ ``<hp:autoNum>`` control real Hancom requires to render the note) is
219
+ preserved: only text content is replaced.
220
+ """
217
221
  sublist = self.element.find(f"{_HP}subList")
218
222
  if sublist is None:
219
- sublist = _append_child(self.element, f"{_HP}subList", _default_sublist_attributes())
220
- for p in sublist.findall(f"{_HP}p"):
223
+ attrs = _default_sublist_attributes()
224
+ attrs["vertAlign"] = "TOP"
225
+ sublist = _append_child(self.element, f"{_HP}subList", attrs)
226
+ paragraphs = sublist.findall(f"{_HP}p")
227
+ template = paragraphs[0] if paragraphs else None
228
+ p_attrs = (
229
+ dict(template.attrib)
230
+ if template is not None
231
+ else {"id": "0", **_DEFAULT_PARAGRAPH_ATTRS}
232
+ )
233
+ first_run = template.find(f"{_HP}run") if template is not None else None
234
+ run_attrs = dict(first_run.attrib) if first_run is not None else {"charPrIDRef": "0"}
235
+ auto_num = (
236
+ first_run.find(f"{_HP}ctrl") if first_run is not None else None
237
+ )
238
+ for p in paragraphs:
221
239
  sublist.remove(p)
222
- paragraph = _append_child(sublist, f"{_HP}p", {"id": _paragraph_id(), **_DEFAULT_PARAGRAPH_ATTRS})
223
- run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
240
+ paragraph = _append_child(sublist, f"{_HP}p", p_attrs)
241
+ run = _append_child(paragraph, f"{_HP}run", run_attrs)
242
+ if auto_num is not None:
243
+ run.append(auto_num)
224
244
  t = _append_child(run, f"{_HP}t", {})
225
245
  t.text = _sanitize_text(value)
226
246
  self.paragraph.section.mark_dirty()
hwpx/oxml/paragraph.py CHANGED
@@ -23,7 +23,6 @@ from ._document_primitives import (
23
23
  _default_sublist_attributes,
24
24
  _is_tab_control_element,
25
25
  _object_id,
26
- _paragraph_id,
27
26
  _sanitize_text,
28
27
  )
29
28
  from .memo import HwpxOxmlNote
@@ -1183,6 +1182,61 @@ class HwpxOxmlParagraph:
1183
1182
  # Footnote / Endnote helpers
1184
1183
  # ------------------------------------------------------------------
1185
1184
 
1185
+ _NOTE_STYLE_NAMES = {"footNote": ("각주", "Footnote"), "endNote": ("미주", "Endnote")}
1186
+ _NOTE_STYLE_FALLBACK = {"footNote": ("15", "10", "3"), "endNote": ("16", "10", "3")}
1187
+
1188
+ def _note_style_refs(self, tag: str) -> tuple[str, str, str]:
1189
+ """(styleIDRef, paraPrIDRef, charPrIDRef) for the note body paragraph.
1190
+
1191
+ Real Hancom puts note bodies on the 각주/미주 paragraph styles. Resolve
1192
+ by style name from the header so documents with re-numbered styles stay
1193
+ correct; fall back to the fixed template coordinates.
1194
+ """
1195
+
1196
+ korean, english = self._NOTE_STYLE_NAMES[tag]
1197
+ document = self.section.document
1198
+ headers = cast(
1199
+ "Sequence[object]",
1200
+ getattr(document, "_headers", []) if document is not None else [],
1201
+ )
1202
+ for header in headers:
1203
+ styles = getattr(header, "_styles_element", None)
1204
+ container = cast(
1205
+ "ET.Element | None", styles() if callable(styles) else None
1206
+ )
1207
+ if container is None:
1208
+ continue
1209
+ for style in container:
1210
+ if style.get("name") == korean or style.get("engName") == english:
1211
+ return (
1212
+ style.get("id") or self._NOTE_STYLE_FALLBACK[tag][0],
1213
+ style.get("paraPrIDRef") or self._NOTE_STYLE_FALLBACK[tag][1],
1214
+ style.get("charPrIDRef") or self._NOTE_STYLE_FALLBACK[tag][2],
1215
+ )
1216
+ return self._NOTE_STYLE_FALLBACK[tag]
1217
+
1218
+ def _note_suffix_char(self, tag: str) -> str:
1219
+ """The note suffix character from secPr, defaulting to ")"."""
1220
+
1221
+ properties = self.section._section_properties_element()
1222
+ if properties is not None:
1223
+ pr = properties.find(f"{_HP}{tag}Pr")
1224
+ if pr is not None:
1225
+ fmt = pr.find(f"{_HP}autoNumFormat")
1226
+ if fmt is not None and fmt.get("suffixChar"):
1227
+ return fmt.get("suffixChar", ")")
1228
+ return ")"
1229
+
1230
+ def _next_note_number(self, tag: str) -> int:
1231
+ """Document-continuous note number, counted per note type."""
1232
+
1233
+ document = self.section.document
1234
+ sections = document.sections if document is not None else [self.section]
1235
+ count = 0
1236
+ for section in sections:
1237
+ count += sum(1 for _ in section.element.iter(f"{_HP}{tag}"))
1238
+ return count + 1
1239
+
1186
1240
  def _add_note(
1187
1241
  self,
1188
1242
  tag: str,
@@ -1191,17 +1245,54 @@ class HwpxOxmlParagraph:
1191
1245
  run_attributes: dict[str, str] | None = None,
1192
1246
  char_pr_id_ref: str | int | None = None,
1193
1247
  ) -> HwpxOxmlNote:
1194
- """Insert a ``<hp:footNote>`` or ``<hp:endNote>`` element."""
1248
+ """Insert a ``<hp:footNote>`` or ``<hp:endNote>`` element.
1195
1249
 
1196
- run = self._create_run_for_object(run_attributes, char_pr_id_ref=char_pr_id_ref)
1197
- note_element = _append_child(run, f"{_HP}{tag}", {"instId": _object_id()})
1198
- sublist = _append_child(note_element, f"{_HP}subList", _default_sublist_attributes())
1199
- p_attrs = {"id": _paragraph_id(), **_DEFAULT_PARAGRAPH_ATTRS}
1250
+ Emits the real-Hancom shape (gold-reversed): the note element is
1251
+ wrapped in ``<hp:ctrl>`` inside a body run, carries ``number`` and
1252
+ ``suffixChar``, and its body paragraph uses the 각주/미주 style with a
1253
+ leading ``<hp:autoNum>`` control — without these real Hancom does not
1254
+ render the note at all.
1255
+ """
1256
+
1257
+ number = self._next_note_number(tag)
1258
+ suffix = self._note_suffix_char(tag)
1259
+ style_ref, para_pr_ref, note_char_ref = self._note_style_refs(tag)
1260
+ if char_pr_id_ref is not None:
1261
+ note_char_ref = str(char_pr_id_ref)
1262
+
1263
+ runs = self._run_elements()
1264
+ if run_attributes is None and runs:
1265
+ run = runs[-1]
1266
+ else:
1267
+ run = self._create_run_for_object(run_attributes, char_pr_id_ref=char_pr_id_ref)
1268
+ ctrl = _append_child(run, f"{_HP}ctrl", {})
1269
+ note_element = _append_child(
1270
+ ctrl,
1271
+ f"{_HP}{tag}",
1272
+ {
1273
+ "number": str(number),
1274
+ "suffixChar": str(ord(suffix[0])) if suffix else "41",
1275
+ "instId": _object_id(),
1276
+ },
1277
+ )
1278
+ sublist_attrs = _default_sublist_attributes()
1279
+ sublist_attrs["vertAlign"] = "TOP"
1280
+ sublist = _append_child(note_element, f"{_HP}subList", sublist_attrs)
1281
+ p_attrs = dict(_DEFAULT_PARAGRAPH_ATTRS)
1282
+ p_attrs.update({"id": "0", "paraPrIDRef": para_pr_ref, "styleIDRef": style_ref})
1200
1283
  paragraph = _append_child(sublist, f"{_HP}p", p_attrs)
1201
- # 본문 run의 charPrIDRef도 인자를 따라가도록 적용 (host run과 동일 스타일).
1202
- # None이면 "0"(default).
1203
- body_cpr = "0" if char_pr_id_ref is None else str(char_pr_id_ref)
1204
- note_run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": body_cpr})
1284
+ note_run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": note_char_ref})
1285
+ num_ctrl = _append_child(note_run, f"{_HP}ctrl", {})
1286
+ auto_num = _append_child(
1287
+ num_ctrl,
1288
+ f"{_HP}autoNum",
1289
+ {"num": str(number), "numType": "FOOTNOTE" if tag == "footNote" else "ENDNOTE"},
1290
+ )
1291
+ _append_child(
1292
+ auto_num,
1293
+ f"{_HP}autoNumFormat",
1294
+ {"type": "DIGIT", "userChar": "", "prefixChar": "", "suffixChar": suffix, "supscript": "0"},
1295
+ )
1205
1296
  t = _append_child(note_run, f"{_HP}t", {})
1206
1297
  t.text = _sanitize_text(text)
1207
1298
  self.section.mark_dirty()
@@ -228,7 +228,7 @@ def _md_push_text(state: _MdParagraphState, cpr, text) -> None:
228
228
  state.items.append((cpr, text))
229
229
 
230
230
 
231
- def _md_handle_ctrl_child(child, state: _MdParagraphState, base_cp, chars) -> None:
231
+ def _md_handle_ctrl_child(child, state: _MdParagraphState, base_cp, chars, doc=None, notes_out=None) -> None:
232
232
  for gc in child:
233
233
  gctag = _local_name(gc)
234
234
  if gctag == "fieldBegin" and gc.get("type") == "HYPERLINK":
@@ -236,6 +236,9 @@ def _md_handle_ctrl_child(child, state: _MdParagraphState, base_cp, chars) -> No
236
236
  state.link_url = gc.get("name", "")
237
237
  elif gctag == "fieldEnd":
238
238
  _md_flush_link(state, base_cp, chars)
239
+ elif gctag in ("footNote", "endNote"):
240
+ # 실한컴 계약: 각주/미주는 run 안 <hp:ctrl>에 래핑되어 있다.
241
+ _md_handle_note_child(gc, gctag, state, base_cp, chars, doc, notes_out)
239
242
 
240
243
 
241
244
  def _md_handle_note_child(
@@ -271,8 +274,9 @@ def _p_element_to_md(p_el, doc, notes_out: list | None = None) -> str:
271
274
  if child.text:
272
275
  _md_push_text(state, cpr, child.text)
273
276
  elif tag == "ctrl":
274
- _md_handle_ctrl_child(child, state, base_cp, chars)
277
+ _md_handle_ctrl_child(child, state, base_cp, chars, doc, notes_out)
275
278
  elif tag in ("footNote", "endNote"):
279
+ # 구식(5.4 이전 자사 방출) 호환: run 직속 각주도 계속 읽는다.
276
280
  _md_handle_note_child(child, tag, state, base_cp, chars, doc, notes_out)
277
281
 
278
282
  _md_flush_items(state, base_cp, chars)
@@ -483,6 +483,20 @@ class TextExtractor:
483
483
  annotations: Optional[AnnotationOptions],
484
484
  preserve_breaks: bool,
485
485
  ) -> None:
486
+ # 실한컴 계약: 각주/미주는 <hp:ctrl> 래핑으로 나타난다 — annotations
487
+ # 유무와 무관하게 노트 분기로 위임한다(구식 run 직속도 별도 분기 유지).
488
+ for note_tag in ("footNote", "endNote"):
489
+ note = _first_child_by_local(element, note_tag)
490
+ if note is not None:
491
+ self._handle_note(
492
+ note,
493
+ fragments,
494
+ note_tag,
495
+ annotations=annotations,
496
+ preserve_breaks=preserve_breaks,
497
+ )
498
+ return
499
+
486
500
  if annotations is None:
487
501
  return
488
502
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 5.4.0
3
+ Version: 5.5.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -52,11 +52,11 @@ hwpx/oxml/document.py,sha256=0Ml-t2fwa4f2xMfu5jbkOoh_KDKCCf3gNpaYICRV-KY,1172
52
52
  hwpx/oxml/document_parts.py,sha256=NSlCmXKfItIaTEYruu6zUh-XHZRRDI9XZ2U-5rD2UUU,49240
53
53
  hwpx/oxml/header.py,sha256=4LtTDPe7YW0o_4jPv3Rd-vyv5suRfebecxtTWbF65Uw,50011
54
54
  hwpx/oxml/header_part.py,sha256=JBOw3Ljkt0PCYYKP7IlFpxA1ZRbrBHqDo8hjvE-yMsk,51409
55
- hwpx/oxml/memo.py,sha256=QlmuELEzywnmywgjm94S3vzJB8wPpoFOmI8KVuzoNPw,9484
55
+ hwpx/oxml/memo.py,sha256=jTtZIrR7LwWn_vvFN6Fr9yX2eSJfMPh8WnNW1DhD2Rs,10283
56
56
  hwpx/oxml/namespaces.py,sha256=c7JfdOdJbzrhyHvjbxoeeeloRE3xPuoB7v3YI8SmKDk,6524
57
57
  hwpx/oxml/numbering.py,sha256=9a0ARGW1DkKsTF7mEEXM4FA5loQmzIBuqE4APA8UFIQ,669
58
58
  hwpx/oxml/objects.py,sha256=8ErDXWvkjLm3FTsx0Z_IovUXbiv2uc-j0B5Y2-OtihA,20864
59
- hwpx/oxml/paragraph.py,sha256=eZHoEnwiVhk9HvvhCZ77WMo1iulOD6BtLU_RvVHoDNY,49635
59
+ hwpx/oxml/paragraph.py,sha256=btJlX6WScB9gjKRu2uAs1qAdEQYar2BuKdSwNfdRm9U,53408
60
60
  hwpx/oxml/parser.py,sha256=pIfyNdW3WdFkCcE8JFY7hn1fobRbMngzfhVZICvWeVs,2937
61
61
  hwpx/oxml/run.py,sha256=F939J1W6zQjr-JQ1Z8-nKh2s7NQ_0Okov4QJAEyw_5M,15503
62
62
  hwpx/oxml/schema.py,sha256=ElR3_IIhhPPZEqJtNKMNCHA-VFVLdhL454W_4spuvlc,1284
@@ -83,7 +83,7 @@ hwpx/tools/idempotence.py,sha256=kRZBWsPJFVprYRJkkxn3DHjrT6qX2nK7KvhmdgJ4JpA,501
83
83
  hwpx/tools/ir_equality.py,sha256=NIjq33-4q7iTo_asv7WUvh_-EfB1JZMTwbLfYIw2c5w,4765
84
84
  hwpx/tools/layout_preview.py,sha256=cHlBUEdMn_ac-hNFDUgF5r2PMM0cTw65l7sqq7Li8NM,22702
85
85
  hwpx/tools/mail_merge.py,sha256=8kGlSk2GQL7cFxZgg6P_bKisj1zVUWy8qh8AXIZLK_w,18611
86
- hwpx/tools/markdown_export.py,sha256=6g0G30Zk0psN9KeZWVMK_fnSzd6Ij9w7t-ZSOsHxW3c,20133
86
+ hwpx/tools/markdown_export.py,sha256=_qhOqnYg2tFQa4-EuXyFdJvkc_-9x6ln9lx-MeqtNeY,20495
87
87
  hwpx/tools/object_finder.py,sha256=ovXlbuPiQFdGzNSU08SuEzNFoeYcIb7GfF6vB9MRwBE,13800
88
88
  hwpx/tools/package_reconcile.py,sha256=y1Hl7hbPh4YaV59LTdDLzQwgn4g1qEnFmSjmajnrEbA,2416
89
89
  hwpx/tools/package_validator.py,sha256=fYkJevV_soQuM8LEUQMkwYaTy2k2HV4VWBwfIXFt29Y,33306
@@ -98,16 +98,16 @@ hwpx/tools/table_cleanup.py,sha256=0_f6NnvNp3QD4owKd_bRX6FZbeUmoQC7a4_VGzF2SCE,1
98
98
  hwpx/tools/table_navigation.py,sha256=rtbrWFKpJhqC3LD0ZXImyHgjmDR2hjHCFy3_S-qNBwA,16479
99
99
  hwpx/tools/template_analyzer.py,sha256=iH7egGRYNb8UesDB1TJ4tvW41-mlmv5Ap3VacqJFSlE,21355
100
100
  hwpx/tools/text_extract_cli.py,sha256=BmsDAwNXpDPhEayb9ez2ORtGNzPd_Xxduy4_cLXhnUw,2188
101
- hwpx/tools/text_extractor.py,sha256=m9OSBd4FjZrzlMP8b4uS2Lnmjp-u-t7EBKFDF3P1bTM,25514
101
+ hwpx/tools/text_extractor.py,sha256=6Wtk3av69z848ZJAEh-fV4TU2BkAwk3qKJpIyUkYT_4,26122
102
102
  hwpx/tools/toc_author.py,sha256=eS1Zm5OBoonuD_H92JxcN2o276d5XmcJDqpfscd4wSg,15445
103
103
  hwpx/tools/toc_fidelity.py,sha256=rvoKH8QJ65WNVrxS0d-i2AtODvBy-4O8NRudfQ244v4,19890
104
104
  hwpx/tools/validator.py,sha256=U856izL9NcJZOiKDYoCpwOSaFNQ2Un8Jn4pzL20A96Q,7100
105
105
  hwpx/tools/_schemas/header.xsd,sha256=mJXuFMuHGT1JnFFaluUpYUglwjMCNlfbFCRVM26eHXE,664
106
106
  hwpx/tools/_schemas/section.xsd,sha256=MgvavVHG05RDfUnVPxVU10H4FQOja5ON04_m9Uk_m7E,522
107
- python_hwpx-5.4.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
108
- python_hwpx-5.4.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
109
- python_hwpx-5.4.0.dist-info/METADATA,sha256=hd6N3Fnhn5arZdkRj-BfKR4or0Zceejz27w-lyHaQS8,13162
110
- python_hwpx-5.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
111
- python_hwpx-5.4.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
112
- python_hwpx-5.4.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
113
- python_hwpx-5.4.0.dist-info/RECORD,,
107
+ python_hwpx-5.5.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
108
+ python_hwpx-5.5.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
109
+ python_hwpx-5.5.0.dist-info/METADATA,sha256=8nE4IAXunnxWpqeKPO01ZzKLHA1mXKqZUb034C9gB9k,13162
110
+ python_hwpx-5.5.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
111
+ python_hwpx-5.5.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
112
+ python_hwpx-5.5.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
113
+ python_hwpx-5.5.0.dist-info/RECORD,,