python-hwpx 2.22.0__tar.gz → 2.23.0__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 (275) hide show
  1. python_hwpx-2.23.0/PKG-INFO +213 -0
  2. python_hwpx-2.23.0/README.md +171 -0
  3. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/pyproject.toml +1 -1
  4. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/table_patch.py +111 -7
  5. python_hwpx-2.23.0/src/python_hwpx.egg-info/PKG-INFO +213 -0
  6. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_table_patch.py +58 -0
  7. python_hwpx-2.22.0/PKG-INFO +0 -467
  8. python_hwpx-2.22.0/README.md +0 -425
  9. python_hwpx-2.22.0/src/python_hwpx.egg-info/PKG-INFO +0 -467
  10. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/LICENSE +0 -0
  11. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/NOTICE +0 -0
  12. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/setup.cfg +0 -0
  13. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/__init__.py +0 -0
  14. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/authoring.py +0 -0
  15. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/builder/__init__.py +0 -0
  16. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/builder/core.py +0 -0
  17. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/builder/report.py +0 -0
  18. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/__init__.py +0 -0
  19. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/badges.py +0 -0
  20. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  21. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  22. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  23. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  24. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/corpus.py +0 -0
  25. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/report.py +0 -0
  26. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  27. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/conformance/runner.py +0 -0
  28. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  29. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/__init__.py +0 -0
  30. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/_support.py +0 -0
  31. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/composer.py +0 -0
  32. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/harvest.py +0 -0
  33. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/plan.py +0 -0
  34. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profile.py +0 -0
  35. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  36. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  37. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  38. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  39. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  40. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  41. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  42. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  43. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  44. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  45. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  46. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  47. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  48. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  49. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  50. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  51. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  52. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  53. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  54. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  55. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  56. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  57. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  58. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/design/validator.py +0 -0
  59. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/document.py +0 -0
  60. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/__init__.py +0 -0
  61. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/compose.py +0 -0
  62. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/ir.py +0 -0
  63. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/measure.py +0 -0
  64. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/parser.py +0 -0
  65. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/exam/profile.py +0 -0
  66. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fill.py +0 -0
  67. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/__init__.py +0 -0
  68. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/apply.py +0 -0
  69. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/engine.py +0 -0
  70. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/measure.py +0 -0
  71. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/policy.py +0 -0
  72. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/report.py +0 -0
  73. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/seal.py +0 -0
  74. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/form_fit/wordbox.py +0 -0
  75. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/layout/__init__.py +0 -0
  76. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/layout/lint.py +0 -0
  77. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/layout/report.py +0 -0
  78. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/opc/package.py +0 -0
  79. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/opc/relationships.py +0 -0
  80. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/opc/security.py +0 -0
  81. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/opc/xml_utils.py +0 -0
  82. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/__init__.py +0 -0
  83. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/_document_impl.py +0 -0
  84. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/body.py +0 -0
  85. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  86. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/common.py +0 -0
  87. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/document.py +0 -0
  88. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/header.py +0 -0
  89. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/header_part.py +0 -0
  90. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/memo.py +0 -0
  91. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/namespaces.py +0 -0
  92. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/numbering.py +0 -0
  93. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/objects.py +0 -0
  94. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/paragraph.py +0 -0
  95. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/parser.py +0 -0
  96. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/run.py +0 -0
  97. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/schema.py +0 -0
  98. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/section.py +0 -0
  99. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/simple_parts.py +0 -0
  100. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/table.py +0 -0
  101. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/oxml/utils.py +0 -0
  102. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/package.py +0 -0
  103. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/patch.py +0 -0
  104. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/presets/__init__.py +0 -0
  105. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/presets/proposal.py +0 -0
  106. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/py.typed +0 -0
  107. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/quality/__init__.py +0 -0
  108. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/quality/ledger.py +0 -0
  109. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/quality/policy.py +0 -0
  110. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/quality/report.py +0 -0
  111. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/quality/save_pipeline.py +0 -0
  112. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/template_formfit.py +0 -0
  113. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/templates.py +0 -0
  114. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/__init__.py +0 -0
  115. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  116. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  117. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/advanced_generators.py +0 -0
  118. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/archive_cli.py +0 -0
  119. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/doc_diff.py +0 -0
  120. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/exporter.py +0 -0
  121. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  122. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  123. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  124. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  125. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  126. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  127. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/generic_inventory.py +0 -0
  128. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/id_integrity.py +0 -0
  129. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/idempotence.py +0 -0
  130. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/ir_equality.py +0 -0
  131. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/layout_preview.py +0 -0
  132. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/mail_merge.py +0 -0
  133. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/markdown_export.py +0 -0
  134. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/object_finder.py +0 -0
  135. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/official_lint.py +0 -0
  136. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/package_reconcile.py +0 -0
  137. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/package_validator.py +0 -0
  138. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/page_guard.py +0 -0
  139. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/pii.py +0 -0
  140. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/read_fidelity.py +0 -0
  141. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/recover.py +0 -0
  142. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/redline.py +0 -0
  143. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/repair.py +0 -0
  144. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/report_parser.py +0 -0
  145. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/report_utils.py +0 -0
  146. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  147. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/style_profile.py +0 -0
  148. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/table_cleanup.py +0 -0
  149. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/table_compute.py +0 -0
  150. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/table_navigation.py +0 -0
  151. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/template_analyzer.py +0 -0
  152. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  153. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/text_extractor.py +0 -0
  154. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/toc_author.py +0 -0
  155. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  156. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/tools/validator.py +0 -0
  157. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/__init__.py +0 -0
  158. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
  159. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
  160. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  161. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  162. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/detectors.py +0 -0
  163. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/diff.py +0 -0
  164. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/masks.py +0 -0
  165. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/oracle.py +0 -0
  166. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/hwpx/visual/report.py +0 -0
  167. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/python_hwpx.egg-info/SOURCES.txt +0 -0
  168. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  169. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  170. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  171. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  172. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_advanced_generators.py +0 -0
  173. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_authoring_profile_routing.py +0 -0
  174. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  175. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_authoring_render_check.py +0 -0
  176. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_builder_core.py +0 -0
  177. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_builder_plan_v2.py +0 -0
  178. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_builder_vertical_slice.py +0 -0
  179. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_comment_node_robustness.py +0 -0
  180. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_conformance.py +0 -0
  181. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_corpus_read_fidelity.py +0 -0
  182. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_coverage_promotion.py +0 -0
  183. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_coverage_targets.py +0 -0
  184. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_design_builder.py +0 -0
  185. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_deviations_registry.py +0 -0
  186. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_doc_diff.py +0 -0
  187. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_document_context_manager.py +0 -0
  188. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_document_formatting.py +0 -0
  189. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_document_plan.py +0 -0
  190. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_document_plan_computed_fields.py +0 -0
  191. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_document_save_api.py +0 -0
  192. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_compose.py +0 -0
  193. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_compose_oracle.py +0 -0
  194. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_fixtures.py +0 -0
  195. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_ir.py +0 -0
  196. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_measure.py +0 -0
  197. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_parser.py +0 -0
  198. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_exam_profile.py +0 -0
  199. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_existing_document_format_editing.py +0 -0
  200. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fields.py +0 -0
  201. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fill_split_run.py +0 -0
  202. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fit.py +0 -0
  203. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fit_integration.py +0 -0
  204. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fit_seal.py +0 -0
  205. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fit_seal_placement.py +0 -0
  206. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_form_fit_wordbox.py +0 -0
  207. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_fuzz_loop.py +0 -0
  208. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_fuzz_regressions.py +0 -0
  209. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_gap_closure_tools.py +0 -0
  210. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_government_report_preset.py +0 -0
  211. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_government_table_profile.py +0 -0
  212. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_hp_tab_support.py +0 -0
  213. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  214. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_id_generator_range.py +0 -0
  215. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_id_integrity.py +0 -0
  216. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_image_object_workflow.py +0 -0
  217. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_inline_models.py +0 -0
  218. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  219. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_integration_roundtrip.py +0 -0
  220. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_kordoc_absorption.py +0 -0
  221. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_layout_lint.py +0 -0
  222. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_layout_preview.py +0 -0
  223. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_mail_merge_fit.py +0 -0
  224. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_mail_merge_table_compute.py +0 -0
  225. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_mail_merge_xlsx.py +0 -0
  226. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_markdown_export.py +0 -0
  227. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_memo_and_style_editing.py +0 -0
  228. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_namespace_handling.py +0 -0
  229. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_new_features.py +0 -0
  230. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_official_document_style.py +0 -0
  231. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  232. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_official_lint_tableaware.py +0 -0
  233. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_opc_package.py +0 -0
  234. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_open_safety_corpus.py +0 -0
  235. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_openrate.py +0 -0
  236. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_oxml_parsing.py +0 -0
  237. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_packaging_license_metadata.py +0 -0
  238. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_packaging_py_typed.py +0 -0
  239. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_paragraph_keep_together.py +0 -0
  240. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_paragraph_section_management.py +0 -0
  241. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_pii.py +0 -0
  242. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_pii_structural.py +0 -0
  243. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_pii_wiring.py +0 -0
  244. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_proposal_preset.py +0 -0
  245. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_question_split_detector.py +0 -0
  246. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_read_fidelity.py +0 -0
  247. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_recover_broken_zip.py +0 -0
  248. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_redline_authoring.py +0 -0
  249. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_redline_verify.py +0 -0
  250. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_repair_repack.py +0 -0
  251. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_report_parser.py +0 -0
  252. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_report_utils.py +0 -0
  253. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_repr_snapshots.py +0 -0
  254. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_rhwp_t1_gates.py +0 -0
  255. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_rhwp_t2_verification.py +0 -0
  256. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_roundtrip_fidelity.py +0 -0
  257. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_save_pipeline.py +0 -0
  258. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  259. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_section_headers.py +0 -0
  260. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_set_paragraph_format_keep.py +0 -0
  261. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_skeleton_template_ids.py +0 -0
  262. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_split_merged_cell.py +0 -0
  263. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_style_profile.py +0 -0
  264. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_table_cleanup.py +0 -0
  265. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_table_navigation.py +0 -0
  266. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_tables_default_border.py +0 -0
  267. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_template_analyzer_enrichment.py +0 -0
  268. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_template_formfit.py +0 -0
  269. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_text_extractor_annotations.py +0 -0
  270. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_toc_author.py +0 -0
  271. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_toc_fidelity.py +0 -0
  272. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_validation_severity.py +0 -0
  273. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_validator_comment_nodes.py +0 -0
  274. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_version_metadata.py +0 -0
  275. {python_hwpx-2.22.0 → python_hwpx-2.23.0}/tests/test_visual_oracle.py +0 -0
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-hwpx
3
+ Version: 2.23.0
4
+ Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
+ Author: python-hwpx Maintainers
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/airmang/python-hwpx
8
+ Project-URL: Documentation, https://airmang.github.io/python-hwpx/
9
+ Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
10
+ Keywords: hwp,hwpx,hancom,opc,xml,document-automation,validation,template
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Text Processing :: Markup :: XML
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ License-File: NOTICE
23
+ Requires-Dist: lxml<6,>=4.9
24
+ Provides-Extra: visual
25
+ Requires-Dist: pymupdf>=1.24; extra == "visual"
26
+ Requires-Dist: pillow>=10.0; extra == "visual"
27
+ Requires-Dist: numpy>=1.26; extra == "visual"
28
+ Provides-Extra: xlsx
29
+ Requires-Dist: openpyxl>=3.1; extra == "xlsx"
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.0; extra == "dev"
32
+ Requires-Dist: twine>=4.0; extra == "dev"
33
+ Requires-Dist: pytest>=7.4; extra == "dev"
34
+ Provides-Extra: test
35
+ Requires-Dist: build>=1.0; extra == "test"
36
+ Requires-Dist: pytest>=7.4; extra == "test"
37
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
38
+ Provides-Extra: typecheck
39
+ Requires-Dist: mypy>=1.10; extra == "typecheck"
40
+ Requires-Dist: pyright>=1.1.390; extra == "typecheck"
41
+ Dynamic: license-file
42
+
43
+ <p align="center">
44
+ <h1 align="center">python-hwpx</h1>
45
+ <p align="center">
46
+ <strong>한글 없이 HWPX 문서를 Python으로 읽고, 편집하고, 생성하고, 검증합니다.</strong>
47
+ </p>
48
+ <p align="center">
49
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
50
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
51
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
52
+ <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
53
+ </p>
54
+ </p>
55
+
56
+ ---
57
+
58
+ ## 🧩 HWPX Stack (3종)
59
+
60
+ | 계층 | 레포 | 역할 |
61
+ |---|---|---|
62
+ | 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
63
+ | 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
64
+ | 🎯 에이전트 스킬 | [`hwpx-skill`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 공식 온보딩 스킬 |
65
+
66
+ ---
67
+
68
+ ## 왜 python-hwpx인가
69
+
70
+ - **한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
71
+ - **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
72
+ - **에이전트·자동화 친화** — 같은 스택 위에서 `hwpx-mcp-server`와 공식 스킬이 직결됩니다.
73
+
74
+ ## 빠른 시작
75
+
76
+ ```bash
77
+ pip install python-hwpx # Python 3.10+ · lxml ≥ 4.9
78
+ ```
79
+
80
+ ```python
81
+ from hwpx import HwpxDocument
82
+
83
+ # 기존 문서 열기 → 편집 → 저장
84
+ doc = HwpxDocument.open("보고서.hwpx")
85
+ doc.add_paragraph("자동화로 추가한 문단입니다.")
86
+ doc.save_to_path("보고서-수정.hwpx")
87
+
88
+ # 새 문서 만들기
89
+ new = HwpxDocument.new()
90
+ new.add_paragraph("python-hwpx로 만든 새 문서")
91
+ new.save_to_path("새문서.hwpx")
92
+ ```
93
+
94
+ > 💡 컨텍스트 매니저도 지원합니다 — `with` 블록을 벗어나면 리소스가 자동 정리됩니다:
95
+ > ```python
96
+ > with HwpxDocument.open("보고서.hwpx") as doc:
97
+ > doc.add_paragraph("자동으로 리소스가 정리됩니다.")
98
+ > doc.save_to_path("결과물.hwpx")
99
+ > ```
100
+
101
+ `open`/`new` → `edit`/`extract` → `save_to_path` 흐름만 잡으면 나머지는 필요할 때 확장하면 됩니다.
102
+
103
+ ## 무엇을 하나
104
+
105
+ ### 🔍 읽기 · 추출
106
+ - 텍스트/HTML/Markdown 내보내기 — `export_text()` · `export_html()` · `export_markdown()`
107
+ - **풍부한 Markdown** — `export_rich_markdown()`은 인라인 서식(`**굵게**`·`*기울임*`·`~~취소선~~`), 중첩 표(colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존
108
+ - `TextExtractor` / `ObjectFinder` — 섹션·문단 순회, 태그·속성·XPath로 객체 탐색 (`hp:tab`은 `\t`로 보존, roundtrip 안전)
109
+
110
+ ```python
111
+ doc = HwpxDocument.open("보고서.hwpx")
112
+ md = doc.export_rich_markdown(
113
+ image_dir="out/images", # BinData 이미지를 디스크에 추출
114
+ image_ref_prefix="images/", # 마크다운 내 ![](images/...) 경로 접두
115
+ detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
116
+ )
117
+ ```
118
+
119
+ ### ✏️ 편집
120
+ - 문단 추가/삭제/서식, Run 단위 볼드·이탤릭·밑줄·색상
121
+ - 섹션 추가/삭제(`add_section(after=)`·`remove_section()`, manifest 자동 관리)
122
+ - 표 생성·셀 텍스트·병합/분할·중첩 테이블, 이미지 임베드, 머리글/바닥글, 메모(앵커 기반), 각주/미주, 북마크/하이퍼링크, 다단 편집
123
+ - **기존 문서 서식 편집** — 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 쪽번호, 불릿/번호
124
+ - **스타일 기반 치환** — 색상·밑줄·`charPrIDRef`로 Run을 필터링해 선택 교체(`replace_text_in_runs`·`find_runs_by_style`)
125
+
126
+ ```python
127
+ # 빨간색 텍스트만 찾아서 치환
128
+ doc.replace_text_in_runs("임시", "확정", text_color="#FF0000")
129
+ ```
130
+
131
+ ### 🖊️ 양식 채우기 (byte-preserving)
132
+ - 누름틀(클릭히어) 필드 조회·서식 보존 채움, 라벨 기반 셀 탐색(`find_cell_by_label`)·경로 채우기(`fill_by_path`)
133
+ - **바이트 보존 구조 편집** — 셀 채우기 / 행·열·표 삭제·삽입 / 열 너비 오토핏 / 폰트 shrink-to-fit 을 문서 재조립 없이 수행해 양식 서식을 그대로 보존. 미수정 영역은 `hwpx.patch`가 section XML 바이트를 splice해 손대지 않음
134
+
135
+ ```python
136
+ doc = HwpxDocument.open("신청서.hwpx")
137
+ result = doc.fill_by_path({
138
+ "성명 > right": "홍길동",
139
+ "소속 > right": "플랫폼팀",
140
+ })
141
+ doc.save_to_path("신청서-작성완료.hwpx")
142
+ print(result["applied_count"], result["failed_count"])
143
+ ```
144
+
145
+ ### 🏗️ 생성 · 공문서 도구
146
+ - `hwpx.builder` — Section/Heading/Table/Image/Header 조립형 생성 + 하드게이트 저장 리포트
147
+ - 공문서 도구 — `official_lint`(항목기호 위계·"끝." 표시·붙임·날짜 lint), 결재란 프리셋
148
+ - `advanced_generators` — 사진대지(image_grid)·회의 명패·표 기반 조직도
149
+ - `mail_merge` — 템플릿+데이터 N부 대량 생성, 표 합계·평균 계산
150
+ - `doc_diff` — 문단 LCS diff·신구대조표·참조 정합 lint
151
+ - `style_profile` — 참조 문서 프로파일 추출·적용, 템플릿 레지스트리
152
+
153
+ ### ✅ 검증 · 안전 · 저수준
154
+ - XSD 스키마 + 패키지 구조 검증 — CLI `hwpx-validate` · `hwpx-validate-package`, `hwpx-analyze-template`
155
+ - `validate_editor_open_safety` — 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환
156
+ - `hwpx.tools.fuzz`(시드 결정적 시나리오·3중 오라클) · `hwpx.tools.layout_preview`(페이지 박스 근사 HTML/PNG 자기검증) · `opc.security`(XML entity·ZIP 압축 폭탄 가드)
157
+ - `hwpx.oxml` 데이터클래스로 OWPML 스키마 ↔ Python 객체 직접 조작, HWPML 2016→2011 네임스페이스 자동 정규화
158
+
159
+ ```bash
160
+ hwpx-validate-package 보고서.hwpx
161
+ hwpx-analyze-template 보고서.hwpx
162
+ ```
163
+
164
+ > 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
165
+
166
+ ## 대항 라이브러리 비교
167
+
168
+ | | python-hwpx | pyhwpx | pyhwp |
169
+ |---|---|---|---|
170
+ | **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
171
+ | **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
172
+ | **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
173
+ | **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
174
+ | **스키마 검증** | ✅ | ❌ | ❌ |
175
+ | **AI 에이전트 연동 (MCP)** | ✅ `hwpx-mcp-server` | ❌ | ❌ |
176
+
177
+ > HWP(v5 바이너리) 파일은 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
178
+
179
+ ## 알려진 제약
180
+
181
+ - `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다. 복잡한 개체 추가 시 한/글에서 열어 검증하세요.
182
+ - 이미지 바이너리 임베드는 지원하지만 `<hp:pic>` 요소의 완전 자동 생성은 제공하지 않습니다.
183
+ - 암호화된 HWPX 파일의 암복호화는 지원하지 않습니다.
184
+
185
+ ## 더 보기
186
+
187
+ - **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
188
+ - **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
189
+ - **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
190
+ - **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
191
+ - **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
192
+
193
+ ## 기여하기
194
+
195
+ 버그 리포트, 기능 제안, PR 모두 환영합니다.
196
+
197
+ ```bash
198
+ git clone https://github.com/airmang/python-hwpx.git
199
+ cd python-hwpx
200
+ pip install -e ".[dev]"
201
+ pytest
202
+ ```
203
+
204
+ ## License
205
+
206
+ Apache License 2.0. See LICENSE and NOTICE.
207
+
208
+ ## Maintainer
209
+
210
+ Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사
211
+
212
+ - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
213
+ - 🐙 [@airmang](https://github.com/airmang)
@@ -0,0 +1,171 @@
1
+ <p align="center">
2
+ <h1 align="center">python-hwpx</h1>
3
+ <p align="center">
4
+ <strong>한글 없이 HWPX 문서를 Python으로 읽고, 편집하고, 생성하고, 검증합니다.</strong>
5
+ </p>
6
+ <p align="center">
7
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
8
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
9
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
10
+ <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
11
+ </p>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ## 🧩 HWPX Stack (3종)
17
+
18
+ | 계층 | 레포 | 역할 |
19
+ |---|---|---|
20
+ | 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
21
+ | 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
22
+ | 🎯 에이전트 스킬 | [`hwpx-skill`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 공식 온보딩 스킬 |
23
+
24
+ ---
25
+
26
+ ## 왜 python-hwpx인가
27
+
28
+ - **한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
29
+ - **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
30
+ - **에이전트·자동화 친화** — 같은 스택 위에서 `hwpx-mcp-server`와 공식 스킬이 직결됩니다.
31
+
32
+ ## 빠른 시작
33
+
34
+ ```bash
35
+ pip install python-hwpx # Python 3.10+ · lxml ≥ 4.9
36
+ ```
37
+
38
+ ```python
39
+ from hwpx import HwpxDocument
40
+
41
+ # 기존 문서 열기 → 편집 → 저장
42
+ doc = HwpxDocument.open("보고서.hwpx")
43
+ doc.add_paragraph("자동화로 추가한 문단입니다.")
44
+ doc.save_to_path("보고서-수정.hwpx")
45
+
46
+ # 새 문서 만들기
47
+ new = HwpxDocument.new()
48
+ new.add_paragraph("python-hwpx로 만든 새 문서")
49
+ new.save_to_path("새문서.hwpx")
50
+ ```
51
+
52
+ > 💡 컨텍스트 매니저도 지원합니다 — `with` 블록을 벗어나면 리소스가 자동 정리됩니다:
53
+ > ```python
54
+ > with HwpxDocument.open("보고서.hwpx") as doc:
55
+ > doc.add_paragraph("자동으로 리소스가 정리됩니다.")
56
+ > doc.save_to_path("결과물.hwpx")
57
+ > ```
58
+
59
+ `open`/`new` → `edit`/`extract` → `save_to_path` 흐름만 잡으면 나머지는 필요할 때 확장하면 됩니다.
60
+
61
+ ## 무엇을 하나
62
+
63
+ ### 🔍 읽기 · 추출
64
+ - 텍스트/HTML/Markdown 내보내기 — `export_text()` · `export_html()` · `export_markdown()`
65
+ - **풍부한 Markdown** — `export_rich_markdown()`은 인라인 서식(`**굵게**`·`*기울임*`·`~~취소선~~`), 중첩 표(colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존
66
+ - `TextExtractor` / `ObjectFinder` — 섹션·문단 순회, 태그·속성·XPath로 객체 탐색 (`hp:tab`은 `\t`로 보존, roundtrip 안전)
67
+
68
+ ```python
69
+ doc = HwpxDocument.open("보고서.hwpx")
70
+ md = doc.export_rich_markdown(
71
+ image_dir="out/images", # BinData 이미지를 디스크에 추출
72
+ image_ref_prefix="images/", # 마크다운 내 ![](images/...) 경로 접두
73
+ detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
74
+ )
75
+ ```
76
+
77
+ ### ✏️ 편집
78
+ - 문단 추가/삭제/서식, Run 단위 볼드·이탤릭·밑줄·색상
79
+ - 섹션 추가/삭제(`add_section(after=)`·`remove_section()`, manifest 자동 관리)
80
+ - 표 생성·셀 텍스트·병합/분할·중첩 테이블, 이미지 임베드, 머리글/바닥글, 메모(앵커 기반), 각주/미주, 북마크/하이퍼링크, 다단 편집
81
+ - **기존 문서 서식 편집** — 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 쪽번호, 불릿/번호
82
+ - **스타일 기반 치환** — 색상·밑줄·`charPrIDRef`로 Run을 필터링해 선택 교체(`replace_text_in_runs`·`find_runs_by_style`)
83
+
84
+ ```python
85
+ # 빨간색 텍스트만 찾아서 치환
86
+ doc.replace_text_in_runs("임시", "확정", text_color="#FF0000")
87
+ ```
88
+
89
+ ### 🖊️ 양식 채우기 (byte-preserving)
90
+ - 누름틀(클릭히어) 필드 조회·서식 보존 채움, 라벨 기반 셀 탐색(`find_cell_by_label`)·경로 채우기(`fill_by_path`)
91
+ - **바이트 보존 구조 편집** — 셀 채우기 / 행·열·표 삭제·삽입 / 열 너비 오토핏 / 폰트 shrink-to-fit 을 문서 재조립 없이 수행해 양식 서식을 그대로 보존. 미수정 영역은 `hwpx.patch`가 section XML 바이트를 splice해 손대지 않음
92
+
93
+ ```python
94
+ doc = HwpxDocument.open("신청서.hwpx")
95
+ result = doc.fill_by_path({
96
+ "성명 > right": "홍길동",
97
+ "소속 > right": "플랫폼팀",
98
+ })
99
+ doc.save_to_path("신청서-작성완료.hwpx")
100
+ print(result["applied_count"], result["failed_count"])
101
+ ```
102
+
103
+ ### 🏗️ 생성 · 공문서 도구
104
+ - `hwpx.builder` — Section/Heading/Table/Image/Header 조립형 생성 + 하드게이트 저장 리포트
105
+ - 공문서 도구 — `official_lint`(항목기호 위계·"끝." 표시·붙임·날짜 lint), 결재란 프리셋
106
+ - `advanced_generators` — 사진대지(image_grid)·회의 명패·표 기반 조직도
107
+ - `mail_merge` — 템플릿+데이터 N부 대량 생성, 표 합계·평균 계산
108
+ - `doc_diff` — 문단 LCS diff·신구대조표·참조 정합 lint
109
+ - `style_profile` — 참조 문서 프로파일 추출·적용, 템플릿 레지스트리
110
+
111
+ ### ✅ 검증 · 안전 · 저수준
112
+ - XSD 스키마 + 패키지 구조 검증 — CLI `hwpx-validate` · `hwpx-validate-package`, `hwpx-analyze-template`
113
+ - `validate_editor_open_safety` — 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환
114
+ - `hwpx.tools.fuzz`(시드 결정적 시나리오·3중 오라클) · `hwpx.tools.layout_preview`(페이지 박스 근사 HTML/PNG 자기검증) · `opc.security`(XML entity·ZIP 압축 폭탄 가드)
115
+ - `hwpx.oxml` 데이터클래스로 OWPML 스키마 ↔ Python 객체 직접 조작, HWPML 2016→2011 네임스페이스 자동 정규화
116
+
117
+ ```bash
118
+ hwpx-validate-package 보고서.hwpx
119
+ hwpx-analyze-template 보고서.hwpx
120
+ ```
121
+
122
+ > 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
123
+
124
+ ## 대항 라이브러리 비교
125
+
126
+ | | python-hwpx | pyhwpx | pyhwp |
127
+ |---|---|---|---|
128
+ | **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
129
+ | **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
130
+ | **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
131
+ | **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
132
+ | **스키마 검증** | ✅ | ❌ | ❌ |
133
+ | **AI 에이전트 연동 (MCP)** | ✅ `hwpx-mcp-server` | ❌ | ❌ |
134
+
135
+ > HWP(v5 바이너리) 파일은 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
136
+
137
+ ## 알려진 제약
138
+
139
+ - `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다. 복잡한 개체 추가 시 한/글에서 열어 검증하세요.
140
+ - 이미지 바이너리 임베드는 지원하지만 `<hp:pic>` 요소의 완전 자동 생성은 제공하지 않습니다.
141
+ - 암호화된 HWPX 파일의 암복호화는 지원하지 않습니다.
142
+
143
+ ## 더 보기
144
+
145
+ - **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
146
+ - **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
147
+ - **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
148
+ - **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
149
+ - **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
150
+
151
+ ## 기여하기
152
+
153
+ 버그 리포트, 기능 제안, PR 모두 환영합니다.
154
+
155
+ ```bash
156
+ git clone https://github.com/airmang/python-hwpx.git
157
+ cd python-hwpx
158
+ pip install -e ".[dev]"
159
+ pytest
160
+ ```
161
+
162
+ ## License
163
+
164
+ Apache License 2.0. See LICENSE and NOTICE.
165
+
166
+ ## Maintainer
167
+
168
+ Primary maintainer/contact: **고규현** — 광교고등학교 정보·컴퓨터 교사
169
+
170
+ - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
171
+ - 🐙 [@airmang](https://github.com/airmang)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "2.22.0"
7
+ version = "2.23.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -196,6 +196,80 @@ def _all_paragraph_spans(cell: bytes) -> list[tuple[int, int]]:
196
196
  return [(m.start(), m.end()) for m in _P_SPAN_RE.finditer(masked)]
197
197
 
198
198
 
199
+ # --- font shrink-to-fit helpers (byte-preserving charPr materialisation) ------
200
+
201
+ def _header_part_name(parts: Mapping[str, bytes]) -> str | None:
202
+ return next((n for n in parts if n.endswith("header.xml")), None)
203
+
204
+ def _charpr_height(header: bytes, cid: str) -> int | None:
205
+ m = re.search(rb'<(?:[A-Za-z_][\w.-]*:)?charPr\b[^>]*?\bid="' + re.escape(cid.encode()) + rb'"[^>]*?\bheight="(\d+)"', header)
206
+ return int(m.group(1)) if m else None
207
+
208
+ def _cell_run_charpr(cell: bytes) -> str | None:
209
+ m = re.search(rb'<(?:[A-Za-z_][\w.-]*:)?run\b[^>]*?\bcharPrIDRef="(\d+)"', cell)
210
+ return m.group(1).decode() if m else None
211
+
212
+ def _cell_inner_width(cell: bytes) -> int:
213
+ w = _iattr(cell, "cellSz", "width") or 0
214
+ m = re.search(rb'<(?:[A-Za-z_][\w.-]*:)?cellMargin\b[^>]*?\bleft="(\d+)"[^>]*?\bright="(\d+)"', cell)
215
+ left, right = (int(m.group(1)), int(m.group(2))) if m else (0, 0)
216
+ return max(w - left - right, 0)
217
+
218
+ def _materialize_charpr(header: bytes, base_id: str, new_height: int, cache: dict[tuple[str, int], str]) -> tuple[bytes, str]:
219
+ """Clone charPr *base_id* with *new_height*, append it to the charProperties
220
+ list (itemCnt bumped), return (new_header, new_charpr_id). Deduped via *cache*."""
221
+ key = (base_id, new_height)
222
+ if key in cache:
223
+ return header, cache[key]
224
+ bid = re.escape(base_id.encode())
225
+ m = re.search(rb'<(?:[A-Za-z_][\w.-]*:)?charPr\b[^>]*?\bid="' + bid + rb'".*?</(?:[A-Za-z_][\w.-]*:)?charPr>', header, re.DOTALL)
226
+ if m is None:
227
+ m = re.search(rb'<(?:[A-Za-z_][\w.-]*:)?charPr\b[^>]*?\bid="' + bid + rb'"[^>]*/>', header)
228
+ if m is None:
229
+ raise KeyError(f"base charPr {base_id} not found")
230
+ ids = [int(x) for x in re.findall(rb'<(?:[A-Za-z_][\w.-]*:)?charPr\b[^>]*?\bid="(\d+)"', header)]
231
+ new_id = str((max(ids) + 1) if ids else 0)
232
+ clone = re.sub(rb'(\bid=")\d+(")', rb'\g<1>' + new_id.encode() + rb'\g<2>', m.group(0), count=1)
233
+ if re.search(rb'\bheight="\d+"', clone):
234
+ clone = re.sub(rb'(\bheight=")\d+(")', rb'\g<1>' + str(new_height).encode() + rb'\g<2>', clone, count=1)
235
+ close = re.search(rb'</(?:[A-Za-z_][\w.-]*:)?charProperties>', header)
236
+ if close is None:
237
+ raise KeyError("charProperties close tag not found")
238
+ header = header[:close.start()] + clone + header[close.start():]
239
+ header = re.sub(
240
+ rb'(<(?:[A-Za-z_][\w.-]*:)?charProperties\b[^>]*?\bitemCnt=")(\d+)(")',
241
+ lambda mm: mm.group(1) + str(int(mm.group(2)) + 1).encode() + mm.group(3),
242
+ header, count=1,
243
+ )
244
+ cache[key] = new_id
245
+ return header, new_id
246
+
247
+ def _shrunk_font_id(header: bytes, cell_bytes: bytes, text: str, target_lines: int, min_font_pt: float,
248
+ cache: dict[tuple[str, int], str]) -> tuple[bytes, str, str] | None:
249
+ """If *text* needs more than *target_lines* at the cell's base font, decide a
250
+ shrink (form_fit FitEngine) and materialise it. Returns (new_header, base_id,
251
+ new_id) or None (no shrink needed / not resolvable)."""
252
+ from .form_fit.engine import FitEngine
253
+ from .form_fit.measure import SlotMetrics
254
+ from .form_fit.policy import FitPolicy
255
+
256
+ base_id = _cell_run_charpr(cell_bytes)
257
+ if base_id is None:
258
+ return None
259
+ inner = _cell_inner_width(cell_bytes)
260
+ base_h = _charpr_height(header, base_id)
261
+ if not inner or not base_h:
262
+ return None
263
+ base_pt = base_h / 100.0
264
+ slot = SlotMetrics(available_width=inner * 0.94, font_pt=base_pt, max_lines=target_lines)
265
+ result = FitEngine().fit(text, slot, FitPolicy(mode="wrap_then_shrink", max_lines=target_lines, min_font_pt=min_font_pt))
266
+ new_pt = result.font_pt
267
+ if not new_pt or new_pt >= base_pt - 1e-6:
268
+ return None
269
+ header, new_id = _materialize_charpr(header, base_id, int(round(new_pt * 100)), cache)
270
+ return header, base_id, new_id
271
+
272
+
199
273
  # --- public API ---------------------------------------------------------------
200
274
 
201
275
  @dataclass(frozen=True)
@@ -256,16 +330,17 @@ class CellFillResult:
256
330
  }
257
331
 
258
332
 
259
- def _normalize(cell: Mapping[str, Any] | Any) -> tuple[str, int, int, int, str]:
333
+ def _normalize(cell: Mapping[str, Any] | Any) -> tuple[str, int, int, int, str, int | None]:
260
334
  get = cell.get if isinstance(cell, Mapping) else (lambda k, d=None: getattr(cell, k, d))
261
335
  section = str(get("section_path") or get("sectionPath") or "Contents/section0.xml")
262
336
  table_index = get("table_index", get("tableIndex"))
263
337
  row = get("row")
264
338
  col = get("col")
265
339
  text = get("text")
340
+ max_lines = get("max_lines", get("maxLines"))
266
341
  if table_index is None or row is None or col is None or text is None:
267
342
  raise ValueError("cell fill requires table_index, row, col, text")
268
- return section, int(table_index), int(row), int(col), str(text)
343
+ return section, int(table_index), int(row), int(col), str(text), (int(max_lines) if max_lines else None)
269
344
 
270
345
 
271
346
  def fill_cells(
@@ -273,6 +348,8 @@ def fill_cells(
273
348
  cells: Sequence[Mapping[str, Any] | Any],
274
349
  *,
275
350
  output_path: str | Path | None = None,
351
+ fit_max_lines: int | None = None,
352
+ min_font_pt: float = 8.0,
276
353
  ) -> CellFillResult:
277
354
  """Byte-preserving fill of table cells by ``(table_index, row, col)`` address.
278
355
 
@@ -281,6 +358,12 @@ def fill_cells(
281
358
  parts and untouched tables round-trip byte-identical. Empty / self-closing
282
359
  cells are filled (text inserted), never silently reported as done. An
283
360
  unresolvable address is reported in ``skipped`` and mutates nothing.
361
+
362
+ **Font shrink-to-fit** (optional): when ``fit_max_lines`` is set (or a cell
363
+ carries ``max_lines``), a cell whose text would wrap past that many lines at
364
+ its template font is shrunk — the smallest font ``>= min_font_pt`` that fits
365
+ (form_fit FitEngine) is materialised as a real ``charPr`` and the cell's run
366
+ points at it. Column widths and the rest of the document are untouched.
284
367
  """
285
368
  source_bytes = _read_source_bytes(source)
286
369
  specs = [_normalize(c) for c in cells]
@@ -292,23 +375,28 @@ def fill_cells(
292
375
  with zipfile.ZipFile(io.BytesIO(source_bytes), "r") as zf:
293
376
  parts = {i.filename: zf.read(i.filename) for i in zf.infolist() if not i.is_dir()}
294
377
 
378
+ header_name = _header_part_name(parts)
379
+ header_xml = parts.get(header_name) if header_name else None
380
+ charpr_cache: dict[tuple[str, int], str] = {}
381
+ header_changed = False
382
+
295
383
  applied: list[CellApplied] = []
296
384
  skipped: list[CellSkipped] = []
297
- by_section: dict[str, list[tuple[int, int, int, str]]] = {}
298
- for section, ti, row, col, text in specs:
299
- by_section.setdefault(section, []).append((ti, row, col, text))
385
+ by_section: dict[str, list[tuple[int, int, int, str, int | None]]] = {}
386
+ for section, ti, row, col, text, mx in specs:
387
+ by_section.setdefault(section, []).append((ti, row, col, text, mx))
300
388
 
301
389
  changed_parts: dict[str, bytes] = {}
302
390
  for section_path, section_specs in by_section.items():
303
391
  section_xml = parts.get(section_path)
304
392
  if section_xml is None:
305
- skipped.extend(CellSkipped(section_path, ti, r, c, "section part not found") for ti, r, c, _ in section_specs)
393
+ skipped.extend(CellSkipped(section_path, ti, r, c, "section part not found") for ti, r, c, _t, _m in section_specs)
306
394
  continue
307
395
  table_spans = _iter_table_spans(section_xml)
308
396
  # accumulate byte edits over the whole section (table region splices)
309
397
  section_edits: list[tuple[int, int, bytes]] = []
310
398
  occupied: list[tuple[int, int]] = [] # filled paragraph spans (merge-collision guard)
311
- for ti, row, col, text in section_specs:
399
+ for ti, row, col, text, mx in section_specs:
312
400
  if ti < 0 or ti >= len(table_spans):
313
401
  skipped.append(CellSkipped(section_path, ti, row, col, "table_index out of range"))
314
402
  continue
@@ -324,6 +412,16 @@ def fill_cells(
324
412
  if not p_spans:
325
413
  skipped.append(CellSkipped(section_path, ti, row, col, "cell has no paragraph"))
326
414
  continue
415
+ # Font shrink-to-fit: a cell with a max-lines target whose text would
416
+ # wrap past it gets its run pointed at a smaller (materialised) charPr.
417
+ shrink: tuple[str, str] | None = None
418
+ target = mx or fit_max_lines
419
+ if target and header_xml is not None and text.strip():
420
+ res = _shrunk_font_id(header_xml, cell_bytes, text, target, min_font_pt, charpr_cache)
421
+ if res is not None:
422
+ header_xml, base_id, new_id = res
423
+ header_changed = True
424
+ shrink = (base_id, new_id)
327
425
  # Replace the WHOLE cell's visible text with `text`: line k -> paragraph k,
328
426
  # trailing paragraphs are emptied (so stale multi-line template content —
329
427
  # e.g. stacked 성취기준 codes — does not survive). newline-separated text
@@ -348,6 +446,9 @@ def fill_cells(
348
446
  collided = True
349
447
  break
350
448
  new_para = _strip_paragraph_layout_cache(replacement if (_s, _e) == (0, len(para)) else _apply_edits(para, [(_s, _e, replacement)]))
449
+ if shrink is not None:
450
+ base_id, new_id = shrink
451
+ new_para = re.sub(rb'(charPrIDRef=")' + re.escape(base_id.encode()) + rb'(")', rb"\g<1>" + new_id.encode() + rb"\g<2>", new_para)
351
452
  occupied.append((a0, a1))
352
453
  cell_edits.append((a0, a1, new_para))
353
454
  if collided:
@@ -362,6 +463,9 @@ def fill_cells(
362
463
  if new_section != section_xml:
363
464
  changed_parts[section_path] = new_section
364
465
 
466
+ if header_changed and header_name and header_xml is not None:
467
+ changed_parts[header_name] = header_xml
468
+
365
469
  if not changed_parts:
366
470
  open_safety, _ = _finalize(source_bytes, output_path, source=source)
367
471
  return CellFillResult(source_bytes, tuple(applied), tuple(skipped), (), True, "none", open_safety)