python-hwpx 5.4.0__tar.gz → 5.5.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 (218) hide show
  1. {python_hwpx-5.4.0/src/python_hwpx.egg-info → python_hwpx-5.5.0}/PKG-INFO +1 -1
  2. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/pyproject.toml +1 -1
  3. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/memo.py +26 -6
  4. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/paragraph.py +101 -10
  5. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/markdown_export.py +6 -2
  6. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/text_extractor.py +14 -0
  7. {python_hwpx-5.4.0 → python_hwpx-5.5.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  8. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
  9. python_hwpx-5.5.0/tests/test_note_authoring_contract.py +121 -0
  10. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/LICENSE +0 -0
  11. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/NOTICE +0 -0
  12. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/README.md +0 -0
  13. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/setup.cfg +0 -0
  14. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/__init__.py +0 -0
  15. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/__init__.py +0 -0
  16. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/_units.py +0 -0
  17. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/fields.py +0 -0
  18. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/layout.py +0 -0
  19. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/media.py +0 -0
  20. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/memos.py +0 -0
  21. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/persistence.py +0 -0
  22. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/shapes.py +0 -0
  23. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/_document/tracked.py +0 -0
  24. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/body_patch.py +0 -0
  25. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  26. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/document.py +0 -0
  27. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/__init__.py +0 -0
  28. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/authoring.py +0 -0
  29. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/eqedit.py +0 -0
  30. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/mathml.py +0 -0
  31. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/render.py +0 -0
  32. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/equation/tokens.py +0 -0
  33. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/errors.py +0 -0
  34. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/experimental.py +0 -0
  35. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/__init__.py +0 -0
  36. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/apply.py +0 -0
  37. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/engine.py +0 -0
  38. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/measure.py +0 -0
  39. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/policy.py +0 -0
  40. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/form_fit/report.py +0 -0
  41. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/ingest/__init__.py +0 -0
  42. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/ingest/base.py +0 -0
  43. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  44. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/layout/__init__.py +0 -0
  45. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/layout/lint.py +0 -0
  46. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/layout/report.py +0 -0
  47. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/mutation_report.py +0 -0
  48. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/opc/package.py +0 -0
  49. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/opc/relationships.py +0 -0
  50. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/opc/security.py +0 -0
  51. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/opc/xml_utils.py +0 -0
  52. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/__init__.py +0 -0
  53. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/_document_impl.py +0 -0
  54. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  55. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/body.py +0 -0
  56. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  57. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/color.py +0 -0
  58. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/common.py +0 -0
  59. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/document.py +0 -0
  60. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/document_parts.py +0 -0
  61. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/header.py +0 -0
  62. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/header_part.py +0 -0
  63. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/namespaces.py +0 -0
  64. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/numbering.py +0 -0
  65. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/objects.py +0 -0
  66. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/parser.py +0 -0
  67. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/run.py +0 -0
  68. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/schema.py +0 -0
  69. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/section.py +0 -0
  70. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/section_format.py +0 -0
  71. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/section_story.py +0 -0
  72. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/simple_parts.py +0 -0
  73. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/table.py +0 -0
  74. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/oxml/utils.py +0 -0
  75. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/package.py +0 -0
  76. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/patch.py +0 -0
  77. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/py.typed +0 -0
  78. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/__init__.py +0 -0
  79. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/ledger.py +0 -0
  80. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/policy.py +0 -0
  81. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/rendering.py +0 -0
  82. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/report.py +0 -0
  83. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/quality/save_pipeline.py +0 -0
  84. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/table_patch.py +0 -0
  85. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/templates.py +0 -0
  86. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/__init__.py +0 -0
  87. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  88. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  89. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/archive_cli.py +0 -0
  90. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/doc_diff.py +0 -0
  91. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/document_viewer.py +0 -0
  92. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/exporter.py +0 -0
  93. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/generic_inventory.py +0 -0
  94. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/id_integrity.py +0 -0
  95. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/idempotence.py +0 -0
  96. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/ir_equality.py +0 -0
  97. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/layout_preview.py +0 -0
  98. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/mail_merge.py +0 -0
  99. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/object_finder.py +0 -0
  100. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/package_reconcile.py +0 -0
  101. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/package_validator.py +0 -0
  102. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/page_guard.py +0 -0
  103. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/read_fidelity.py +0 -0
  104. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/recover.py +0 -0
  105. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/redline.py +0 -0
  106. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/repair.py +0 -0
  107. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/report_utils.py +0 -0
  108. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  109. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/table_cleanup.py +0 -0
  110. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/table_navigation.py +0 -0
  111. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/template_analyzer.py +0 -0
  112. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  113. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/toc_author.py +0 -0
  114. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  115. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/hwpx/tools/validator.py +0 -0
  116. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  117. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  118. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  119. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  120. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_add_control.py +0 -0
  121. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  122. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_authoring_defaults.py +0 -0
  123. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_authoring_quality_corpus.py +0 -0
  124. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_blind_eval_fixture.py +0 -0
  125. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_byte_patch_identity.py +0 -0
  126. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_cell_line_spacing.py +0 -0
  127. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_chart_authoring.py +0 -0
  128. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_comment_node_robustness.py +0 -0
  129. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_corpus_read_fidelity.py +0 -0
  130. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_coverage_promotion.py +0 -0
  131. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_coverage_targets.py +0 -0
  132. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_deviations_registry.py +0 -0
  133. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_doc_diff.py +0 -0
  134. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_context_manager.py +0 -0
  135. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_facade_surface.py +0 -0
  136. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_formatting.py +0 -0
  137. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  138. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_save_api.py +0 -0
  139. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_document_viewer.py +0 -0
  140. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_documentation_code_fences.py +0 -0
  141. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_equation_authoring.py +0 -0
  142. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_equation_converter.py +0 -0
  143. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_equation_render.py +0 -0
  144. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_error_messages.py +0 -0
  145. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_evalplan_core_primitives.py +0 -0
  146. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_existing_document_format_editing.py +0 -0
  147. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_form_field_authoring.py +0 -0
  148. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_form_fields.py +0 -0
  149. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_form_fill_core_primitives.py +0 -0
  150. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_form_fit.py +0 -0
  151. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_form_fit_integration.py +0 -0
  152. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  153. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_fuzz_loop.py +0 -0
  154. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_fuzz_regressions.py +0 -0
  155. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_gap_closure_tools.py +0 -0
  156. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_golden_api.py +0 -0
  157. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_hp_tab_support.py +0 -0
  158. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  159. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_id_generator_range.py +0 -0
  160. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_image_object_workflow.py +0 -0
  161. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_ingest.py +0 -0
  162. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_inline_models.py +0 -0
  163. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  164. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_integration_roundtrip.py +0 -0
  165. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_layout_cache_scope.py +0 -0
  166. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_layout_lint.py +0 -0
  167. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_layout_preview.py +0 -0
  168. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_library_boundary_closure.py +0 -0
  169. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_llms_txt.py +0 -0
  170. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_mail_merge_xlsx.py +0 -0
  171. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_markdown_export.py +0 -0
  172. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_memo_and_style_editing.py +0 -0
  173. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_moved_surface_hints.py +0 -0
  174. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_mutation_report.py +0 -0
  175. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_namespace_handling.py +0 -0
  176. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_new_features.py +0 -0
  177. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_opc_package.py +0 -0
  178. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_open_safety_corpus.py +0 -0
  179. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_oxml_modularization.py +0 -0
  180. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_oxml_parsing.py +0 -0
  181. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_packaging_license_metadata.py +0 -0
  182. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_packaging_py_typed.py +0 -0
  183. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_paragraph_keep_together.py +0 -0
  184. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_paragraph_section_management.py +0 -0
  185. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_product_boundary.py +0 -0
  186. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_public_artifact_hygiene.py +0 -0
  187. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_public_runtime_boundary.py +0 -0
  188. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_read_fidelity.py +0 -0
  189. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_recover_broken_zip.py +0 -0
  190. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_redline_authoring.py +0 -0
  191. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_redline_verify.py +0 -0
  192. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_release_workflow_safety.py +0 -0
  193. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_repair_repack.py +0 -0
  194. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_report_utils.py +0 -0
  195. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_repr_snapshots.py +0 -0
  196. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_roundtrip_fidelity.py +0 -0
  197. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_row_heights.py +0 -0
  198. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_save_pipeline.py +0 -0
  199. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_section_headers.py +0 -0
  200. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_set_paragraph_format_keep.py +0 -0
  201. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_shape_geometry_contract.py +0 -0
  202. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_skeleton_template_ids.py +0 -0
  203. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_split_cell.py +0 -0
  204. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_split_merged_cell.py +0 -0
  205. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_stable_surface.py +0 -0
  206. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_table_cleanup.py +0 -0
  207. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_table_navigation.py +0 -0
  208. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_table_patch.py +0 -0
  209. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_table_patch_dryrun.py +0 -0
  210. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_table_patch_m105.py +0 -0
  211. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_tables_default_border.py +0 -0
  212. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_template_analyzer_enrichment.py +0 -0
  213. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_text_extractor_annotations.py +0 -0
  214. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_toc_author.py +0 -0
  215. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_toc_fidelity.py +0 -0
  216. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_validation_severity.py +0 -0
  217. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_validator_comment_nodes.py +0 -0
  218. {python_hwpx-5.4.0 → python_hwpx-5.5.0}/tests/test_version_metadata.py +0 -0
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "5.4.0"
7
+ version = "5.5.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -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()
@@ -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
@@ -171,6 +171,7 @@ tests/test_moved_surface_hints.py
171
171
  tests/test_mutation_report.py
172
172
  tests/test_namespace_handling.py
173
173
  tests/test_new_features.py
174
+ tests/test_note_authoring_contract.py
174
175
  tests/test_opc_package.py
175
176
  tests/test_open_safety_corpus.py
176
177
  tests/test_oxml_modularization.py
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+
5
+ from hwpx import HwpxDocument
6
+ from hwpx.tools.markdown_export import export_markdown
7
+
8
+ HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
9
+
10
+
11
+ def _roundtrip(doc: HwpxDocument) -> HwpxDocument:
12
+ buffer = io.BytesIO()
13
+ doc.save_to_stream(buffer)
14
+ buffer.seek(0)
15
+ return HwpxDocument.open(buffer)
16
+
17
+
18
+ class TestGoldShapedEmission:
19
+ """Real-Hancom footnote contract (specs/058 gold reverse): without the
20
+ ctrl wrapper, autoNum, and note styles Hancom does not render notes."""
21
+
22
+ def test_note_is_ctrl_wrapped_inside_the_host_run(self) -> None:
23
+ doc = HwpxDocument.new()
24
+ para = doc.add_paragraph("본문 앵커 문장")
25
+ para.add_footnote("각주 본문")
26
+ host_run = para.element.findall(f"{HP}run")[-1]
27
+ ctrl = host_run.find(f"{HP}ctrl")
28
+ assert ctrl is not None
29
+ assert ctrl.find(f"{HP}footNote") is not None
30
+ assert host_run.find(f"{HP}footNote") is None
31
+
32
+ def test_note_numbering_is_continuous_per_type(self) -> None:
33
+ doc = HwpxDocument.new()
34
+ p1 = doc.add_paragraph("첫 문단")
35
+ p2 = doc.add_paragraph("둘째 문단")
36
+ f1 = p1.add_footnote("하나")
37
+ f2 = p2.add_footnote("둘")
38
+ e1 = p2.add_endnote("미주 하나")
39
+ assert f1.element.get("number") == "1"
40
+ assert f2.element.get("number") == "2"
41
+ assert e1.element.get("number") == "1"
42
+
43
+ def test_note_body_carries_style_autonum_and_suffix(self) -> None:
44
+ doc = HwpxDocument.new()
45
+ para = doc.add_paragraph("앵커")
46
+ note = para.add_footnote("각주 텍스트")
47
+ el = note.element
48
+ assert el.get("suffixChar") == "41"
49
+ sub = el.find(f"{HP}subList")
50
+ assert sub.get("vertAlign") == "TOP"
51
+ body_p = sub.find(f"{HP}p")
52
+ assert body_p.get("styleIDRef") == "15"
53
+ assert body_p.get("paraPrIDRef") == "10"
54
+ run = body_p.find(f"{HP}run")
55
+ assert run.get("charPrIDRef") == "3"
56
+ auto = run.find(f"{HP}ctrl/{HP}autoNum")
57
+ assert auto.get("numType") == "FOOTNOTE"
58
+ assert auto.get("num") == "1"
59
+ fmt = auto.find(f"{HP}autoNumFormat")
60
+ assert fmt.get("suffixChar") == ")"
61
+
62
+ def test_endnote_uses_endnote_style_and_numtype(self) -> None:
63
+ doc = HwpxDocument.new()
64
+ note = doc.add_paragraph("앵커").add_endnote("미주")
65
+ body_p = note.element.find(f"{HP}subList/{HP}p")
66
+ assert body_p.get("styleIDRef") == "16"
67
+ auto = body_p.find(f"{HP}run/{HP}ctrl/{HP}autoNum")
68
+ assert auto.get("numType") == "ENDNOTE"
69
+
70
+ def test_text_setter_preserves_the_gold_body(self) -> None:
71
+ doc = HwpxDocument.new()
72
+ note = doc.add_paragraph("앵커").add_footnote("원래 텍스트")
73
+ note.text = "고친 텍스트"
74
+ body_p = note.element.find(f"{HP}subList/{HP}p")
75
+ assert body_p.get("styleIDRef") == "15"
76
+ run = body_p.find(f"{HP}run")
77
+ assert run.find(f"{HP}ctrl/{HP}autoNum") is not None
78
+ assert run.find(f"{HP}t").text == "고친 텍스트"
79
+
80
+
81
+ class TestReaderCoversBothShapes:
82
+ def test_markdown_reads_gold_shaped_notes(self) -> None:
83
+ doc = HwpxDocument.new()
84
+ para = doc.add_paragraph("누리호 발사 성공")
85
+ para.add_footnote("한국형 발사체")
86
+ md = export_markdown(_roundtrip(doc))
87
+ assert "[^fn" in md
88
+ assert "한국형 발사체" in md
89
+
90
+ def test_markdown_still_reads_legacy_run_direct_notes(self) -> None:
91
+ # 5.4 이전 자사 방출(run 직속 footNote) 호환 유지
92
+ doc = HwpxDocument.new()
93
+ para = doc.add_paragraph("본문")
94
+ run = para.element.makeelement(f"{HP}run", {"charPrIDRef": "0"})
95
+ para.element.append(run)
96
+ note = run.makeelement(f"{HP}footNote", {"instId": "9999"})
97
+ run.append(note)
98
+ sub = note.makeelement(f"{HP}subList", {"id": "", "vertAlign": "TOP"})
99
+ note.append(sub)
100
+ body_p = sub.makeelement(f"{HP}p", {"id": "0", "paraPrIDRef": "0", "styleIDRef": "0"})
101
+ sub.append(body_p)
102
+ body_run = body_p.makeelement(f"{HP}run", {"charPrIDRef": "0"})
103
+ body_p.append(body_run)
104
+ t = body_run.makeelement(f"{HP}t", {})
105
+ t.text = "구식 각주 본문"
106
+ body_run.append(t)
107
+ md = export_markdown(_roundtrip(doc))
108
+ assert "구식 각주 본문" in md
109
+
110
+ def test_notes_survive_roundtrip_and_are_discoverable(self) -> None:
111
+ doc = HwpxDocument.new()
112
+ para = doc.add_paragraph("앵커 문장")
113
+ para.add_footnote("왕복 각주")
114
+ reopened = _roundtrip(doc)
115
+ notes = [
116
+ el
117
+ for section in reopened.sections
118
+ for el in section.element.iter(f"{HP}footNote")
119
+ ]
120
+ assert len(notes) == 1
121
+ assert notes[0].get("number") == "1"
File without changes
File without changes
File without changes
File without changes