python-hwpx 5.2.0__tar.gz → 5.3.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 (216) hide show
  1. {python_hwpx-5.2.0/src/python_hwpx.egg-info → python_hwpx-5.3.0}/PKG-INFO +1 -1
  2. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/pyproject.toml +1 -1
  3. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/shapes.py +75 -0
  4. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/document.py +42 -0
  5. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/paragraph.py +72 -0
  6. {python_hwpx-5.2.0 → python_hwpx-5.3.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  7. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
  8. python_hwpx-5.3.0/tests/test_chart_authoring.py +172 -0
  9. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/LICENSE +0 -0
  10. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/NOTICE +0 -0
  11. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/README.md +0 -0
  12. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/setup.cfg +0 -0
  13. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/__init__.py +0 -0
  14. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/__init__.py +0 -0
  15. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/_units.py +0 -0
  16. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/fields.py +0 -0
  17. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/layout.py +0 -0
  18. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/media.py +0 -0
  19. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/memos.py +0 -0
  20. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/persistence.py +0 -0
  21. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/tracked.py +0 -0
  22. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/body_patch.py +0 -0
  23. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  24. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/__init__.py +0 -0
  25. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/authoring.py +0 -0
  26. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/eqedit.py +0 -0
  27. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/mathml.py +0 -0
  28. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/render.py +0 -0
  29. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/tokens.py +0 -0
  30. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/errors.py +0 -0
  31. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/experimental.py +0 -0
  32. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/__init__.py +0 -0
  33. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/apply.py +0 -0
  34. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/engine.py +0 -0
  35. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/measure.py +0 -0
  36. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/policy.py +0 -0
  37. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/report.py +0 -0
  38. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/__init__.py +0 -0
  39. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/base.py +0 -0
  40. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  41. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/__init__.py +0 -0
  42. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/lint.py +0 -0
  43. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/report.py +0 -0
  44. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/mutation_report.py +0 -0
  45. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/package.py +0 -0
  46. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/relationships.py +0 -0
  47. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/security.py +0 -0
  48. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/xml_utils.py +0 -0
  49. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/__init__.py +0 -0
  50. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/_document_impl.py +0 -0
  51. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  52. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/body.py +0 -0
  53. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  54. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/color.py +0 -0
  55. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/common.py +0 -0
  56. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/document.py +0 -0
  57. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/document_parts.py +0 -0
  58. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/header.py +0 -0
  59. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/header_part.py +0 -0
  60. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/memo.py +0 -0
  61. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/namespaces.py +0 -0
  62. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/numbering.py +0 -0
  63. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/objects.py +0 -0
  64. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/parser.py +0 -0
  65. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/run.py +0 -0
  66. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/schema.py +0 -0
  67. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section.py +0 -0
  68. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section_format.py +0 -0
  69. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section_story.py +0 -0
  70. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/simple_parts.py +0 -0
  71. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/table.py +0 -0
  72. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/utils.py +0 -0
  73. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/package.py +0 -0
  74. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/patch.py +0 -0
  75. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/py.typed +0 -0
  76. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/__init__.py +0 -0
  77. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/ledger.py +0 -0
  78. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/policy.py +0 -0
  79. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/rendering.py +0 -0
  80. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/report.py +0 -0
  81. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/save_pipeline.py +0 -0
  82. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/table_patch.py +0 -0
  83. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/templates.py +0 -0
  84. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/__init__.py +0 -0
  85. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  86. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  87. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/archive_cli.py +0 -0
  88. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/doc_diff.py +0 -0
  89. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/document_viewer.py +0 -0
  90. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/exporter.py +0 -0
  91. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/generic_inventory.py +0 -0
  92. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/id_integrity.py +0 -0
  93. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/idempotence.py +0 -0
  94. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/ir_equality.py +0 -0
  95. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/layout_preview.py +0 -0
  96. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/mail_merge.py +0 -0
  97. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/markdown_export.py +0 -0
  98. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/object_finder.py +0 -0
  99. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/package_reconcile.py +0 -0
  100. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/package_validator.py +0 -0
  101. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/page_guard.py +0 -0
  102. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/read_fidelity.py +0 -0
  103. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/recover.py +0 -0
  104. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/redline.py +0 -0
  105. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/repair.py +0 -0
  106. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/report_utils.py +0 -0
  107. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  108. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/table_cleanup.py +0 -0
  109. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/table_navigation.py +0 -0
  110. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/template_analyzer.py +0 -0
  111. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  112. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/text_extractor.py +0 -0
  113. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/toc_author.py +0 -0
  114. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  115. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/validator.py +0 -0
  116. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  117. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  118. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  119. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  120. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_add_control.py +0 -0
  121. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  122. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_authoring_quality_corpus.py +0 -0
  123. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_blind_eval_fixture.py +0 -0
  124. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_byte_patch_identity.py +0 -0
  125. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_cell_line_spacing.py +0 -0
  126. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_comment_node_robustness.py +0 -0
  127. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_corpus_read_fidelity.py +0 -0
  128. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_coverage_promotion.py +0 -0
  129. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_coverage_targets.py +0 -0
  130. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_deviations_registry.py +0 -0
  131. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_doc_diff.py +0 -0
  132. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_context_manager.py +0 -0
  133. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_facade_surface.py +0 -0
  134. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_formatting.py +0 -0
  135. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  136. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_save_api.py +0 -0
  137. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_viewer.py +0 -0
  138. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_documentation_code_fences.py +0 -0
  139. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_authoring.py +0 -0
  140. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_converter.py +0 -0
  141. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_render.py +0 -0
  142. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_error_messages.py +0 -0
  143. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_evalplan_core_primitives.py +0 -0
  144. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_existing_document_format_editing.py +0 -0
  145. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_field_authoring.py +0 -0
  146. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fields.py +0 -0
  147. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fill_core_primitives.py +0 -0
  148. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fit.py +0 -0
  149. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fit_integration.py +0 -0
  150. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  151. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_loop.py +0 -0
  152. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_regressions.py +0 -0
  153. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_gap_closure_tools.py +0 -0
  154. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_golden_api.py +0 -0
  155. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_hp_tab_support.py +0 -0
  156. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  157. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_id_generator_range.py +0 -0
  158. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_image_object_workflow.py +0 -0
  159. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_ingest.py +0 -0
  160. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_inline_models.py +0 -0
  161. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  162. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_integration_roundtrip.py +0 -0
  163. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_cache_scope.py +0 -0
  164. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_lint.py +0 -0
  165. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_preview.py +0 -0
  166. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_library_boundary_closure.py +0 -0
  167. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_llms_txt.py +0 -0
  168. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_mail_merge_xlsx.py +0 -0
  169. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_markdown_export.py +0 -0
  170. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_memo_and_style_editing.py +0 -0
  171. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_moved_surface_hints.py +0 -0
  172. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_mutation_report.py +0 -0
  173. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_namespace_handling.py +0 -0
  174. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_new_features.py +0 -0
  175. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_opc_package.py +0 -0
  176. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_open_safety_corpus.py +0 -0
  177. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_oxml_modularization.py +0 -0
  178. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_oxml_parsing.py +0 -0
  179. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_packaging_license_metadata.py +0 -0
  180. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_packaging_py_typed.py +0 -0
  181. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_paragraph_keep_together.py +0 -0
  182. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_paragraph_section_management.py +0 -0
  183. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_product_boundary.py +0 -0
  184. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_public_artifact_hygiene.py +0 -0
  185. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_public_runtime_boundary.py +0 -0
  186. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_read_fidelity.py +0 -0
  187. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_recover_broken_zip.py +0 -0
  188. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_redline_authoring.py +0 -0
  189. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_redline_verify.py +0 -0
  190. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_release_workflow_safety.py +0 -0
  191. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_repair_repack.py +0 -0
  192. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_report_utils.py +0 -0
  193. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_repr_snapshots.py +0 -0
  194. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_roundtrip_fidelity.py +0 -0
  195. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_row_heights.py +0 -0
  196. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_save_pipeline.py +0 -0
  197. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_section_headers.py +0 -0
  198. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_set_paragraph_format_keep.py +0 -0
  199. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_shape_geometry_contract.py +0 -0
  200. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_skeleton_template_ids.py +0 -0
  201. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_split_cell.py +0 -0
  202. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_split_merged_cell.py +0 -0
  203. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_stable_surface.py +0 -0
  204. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_cleanup.py +0 -0
  205. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_navigation.py +0 -0
  206. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch.py +0 -0
  207. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch_dryrun.py +0 -0
  208. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch_m105.py +0 -0
  209. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_tables_default_border.py +0 -0
  210. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_template_analyzer_enrichment.py +0 -0
  211. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_text_extractor_annotations.py +0 -0
  212. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_toc_author.py +0 -0
  213. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_toc_fidelity.py +0 -0
  214. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_validation_severity.py +0 -0
  215. {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_validator_comment_nodes.py +0 -0
  216. {python_hwpx-5.2.0 → python_hwpx-5.3.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.2.0
3
+ Version: 5.3.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.2.0"
7
+ version = "5.3.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -282,3 +282,78 @@ def add_equation(
282
282
  raise RuntimeError(
283
283
  "created equation was not recognized by the standard section scan"
284
284
  )
285
+
286
+
287
+ def add_chart(
288
+ doc: "HwpxDocument",
289
+ chart_xml: bytes | str,
290
+ *,
291
+ paragraph: HwpxOxmlParagraph | None = None,
292
+ section: HwpxOxmlSection | None = None,
293
+ section_index: int | None = None,
294
+ size: tuple[int, int] | None = None,
295
+ treat_as_char: bool = False,
296
+ char_pr_id_ref: str | int | None = None,
297
+ ) -> HwpxOxmlInlineObject:
298
+ """Add a chartML part and its ``<hp:chart>`` anchor, then prove recognition.
299
+
300
+ The part is stored under ``Chart/chartN.xml`` and addressed directly by
301
+ the anchor's ``chartIDRef`` — real Hancom registers chart parts in no
302
+ manifest and draws the chart from the ECMA-376 chartML alone
303
+ (specs/055-chart-authoring/evidence/p0/chart-contract.md). The chartML is
304
+ validated to parse and to carry the ``c:chartSpace`` root before any part
305
+ is written; after insertion the anchor is re-read through the standard
306
+ section scan — creation fails loudly if it did not land (no
307
+ special-casing by design).
308
+ """
309
+ from lxml import etree as _etree # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
310
+
311
+ from ..oxml.namespaces import HP
312
+
313
+ _CHART_SPACE = "{http://schemas.openxmlformats.org/drawingml/2006/chart}chartSpace"
314
+
315
+ data = chart_xml.encode("utf-8") if isinstance(chart_xml, str) else bytes(chart_xml)
316
+ if not data.strip():
317
+ raise ValueError("chart_xml must be non-empty chartML")
318
+ try:
319
+ root = _etree.fromstring(data)
320
+ except _etree.XMLSyntaxError as exc:
321
+ raise ValueError(f"chart_xml is not well-formed XML: {exc}") from exc
322
+ if root.tag != _CHART_SPACE:
323
+ raise ValueError(
324
+ "chart_xml root must be the ECMA-376 c:chartSpace element, "
325
+ f"got {root.tag!r}"
326
+ )
327
+
328
+ existing = {name for name in doc._package.part_names() if name.startswith("Chart/")}
329
+ n = 1
330
+ while f"Chart/chart{n}.xml" in existing:
331
+ n += 1
332
+ part_path = f"Chart/chart{n}.xml"
333
+
334
+ if paragraph is None:
335
+ paragraph = doc.add_paragraph(
336
+ "", section=section, section_index=section_index,
337
+ include_run=False,
338
+ )
339
+ doc._package.write(part_path, data)
340
+ inline_object = paragraph.add_chart(
341
+ part_path,
342
+ size=size,
343
+ treat_as_char=treat_as_char,
344
+ char_pr_id_ref=char_pr_id_ref,
345
+ )
346
+
347
+ created_id = inline_object.element.get("id", "")
348
+ for owning_section in doc.sections:
349
+ for candidate in owning_section.element.iter(f"{HP}chart"):
350
+ if candidate.get("id") != created_id:
351
+ continue
352
+ if candidate.get("chartIDRef") != part_path:
353
+ raise RuntimeError(
354
+ "created chart anchor does not reference its part"
355
+ )
356
+ return inline_object
357
+ raise RuntimeError(
358
+ "created chart was not recognized by the standard section scan"
359
+ )
@@ -1431,6 +1431,48 @@ class HwpxDocument:
1431
1431
  )
1432
1432
 
1433
1433
 
1434
+ def add_chart(
1435
+ self,
1436
+ chart_xml: bytes | str,
1437
+ *,
1438
+ paragraph: HwpxOxmlParagraph | None = None,
1439
+ section: HwpxOxmlSection | None = None,
1440
+ section_index: int | None = None,
1441
+ size: tuple[int, int] | None = None,
1442
+ treat_as_char: bool = False,
1443
+ char_pr_id_ref: str | int | None = None,
1444
+ ) -> HwpxOxmlInlineObject:
1445
+ """Insert a native chart from ECMA-376 chartML. **Experimental contract.**
1446
+
1447
+ Stores *chart_xml* as a ``Chart/chartN.xml`` package part and emits the
1448
+ real-Hancom ``<hp:chart>`` anchor referencing it via ``chartIDRef``
1449
+ (contract: ``specs/055-chart-authoring/evidence/p0/chart-contract.md``).
1450
+ Hancom draws the chart from the chartML alone — no OLE fallback or
1451
+ pre-rendered image is written. The chartML must parse and carry the
1452
+ ``c:chartSpace`` root (typed rejection otherwise), and the created
1453
+ anchor is re-read through the standard section scan — creation fails
1454
+ loudly if the standard consumer would not see it.
1455
+
1456
+ Args:
1457
+ chart_xml: ECMA-376 chartML document (``c:chartSpace``).
1458
+ paragraph: Target paragraph (e.g. inside a table cell). When
1459
+ omitted a new paragraph is appended to *section*.
1460
+ size: Optional ``(width, height)`` HWPUNIT pair for the anchor.
1461
+ treat_as_char: ``True`` places the chart inline in the text flow;
1462
+ default mirrors the render-verified gold float placement.
1463
+ """
1464
+
1465
+ return _shapes.add_chart(
1466
+ self,
1467
+ chart_xml,
1468
+ paragraph=paragraph,
1469
+ section=section,
1470
+ section_index=section_index,
1471
+ size=size,
1472
+ treat_as_char=treat_as_char,
1473
+ char_pr_id_ref=char_pr_id_ref,
1474
+ )
1475
+
1434
1476
  def add_equation(
1435
1477
  self,
1436
1478
  script: str,
@@ -834,6 +834,78 @@ class HwpxOxmlParagraph:
834
834
  self.section.mark_dirty()
835
835
  return HwpxOxmlInlineObject(ctrl1, self)
836
836
 
837
+ def add_chart(
838
+ self,
839
+ chart_id_ref: str,
840
+ *,
841
+ size: tuple[int, int] | None = None,
842
+ treat_as_char: bool = False,
843
+ char_pr_id_ref: str | int | None = None,
844
+ run_attributes: dict[str, str] | None = None,
845
+ ) -> HwpxOxmlInlineObject:
846
+ """Insert a native ``<hp:chart>`` anchor referencing a chartML part.
847
+
848
+ Emits the real-Hancom chart anchor (contract reverse-engineered in
849
+ specs/055-chart-authoring/evidence/p0/chart-contract.md): the chart
850
+ content lives in an ECMA-376 chartML part addressed directly by
851
+ ``chartIDRef`` (real Hancom registers the part in no manifest), and
852
+ Hancom lays the chart out from the chartML alone — no OLE fallback or
853
+ pre-rendered image is required.
854
+
855
+ Args:
856
+ chart_id_ref: Package path of the chartML part
857
+ (e.g. ``Chart/chart1.xml``).
858
+ size: Optional ``(width, height)`` HWPUNIT pair for ``<hp:sz>``;
859
+ defaults to the gold document's 32250x18750.
860
+ treat_as_char: ``True`` places the chart inline in the text flow;
861
+ the default mirrors the gold float placement.
862
+ """
863
+ reference = chart_id_ref.strip()
864
+ if not reference:
865
+ raise ValueError("chart_id_ref must be a non-empty package path")
866
+ width, height = size if size is not None else (32250, 18750)
867
+ run = self._create_run_for_object(
868
+ run_attributes, char_pr_id_ref=char_pr_id_ref
869
+ )
870
+ chart = _append_child(run, f"{_HP}chart", {
871
+ "id": _object_id(),
872
+ "zOrder": "0",
873
+ "numberingType": "PICTURE",
874
+ "textWrap": "TOP_AND_BOTTOM" if treat_as_char else "SQUARE",
875
+ "textFlow": "BOTH_SIDES",
876
+ "lock": "0",
877
+ "dropcapstyle": "None",
878
+ "chartIDRef": reference,
879
+ })
880
+ _append_child(chart, f"{_HP}sz", {
881
+ "width": str(width),
882
+ "widthRelTo": "ABSOLUTE",
883
+ "height": str(height),
884
+ "heightRelTo": "ABSOLUTE",
885
+ "protect": "0",
886
+ })
887
+ _append_child(chart, f"{_HP}pos", {
888
+ "treatAsChar": "1" if treat_as_char else "0",
889
+ "affectLSpacing": "0",
890
+ "flowWithText": "1",
891
+ "allowOverlap": "0",
892
+ "holdAnchorAndSO": "0",
893
+ "vertRelTo": "PARA",
894
+ "horzRelTo": "PARA" if treat_as_char else "COLUMN",
895
+ "vertAlign": "TOP",
896
+ "horzAlign": "LEFT",
897
+ "vertOffset": "0",
898
+ "horzOffset": "0",
899
+ })
900
+ _append_child(chart, f"{_HP}outMargin", {
901
+ "left": "0",
902
+ "right": "0",
903
+ "top": "0",
904
+ "bottom": "0",
905
+ })
906
+ self.section.mark_dirty()
907
+ return HwpxOxmlInlineObject(chart, self)
908
+
837
909
  def add_equation(
838
910
  self,
839
911
  script: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 5.2.0
3
+ Version: 5.3.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -120,6 +120,7 @@ tests/test_authoring_quality_corpus.py
120
120
  tests/test_blind_eval_fixture.py
121
121
  tests/test_byte_patch_identity.py
122
122
  tests/test_cell_line_spacing.py
123
+ tests/test_chart_authoring.py
123
124
  tests/test_comment_node_robustness.py
124
125
  tests/test_corpus_read_fidelity.py
125
126
  tests/test_coverage_promotion.py
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import zipfile
5
+ from pathlib import Path
6
+
7
+ from hwpx import HwpxDocument
8
+ from hwpx.tools.package_validator import validate_editor_open_safety
9
+
10
+ import pytest
11
+
12
+ HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
13
+ FIXTURES = Path(__file__).parent / "fixtures"
14
+ GOLD_HANCOM = FIXTURES / "hwpxlib_corpus" / "error__20230426__HwpxTest1.hwpx"
15
+
16
+ CHART_HEAD = (
17
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'
18
+ '<c:chartSpace xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
19
+ 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
20
+ 'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">'
21
+ "<c:chart><c:plotArea><c:layout/>"
22
+ )
23
+ PIE_CHARTML = (
24
+ CHART_HEAD
25
+ + '<c:pieChart><c:varyColors val="1"/><c:ser><c:idx val="0"/><c:order val="0"/>'
26
+ '<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$3</c:f><c:strCache><c:ptCount val="2"/>'
27
+ '<c:pt idx="0"><c:v>A</c:v></c:pt><c:pt idx="1"><c:v>B</c:v></c:pt></c:strCache></c:strRef></c:cat>'
28
+ '<c:val><c:numRef><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:formatCode>General</c:formatCode>'
29
+ '<c:ptCount val="2"/><c:pt idx="0"><c:v>60</c:v></c:pt><c:pt idx="1"><c:v>40</c:v></c:pt>'
30
+ "</c:numCache></c:numRef></c:val></c:ser></c:pieChart>"
31
+ "</c:plotArea></c:chart></c:chartSpace>"
32
+ )
33
+
34
+
35
+ def _roundtrip(doc: HwpxDocument) -> tuple[HwpxDocument, bytes]:
36
+ buffer = io.BytesIO()
37
+ doc.save_to_stream(buffer)
38
+ payload = buffer.getvalue()
39
+ return HwpxDocument.open(io.BytesIO(payload)), payload
40
+
41
+
42
+ def _anchors(doc: HwpxDocument):
43
+ found = []
44
+ for section in doc.sections:
45
+ found.extend(section.element.iter(f"{HP}chart"))
46
+ return found
47
+
48
+
49
+ class TestGoldContractShape:
50
+ """The emitted anchor must match the real-Hancom chart contract
51
+ (specs/055-chart-authoring/evidence/p0/chart-contract.md)."""
52
+
53
+ def test_anchor_attributes_and_part(self) -> None:
54
+ doc = HwpxDocument.new()
55
+ doc.add_chart(PIE_CHARTML)
56
+ (anchor,) = _anchors(doc)
57
+ assert anchor.get("chartIDRef") == "Chart/chart1.xml"
58
+ assert anchor.get("numberingType") == "PICTURE"
59
+ assert anchor.get("textWrap") == "SQUARE"
60
+ assert anchor.get("id")
61
+ local_names = [child.tag.rsplit("}", 1)[-1] for child in anchor]
62
+ assert local_names == ["sz", "pos", "outMargin"]
63
+ assert anchor.find(f"{HP}pos").get("treatAsChar") == "0"
64
+
65
+ def test_part_written_verbatim_and_unregistered(self) -> None:
66
+ doc = HwpxDocument.new()
67
+ doc.add_chart(PIE_CHARTML)
68
+ _reopened, payload = _roundtrip(doc)
69
+ with zipfile.ZipFile(io.BytesIO(payload)) as package:
70
+ assert package.read("Chart/chart1.xml") == PIE_CHARTML.encode("utf-8")
71
+ hpf = package.read("Contents/content.hpf").decode("utf-8")
72
+ # Real Hancom registers chart parts in no manifest (gold contract).
73
+ assert "chart1" not in hpf
74
+
75
+ def test_part_paths_allocate_sequentially(self) -> None:
76
+ doc = HwpxDocument.new()
77
+ doc.add_chart(PIE_CHARTML)
78
+ doc.add_chart(PIE_CHARTML)
79
+ refs = [anchor.get("chartIDRef") for anchor in _anchors(doc)]
80
+ assert refs == ["Chart/chart1.xml", "Chart/chart2.xml"]
81
+
82
+ def test_inline_placement(self) -> None:
83
+ doc = HwpxDocument.new()
84
+ doc.add_chart(PIE_CHARTML, treat_as_char=True, size=(20000, 12000))
85
+ (anchor,) = _anchors(doc)
86
+ assert anchor.get("textWrap") == "TOP_AND_BOTTOM"
87
+ pos = anchor.find(f"{HP}pos")
88
+ assert pos.get("treatAsChar") == "1"
89
+ sz = anchor.find(f"{HP}sz")
90
+ assert (sz.get("width"), sz.get("height")) == ("20000", "12000")
91
+
92
+
93
+ class TestSelfRoundtrip:
94
+ def test_reopen_keeps_anchor_and_part_bytes(self) -> None:
95
+ doc = HwpxDocument.new()
96
+ doc.add_chart(PIE_CHARTML)
97
+ reopened, _payload = _roundtrip(doc)
98
+ (anchor,) = _anchors(reopened)
99
+ assert anchor.get("chartIDRef") == "Chart/chart1.xml"
100
+ # patch-grade resave must keep the chart part byte-identical
101
+ _again, payload2 = _roundtrip(reopened)
102
+ with zipfile.ZipFile(io.BytesIO(payload2)) as package:
103
+ assert package.read("Chart/chart1.xml") == PIE_CHARTML.encode("utf-8")
104
+
105
+ def test_chart_in_table_cell(self) -> None:
106
+ doc = HwpxDocument.new()
107
+ table = doc.add_table(2, 2)
108
+ cell_paragraph = table.cell(0, 0).paragraphs[0]
109
+ doc.add_chart(PIE_CHARTML, paragraph=cell_paragraph)
110
+ reopened, _payload = _roundtrip(doc)
111
+ (anchor,) = _anchors(reopened)
112
+ ancestors = []
113
+ parent = anchor.getparent()
114
+ while parent is not None:
115
+ ancestors.append(parent.tag.rsplit("}", 1)[-1])
116
+ parent = parent.getparent()
117
+ assert "tc" in ancestors
118
+
119
+ def test_byte_preservation_of_unrelated_parts(self) -> None:
120
+ doc = HwpxDocument.open(GOLD_HANCOM)
121
+ with zipfile.ZipFile(GOLD_HANCOM) as package:
122
+ before = {name: package.read(name) for name in package.namelist()}
123
+ doc.add_chart(PIE_CHARTML)
124
+ buffer = io.BytesIO()
125
+ doc.save_to_stream(buffer)
126
+ with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as package:
127
+ after = {name: package.read(name) for name in package.namelist()}
128
+ changed = {
129
+ name for name in before if name in after and before[name] != after[name]
130
+ }
131
+ assert changed <= {"Contents/section0.xml"}
132
+ # The gold file already carries Chart/chart1.xml — ours must not clobber it.
133
+ assert after["Chart/chart1.xml"] == before["Chart/chart1.xml"]
134
+ assert _anchors(HwpxDocument.open(io.BytesIO(buffer.getvalue())))[-1].get(
135
+ "chartIDRef"
136
+ ) == "Chart/chart2.xml"
137
+
138
+ def test_open_safety(self) -> None:
139
+ doc = HwpxDocument.new()
140
+ doc.add_chart(PIE_CHARTML)
141
+ buffer = io.BytesIO()
142
+ doc.save_to_stream(buffer)
143
+ report = validate_editor_open_safety(buffer.getvalue())
144
+ assert report.ok, report
145
+
146
+ def test_gold_fixture_anchor_still_reads(self) -> None:
147
+ doc = HwpxDocument.open(GOLD_HANCOM)
148
+ anchors = _anchors(doc)
149
+ assert anchors
150
+ assert anchors[0].get("chartIDRef") == "Chart/chart1.xml"
151
+
152
+
153
+ class TestValidation:
154
+ def test_empty_chartml_rejected(self) -> None:
155
+ doc = HwpxDocument.new()
156
+ with pytest.raises(ValueError):
157
+ doc.add_chart(" ")
158
+
159
+ def test_malformed_xml_rejected_before_any_write(self) -> None:
160
+ doc = HwpxDocument.new()
161
+ with pytest.raises(ValueError):
162
+ doc.add_chart("<c:chartSpace")
163
+ assert _anchors(doc) == []
164
+ buffer = io.BytesIO()
165
+ doc.save_to_stream(buffer)
166
+ with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as package:
167
+ assert not [n for n in package.namelist() if n.startswith("Chart/")]
168
+
169
+ def test_wrong_root_rejected(self) -> None:
170
+ doc = HwpxDocument.new()
171
+ with pytest.raises(ValueError, match="chartSpace"):
172
+ doc.add_chart("<not-a-chart/>")
File without changes
File without changes
File without changes
File without changes