python-hwpx 2.10.1__tar.gz → 2.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {python_hwpx-2.10.1/src/python_hwpx.egg-info → python_hwpx-2.10.2}/PKG-INFO +49 -1
  2. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/README.md +48 -0
  3. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/pyproject.toml +1 -1
  4. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/document.py +8 -0
  5. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/document.py +147 -2
  6. python_hwpx-2.10.2/src/hwpx/tools/markdown_export.py +488 -0
  7. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/table_navigation.py +77 -1
  8. {python_hwpx-2.10.1 → python_hwpx-2.10.2/src/python_hwpx.egg-info}/PKG-INFO +49 -1
  9. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/python_hwpx.egg-info/SOURCES.txt +2 -0
  10. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_document_formatting.py +42 -0
  11. python_hwpx-2.10.2/tests/test_markdown_export.py +436 -0
  12. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_table_navigation.py +57 -31
  13. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/LICENSE +0 -0
  14. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/NOTICE +0 -0
  15. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/setup.cfg +0 -0
  16. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/__init__.py +0 -0
  17. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/authoring.py +0 -0
  18. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/builder/__init__.py +0 -0
  19. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/builder/core.py +0 -0
  20. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/builder/report.py +0 -0
  21. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/data/Skeleton.hwpx +0 -0
  22. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/form_fill.py +0 -0
  23. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/opc/package.py +0 -0
  24. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/opc/relationships.py +0 -0
  25. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/opc/xml_utils.py +0 -0
  26. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/__init__.py +0 -0
  27. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/body.py +0 -0
  28. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/common.py +0 -0
  29. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/header.py +0 -0
  30. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/header_part.py +0 -0
  31. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/memo.py +0 -0
  32. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/namespaces.py +0 -0
  33. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/paragraph.py +0 -0
  34. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/parser.py +0 -0
  35. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/schema.py +0 -0
  36. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/section.py +0 -0
  37. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/table.py +0 -0
  38. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/oxml/utils.py +0 -0
  39. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/package.py +0 -0
  40. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/presets/__init__.py +0 -0
  41. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/presets/proposal.py +0 -0
  42. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/py.typed +0 -0
  43. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/template_formfit.py +0 -0
  44. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/templates.py +0 -0
  45. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/__init__.py +0 -0
  46. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/_schemas/header.xsd +0 -0
  47. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/_schemas/section.xsd +0 -0
  48. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/archive_cli.py +0 -0
  49. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/exporter.py +0 -0
  50. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/generic_inventory.py +0 -0
  51. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/id_integrity.py +0 -0
  52. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/object_finder.py +0 -0
  53. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/package_validator.py +0 -0
  54. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/page_guard.py +0 -0
  55. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/recover.py +0 -0
  56. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/repair.py +0 -0
  57. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/report_parser.py +0 -0
  58. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/report_utils.py +0 -0
  59. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/roundtrip_diff.py +0 -0
  60. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/table_cleanup.py +0 -0
  61. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/template_analyzer.py +0 -0
  62. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/text_extract_cli.py +0 -0
  63. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/text_extractor.py +0 -0
  64. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/hwpx/tools/validator.py +0 -0
  65. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  66. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  67. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/python_hwpx.egg-info/requires.txt +0 -0
  68. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/src/python_hwpx.egg-info/top_level.txt +0 -0
  69. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_builder_core.py +0 -0
  70. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_builder_plan_v2.py +0 -0
  71. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_builder_vertical_slice.py +0 -0
  72. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_coverage_promotion.py +0 -0
  73. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_coverage_targets.py +0 -0
  74. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_deviations_registry.py +0 -0
  75. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_document_context_manager.py +0 -0
  76. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_document_plan.py +0 -0
  77. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_document_plan_computed_fields.py +0 -0
  78. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_document_save_api.py +0 -0
  79. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_form_fill_split_run.py +0 -0
  80. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_gap_closure_tools.py +0 -0
  81. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_government_report_preset.py +0 -0
  82. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_government_table_profile.py +0 -0
  83. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_hp_tab_support.py +0 -0
  84. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_hwpxlib_corpus_read.py +0 -0
  85. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_id_generator_range.py +0 -0
  86. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_id_integrity.py +0 -0
  87. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_inline_models.py +0 -0
  88. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_integration_hwpx_compatibility.py +0 -0
  89. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_integration_roundtrip.py +0 -0
  90. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_memo_and_style_editing.py +0 -0
  91. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_namespace_handling.py +0 -0
  92. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_new_features.py +0 -0
  93. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_opc_package.py +0 -0
  94. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_oxml_parsing.py +0 -0
  95. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_packaging_license_metadata.py +0 -0
  96. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_packaging_py_typed.py +0 -0
  97. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_paragraph_section_management.py +0 -0
  98. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_proposal_preset.py +0 -0
  99. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_recover_broken_zip.py +0 -0
  100. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_repair_repack.py +0 -0
  101. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_report_parser.py +0 -0
  102. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_report_utils.py +0 -0
  103. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_repr_snapshots.py +0 -0
  104. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_roundtrip_fidelity.py +0 -0
  105. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_section_headers.py +0 -0
  106. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_skeleton_template_ids.py +0 -0
  107. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_split_merged_cell.py +0 -0
  108. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_table_cleanup.py +0 -0
  109. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_tables_default_border.py +0 -0
  110. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_template_formfit.py +0 -0
  111. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_text_extractor_annotations.py +0 -0
  112. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_validation_severity.py +0 -0
  113. {python_hwpx-2.10.1 → python_hwpx-2.10.2}/tests/test_version_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.10.1
3
+ Version: 2.10.2
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -115,6 +115,47 @@ hwpx-validate-package 보고서.hwpx
115
115
  hwpx-analyze-template 보고서.hwpx
116
116
  ```
117
117
 
118
+ ### 4. 풍부한 Markdown 변환 (서식·표·각주·이미지 보존)
119
+
120
+ `export_markdown()`는 단순 평문 추출이고, `export_rich_markdown()`는 인라인 서식(`**굵게**`, `*기울임*`, `~~취소선~~`),
121
+ 표(중첩 포함, colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존한다.
122
+
123
+ ```python
124
+ from hwpx import HwpxDocument
125
+
126
+ doc = HwpxDocument.open("보고서.hwpx")
127
+
128
+ md = doc.export_rich_markdown(
129
+ image_dir="out/images", # BinData 이미지를 디스크에 추출
130
+ image_ref_prefix="images/", # 마크다운 내 ![](images/...) 경로 접두
131
+ detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
132
+ )
133
+ print(md)
134
+ ```
135
+
136
+ 문자열·경로·바이트도 그대로 받는다:
137
+
138
+ ```python
139
+ from hwpx.tools.markdown_export import export_markdown
140
+
141
+ md = export_markdown("보고서.hwpx") # 경로
142
+ md = export_markdown(open("a.hwpx", "rb").read()) # bytes
143
+ ```
144
+
145
+ ### 5. 각주 본문에 혼합 서식 / 하이퍼링크 추가
146
+
147
+ `HwpxOxmlNote`에 `body_paragraph`, `add_run`, `add_hyperlink` helper가 있어 각주 본문을
148
+ 직접 paragraph로 다루지 않고도 인라인 서식·링크를 손쉽게 채울 수 있다.
149
+
150
+ ```python
151
+ para = section.paragraphs[0]
152
+ note = para.add_footnote("") # 빈 각주 생성 후 본문 구성
153
+ note.add_run("자세한 내용은 ", )
154
+ note.add_run("정부 공식 사이트", bold=True)
155
+ note.add_run("를 참고하라: ")
156
+ note.add_hyperlink("https://www.kasa.go.kr", "우주항공청")
157
+ ```
158
+
118
159
  처음에는 `open/new -> edit/extract -> save_to_path` 흐름만 잡으면 된다. 패키지 구조, XML 파트, 템플릿 회귀 점검은 필요할 때만 확장하면 된다.
119
160
 
120
161
  ## 어디부터 읽으면 되나
@@ -244,6 +285,7 @@ doc.set_footer_text("1 / 10", page_type="BOTH")
244
285
  # 표 셀 병합·분할
245
286
  table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
246
287
  table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)
288
+ table.set_cell_text(0, 0, "line 1\nline 2", split_paragraphs=True)
247
289
 
248
290
  # 양식형 표 자동 채우기
249
291
  form = doc.add_table(2, 2)
@@ -257,6 +299,12 @@ doc.fill_by_path({
257
299
  })
258
300
  ```
259
301
 
302
+ `doc.paragraphs`의 인덱스는 본문 직속 문단 0-based 기준입니다. 표 안 문단은
303
+ 본문 `paragraph_index`에 섞지 않고 `get_table_map()`의 cell `location`
304
+ (`table_index`, `row`, `col`, `cell_paragraph_index`)으로 다룹니다.
305
+ `get_table_map()`은 `caption_text`와 `preceding_paragraph_text`를 분리해
306
+ 반환하고, 셀 미리보기의 여러 문단은 `\n`으로 유지합니다.
307
+
260
308
  ### 🔍 텍스트 추출 & 검색
261
309
 
262
310
  ```python
@@ -79,6 +79,47 @@ hwpx-validate-package 보고서.hwpx
79
79
  hwpx-analyze-template 보고서.hwpx
80
80
  ```
81
81
 
82
+ ### 4. 풍부한 Markdown 변환 (서식·표·각주·이미지 보존)
83
+
84
+ `export_markdown()`는 단순 평문 추출이고, `export_rich_markdown()`는 인라인 서식(`**굵게**`, `*기울임*`, `~~취소선~~`),
85
+ 표(중첩 포함, colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존한다.
86
+
87
+ ```python
88
+ from hwpx import HwpxDocument
89
+
90
+ doc = HwpxDocument.open("보고서.hwpx")
91
+
92
+ md = doc.export_rich_markdown(
93
+ image_dir="out/images", # BinData 이미지를 디스크에 추출
94
+ image_ref_prefix="images/", # 마크다운 내 ![](images/...) 경로 접두
95
+ detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
96
+ )
97
+ print(md)
98
+ ```
99
+
100
+ 문자열·경로·바이트도 그대로 받는다:
101
+
102
+ ```python
103
+ from hwpx.tools.markdown_export import export_markdown
104
+
105
+ md = export_markdown("보고서.hwpx") # 경로
106
+ md = export_markdown(open("a.hwpx", "rb").read()) # bytes
107
+ ```
108
+
109
+ ### 5. 각주 본문에 혼합 서식 / 하이퍼링크 추가
110
+
111
+ `HwpxOxmlNote`에 `body_paragraph`, `add_run`, `add_hyperlink` helper가 있어 각주 본문을
112
+ 직접 paragraph로 다루지 않고도 인라인 서식·링크를 손쉽게 채울 수 있다.
113
+
114
+ ```python
115
+ para = section.paragraphs[0]
116
+ note = para.add_footnote("") # 빈 각주 생성 후 본문 구성
117
+ note.add_run("자세한 내용은 ", )
118
+ note.add_run("정부 공식 사이트", bold=True)
119
+ note.add_run("를 참고하라: ")
120
+ note.add_hyperlink("https://www.kasa.go.kr", "우주항공청")
121
+ ```
122
+
82
123
  처음에는 `open/new -> edit/extract -> save_to_path` 흐름만 잡으면 된다. 패키지 구조, XML 파트, 템플릿 회귀 점검은 필요할 때만 확장하면 된다.
83
124
 
84
125
  ## 어디부터 읽으면 되나
@@ -208,6 +249,7 @@ doc.set_footer_text("1 / 10", page_type="BOTH")
208
249
  # 표 셀 병합·분할
209
250
  table.merge_cells(0, 0, 1, 1) # (0,0)~(1,1) 병합
210
251
  table.set_cell_text(0, 0, "병합된 셀", logical=True, split_merged=True)
252
+ table.set_cell_text(0, 0, "line 1\nline 2", split_paragraphs=True)
211
253
 
212
254
  # 양식형 표 자동 채우기
213
255
  form = doc.add_table(2, 2)
@@ -221,6 +263,12 @@ doc.fill_by_path({
221
263
  })
222
264
  ```
223
265
 
266
+ `doc.paragraphs`의 인덱스는 본문 직속 문단 0-based 기준입니다. 표 안 문단은
267
+ 본문 `paragraph_index`에 섞지 않고 `get_table_map()`의 cell `location`
268
+ (`table_index`, `row`, `col`, `cell_paragraph_index`)으로 다룹니다.
269
+ `get_table_map()`은 `caption_text`와 `preceding_paragraph_text`를 분리해
270
+ 반환하고, 셀 미리보기의 여러 문단은 `\n`으로 유지합니다.
271
+
224
272
  ### 🔍 텍스트 추출 & 검색
225
273
 
226
274
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "2.10.1"
7
+ version = "2.10.2"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -1472,6 +1472,14 @@ class HwpxDocument:
1472
1472
  from .tools.exporter import export_markdown
1473
1473
  return export_markdown(self, **kwargs) # type: ignore[arg-type]
1474
1474
 
1475
+ def export_rich_markdown(self, **kwargs: object) -> str:
1476
+ """Export rich Markdown preserving inline styles, tables, footnotes, hyperlinks, images, and shape text.
1477
+
1478
+ Keyword args forwarded to :func:`~hwpx.tools.markdown_export.export_markdown`.
1479
+ """
1480
+ from .tools.markdown_export import export_markdown as _rich
1481
+ return _rich(self, **kwargs) # type: ignore[arg-type]
1482
+
1475
1483
  # ------------------------------------------------------------------
1476
1484
  # Validation
1477
1485
  # ------------------------------------------------------------------
@@ -1872,6 +1872,68 @@ class HwpxOxmlNote:
1872
1872
  t.text = _sanitize_text(value)
1873
1873
  self.paragraph.section.mark_dirty()
1874
1874
 
1875
+ @property
1876
+ def body_paragraph(self) -> "HwpxOxmlParagraph":
1877
+ """Return the note's body ``<hp:p>`` wrapped as :class:`HwpxOxmlParagraph`.
1878
+
1879
+ The body lives inside ``<hp:subList>`` and is distinct from
1880
+ :attr:`paragraph`, which is the *hosting* paragraph (where the note
1881
+ marker is inserted). Use this to add runs with mixed formatting
1882
+ directly into the note body:
1883
+
1884
+ >>> note = para.add_footnote("기본 ")
1885
+ >>> note.add_run("청색", char_pr_id_ref=5)
1886
+ """
1887
+ p = self.element.find(f".//{_HP}p")
1888
+ if p is None:
1889
+ raise ValueError("note has no body paragraph element")
1890
+ return HwpxOxmlParagraph(p, self.paragraph.section)
1891
+
1892
+ def add_run(
1893
+ self,
1894
+ text: str = "",
1895
+ *,
1896
+ char_pr_id_ref: str | int | None = None,
1897
+ bold: bool = False,
1898
+ italic: bool = False,
1899
+ underline: bool = False,
1900
+ color: str | None = None,
1901
+ font: str | None = None,
1902
+ size: int | float | None = None,
1903
+ highlight: str | None = None,
1904
+ strike: bool | None = None,
1905
+ attributes: dict[str, str] | None = None,
1906
+ ) -> "HwpxOxmlRun":
1907
+ """Append a run to the note body paragraph (delegates to body_paragraph.add_run)."""
1908
+ return self.body_paragraph.add_run(
1909
+ text,
1910
+ char_pr_id_ref=char_pr_id_ref,
1911
+ bold=bold,
1912
+ italic=italic,
1913
+ underline=underline,
1914
+ color=color,
1915
+ font=font,
1916
+ size=size,
1917
+ highlight=highlight,
1918
+ strike=strike,
1919
+ attributes=attributes,
1920
+ )
1921
+
1922
+ def add_hyperlink(
1923
+ self,
1924
+ url: str,
1925
+ display_text: str,
1926
+ *,
1927
+ char_pr_id_ref: str | int | None = None,
1928
+ ) -> "HwpxOxmlInlineObject":
1929
+ """Append a hyperlink to the note body paragraph.
1930
+
1931
+ Convenience wrapper around ``body_paragraph.add_hyperlink``.
1932
+ """
1933
+ return self.body_paragraph.add_hyperlink(
1934
+ url, display_text, char_pr_id_ref=char_pr_id_ref
1935
+ )
1936
+
1875
1937
 
1876
1938
  def _default_sublist_attributes() -> dict[str, str]:
1877
1939
  """Return standard attributes for a ``<hp:subList>`` element.
@@ -2425,6 +2487,9 @@ class HwpxOxmlTableCell:
2425
2487
 
2426
2488
  @property
2427
2489
  def text(self) -> str:
2490
+ paragraphs = self.paragraphs
2491
+ if paragraphs:
2492
+ return "\n".join(paragraph.text or "" for paragraph in paragraphs)
2428
2493
  parts: list[str] = []
2429
2494
  for t_elem in self.element.findall(f".//{_HP}t"):
2430
2495
  if t_elem.text:
@@ -2433,8 +2498,79 @@ class HwpxOxmlTableCell:
2433
2498
 
2434
2499
  @text.setter
2435
2500
  def text(self, value: str) -> None:
2501
+ self.set_text(value)
2502
+
2503
+ def _first_run_char_pr_id_ref(self) -> str:
2504
+ for paragraph in self.paragraphs:
2505
+ for run in paragraph.runs:
2506
+ if run.char_pr_id_ref is not None:
2507
+ return str(run.char_pr_id_ref)
2508
+ return "0"
2509
+
2510
+ def _paragraph_format_attrs(self, paragraph: "HwpxOxmlParagraph" | None = None) -> dict[str, str]:
2511
+ source = paragraph.element if paragraph is not None else None
2512
+ attrs = dict(_default_cell_paragraph_attributes())
2513
+ if source is not None:
2514
+ for key in ("paraPrIDRef", "styleIDRef", "pageBreak", "columnBreak", "merged"):
2515
+ value = source.get(key)
2516
+ if value is not None:
2517
+ attrs[key] = value
2518
+ attrs["id"] = _paragraph_id()
2519
+ return attrs
2520
+
2521
+ def _run_char_pr_for_line(self, paragraphs: Sequence["HwpxOxmlParagraph"], index: int) -> str:
2522
+ if index < len(paragraphs):
2523
+ for run in paragraphs[index].runs:
2524
+ if run.char_pr_id_ref is not None:
2525
+ return str(run.char_pr_id_ref)
2526
+ return self._first_run_char_pr_id_ref()
2527
+
2528
+ def _set_split_paragraph_text(self, value: str) -> None:
2529
+ sublist = self._ensure_sublist()
2530
+ existing = self.paragraphs
2531
+ lines = (value or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
2532
+ if not lines:
2533
+ lines = [""]
2534
+
2535
+ for paragraph in list(sublist.findall(f"{_HP}p")):
2536
+ sublist.remove(paragraph)
2537
+
2538
+ for index, line in enumerate(lines):
2539
+ source = existing[index] if index < len(existing) else existing[0] if existing else None
2540
+ paragraph = _append_child(sublist, f"{_HP}p", self._paragraph_format_attrs(source))
2541
+ run = _append_child(
2542
+ paragraph,
2543
+ f"{_HP}run",
2544
+ {"charPrIDRef": self._run_char_pr_for_line(existing, index)},
2545
+ )
2546
+ _append_text_with_tabs(run, line)
2547
+
2548
+ def set_text(
2549
+ self,
2550
+ value: str,
2551
+ *,
2552
+ preserve_format: bool = True,
2553
+ split_paragraphs: bool = False,
2554
+ ) -> None:
2555
+ if split_paragraphs:
2556
+ self._set_split_paragraph_text(value)
2557
+ self.element.set("dirty", "1")
2558
+ self.table.mark_dirty()
2559
+ return
2560
+
2436
2561
  text_element = self._ensure_text_element()
2437
2562
  text_element.text = _sanitize_text(value)
2563
+ for node in self.element.findall(f".//{_HP}t"):
2564
+ if node is text_element:
2565
+ continue
2566
+ if node.text:
2567
+ node.text = ""
2568
+ if not preserve_format:
2569
+ run = text_element
2570
+ while run is not None and _element_local_name(run) != "run":
2571
+ run = run.getparent() if hasattr(run, "getparent") else None
2572
+ if run is not None:
2573
+ run.set("charPrIDRef", "0")
2438
2574
  self.element.set("dirty", "1")
2439
2575
  self.table.mark_dirty()
2440
2576
 
@@ -2898,6 +3034,8 @@ class HwpxOxmlTable:
2898
3034
  *,
2899
3035
  logical: bool = False,
2900
3036
  split_merged: bool = False,
3037
+ preserve_format: bool = True,
3038
+ split_paragraphs: bool = False,
2901
3039
  ) -> None:
2902
3040
  if logical:
2903
3041
  entry = self._grid_entry(row_index, col_index)
@@ -2907,7 +3045,11 @@ class HwpxOxmlTable:
2907
3045
  cell = entry.cell
2908
3046
  else:
2909
3047
  cell = self.cell(row_index, col_index)
2910
- cell.text = text
3048
+ cell.set_text(
3049
+ text,
3050
+ preserve_format=preserve_format,
3051
+ split_paragraphs=split_paragraphs,
3052
+ )
2911
3053
 
2912
3054
  def split_merged_cell(
2913
3055
  self, row_index: int, col_index: int
@@ -3797,7 +3939,10 @@ class HwpxOxmlParagraph:
3797
3939
  sublist = _append_child(note_element, f"{_HP}subList", _default_sublist_attributes())
3798
3940
  p_attrs = {"id": _paragraph_id(), **_DEFAULT_PARAGRAPH_ATTRS}
3799
3941
  paragraph = _append_child(sublist, f"{_HP}p", p_attrs)
3800
- note_run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": "0"})
3942
+ # 본문 run의 charPrIDRef도 인자를 따라가도록 적용 (host run 동일 스타일).
3943
+ # None이면 "0"(default).
3944
+ body_cpr = "0" if char_pr_id_ref is None else str(char_pr_id_ref)
3945
+ note_run = _append_child(paragraph, f"{_HP}run", {"charPrIDRef": body_cpr})
3801
3946
  t = _append_child(note_run, f"{_HP}t", {})
3802
3947
  t.text = _sanitize_text(text)
3803
3948
  self.section.mark_dirty()