python-hwpx 6.4.0__tar.gz → 6.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 (328) hide show
  1. {python_hwpx-6.4.0/src/python_hwpx.egg-info → python_hwpx-6.5.0}/PKG-INFO +1 -1
  2. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/pyproject.toml +1 -1
  3. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/authoring.py +5 -0
  4. python_hwpx-6.5.0/src/hwpx/equation/mathml.py +135 -0
  5. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/render.py +2 -2
  6. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/tokens.py +11 -0
  7. python_hwpx-6.5.0/src/hwpx/oxml/_paragraph_text_edit.py +42 -0
  8. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/paragraph.py +37 -0
  9. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/table.py +24 -0
  10. {python_hwpx-6.4.0 → python_hwpx-6.5.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  11. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/SOURCES.txt +2 -0
  12. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_authoring.py +2 -0
  13. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_converter.py +25 -0
  14. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_render.py +47 -0
  15. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_golden_api.py +25 -6
  16. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_product_boundary.py +2 -1
  17. python_hwpx-6.5.0/tests/test_table_page_flow.py +18 -0
  18. python_hwpx-6.4.0/src/hwpx/equation/mathml.py +0 -69
  19. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/LICENSE +0 -0
  20. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/NOTICE +0 -0
  21. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/README.md +0 -0
  22. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/setup.cfg +0 -0
  23. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/__init__.py +0 -0
  24. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/__init__.py +0 -0
  25. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_legacy.py +0 -0
  26. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_resolve.py +0 -0
  27. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_units.py +0 -0
  28. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/fields.py +0 -0
  29. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/headings.py +0 -0
  30. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/highlight.py +0 -0
  31. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/layout.py +0 -0
  32. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/media.py +0 -0
  33. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/memos.py +0 -0
  34. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/__init__.py +0 -0
  35. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/_base.py +0 -0
  36. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/fields.py +0 -0
  37. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/media.py +0 -0
  38. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/notes.py +0 -0
  39. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/page.py +0 -0
  40. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/parts.py +0 -0
  41. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/refs.py +0 -0
  42. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/shapes.py +0 -0
  43. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/styles.py +0 -0
  44. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/tables.py +0 -0
  45. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/text.py +0 -0
  46. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/tracking.py +0 -0
  47. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/persistence.py +0 -0
  48. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/shapes.py +0 -0
  49. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/tracked.py +0 -0
  50. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/body_patch.py +0 -0
  51. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/capabilities.py +0 -0
  52. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  53. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
  54. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
  55. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
  56. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/support-matrix.md +0 -0
  57. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/document.py +0 -0
  58. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/__init__.py +0 -0
  59. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/eqedit.py +0 -0
  60. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/errors.py +0 -0
  61. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/experimental.py +0 -0
  62. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/__init__.py +0 -0
  63. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/apply.py +0 -0
  64. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/engine.py +0 -0
  65. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/measure.py +0 -0
  66. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/policy.py +0 -0
  67. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/report.py +0 -0
  68. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/__init__.py +0 -0
  69. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/base.py +0 -0
  70. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  71. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/__init__.py +0 -0
  72. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/lint.py +0 -0
  73. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/report.py +0 -0
  74. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/model.py +0 -0
  75. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/mutation_report.py +0 -0
  76. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/__init__.py +0 -0
  77. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/binary_item.py +0 -0
  78. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/checkbox.py +0 -0
  79. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/form_field.py +0 -0
  80. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/highlight.py +0 -0
  81. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/results.py +0 -0
  82. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/tracked.py +0 -0
  83. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/package.py +0 -0
  84. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/relationships.py +0 -0
  85. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/security.py +0 -0
  86. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/xml_utils.py +0 -0
  87. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/__init__.py +0 -0
  88. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/_document_impl.py +0 -0
  89. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/_document_primitives.py +0 -0
  90. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/body.py +0 -0
  91. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  92. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/color.py +0 -0
  93. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/common.py +0 -0
  94. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document.py +0 -0
  95. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document_metadata.py +0 -0
  96. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document_parts.py +0 -0
  97. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/drop_cap.py +0 -0
  98. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/dutmal_compose.py +0 -0
  99. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/field_marks.py +0 -0
  100. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header.py +0 -0
  101. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header_compat.py +0 -0
  102. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header_part.py +0 -0
  103. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/history_part.py +0 -0
  104. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/master_page.py +0 -0
  105. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/master_page_authoring.py +0 -0
  106. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/memo.py +0 -0
  107. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/namespaces.py +0 -0
  108. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/note_authoring.py +0 -0
  109. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/numbering.py +0 -0
  110. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/numbering_kinds.py +0 -0
  111. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/objects.py +0 -0
  112. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/parser.py +0 -0
  113. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/run.py +0 -0
  114. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/schema.py +0 -0
  115. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section.py +0 -0
  116. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_format.py +0 -0
  117. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_layout.py +0 -0
  118. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_story.py +0 -0
  119. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/settings.py +0 -0
  120. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/simple_parts.py +0 -0
  121. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/utils.py +0 -0
  122. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/version_part.py +0 -0
  123. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/package.py +0 -0
  124. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/patch.py +0 -0
  125. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/__init__.py +0 -0
  126. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_execute.py +0 -0
  127. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_model.py +0 -0
  128. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_schema.py +0 -0
  129. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_validate.py +0 -0
  130. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/py.typed +0 -0
  131. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/__init__.py +0 -0
  132. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/ledger.py +0 -0
  133. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/policy.py +0 -0
  134. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/rendering.py +0 -0
  135. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/report.py +0 -0
  136. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/save_pipeline.py +0 -0
  137. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/table_patch.py +0 -0
  138. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/templates.py +0 -0
  139. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/__init__.py +0 -0
  140. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  141. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  142. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/archive_cli.py +0 -0
  143. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/doc_diff.py +0 -0
  144. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/document_merge.py +0 -0
  145. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/document_viewer.py +0 -0
  146. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/exporter.py +0 -0
  147. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/generic_inventory.py +0 -0
  148. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/id_integrity.py +0 -0
  149. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/idempotence.py +0 -0
  150. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/ir_equality.py +0 -0
  151. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/layout_preview.py +0 -0
  152. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/mail_merge.py +0 -0
  153. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/markdown_export.py +0 -0
  154. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/object_finder.py +0 -0
  155. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/package_reconcile.py +0 -0
  156. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/package_validator.py +0 -0
  157. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/page_guard.py +0 -0
  158. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/read_fidelity.py +0 -0
  159. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/recover.py +0 -0
  160. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/redline.py +0 -0
  161. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/repair.py +0 -0
  162. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/report_utils.py +0 -0
  163. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  164. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/table_cleanup.py +0 -0
  165. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/table_navigation.py +0 -0
  166. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/template_analyzer.py +0 -0
  167. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  168. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/text_extractor.py +0 -0
  169. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/toc_author.py +0 -0
  170. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  171. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/validator.py +0 -0
  172. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  173. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  174. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  175. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  176. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_add_control.py +0 -0
  177. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_add_heading.py +0 -0
  178. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  179. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_arc_authoring.py +0 -0
  180. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_authoring_defaults.py +0 -0
  181. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_authoring_quality_corpus.py +0 -0
  182. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_blind_eval_fixture.py +0 -0
  183. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_body_patch.py +0 -0
  184. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_border_fill_image_gradient.py +0 -0
  185. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_build_element_census.py +0 -0
  186. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_byte_patch_identity.py +0 -0
  187. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_capabilities_surface.py +0 -0
  188. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_cell_line_spacing.py +0 -0
  189. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_chart_authoring.py +0 -0
  190. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_check_box_authoring.py +0 -0
  191. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_column_break.py +0 -0
  192. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_comment_node_robustness.py +0 -0
  193. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_container_authoring.py +0 -0
  194. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_contract_docs_sync.py +0 -0
  195. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_corpus_read_fidelity.py +0 -0
  196. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_ledger.py +0 -0
  197. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_promotion.py +0 -0
  198. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_targets.py +0 -0
  199. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_deviations_registry.py +0 -0
  200. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_doc_diff.py +0 -0
  201. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_context_manager.py +0 -0
  202. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_facade_surface.py +0 -0
  203. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_formatting.py +0 -0
  204. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_merge.py +0 -0
  205. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_metadata.py +0 -0
  206. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  207. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_save_api.py +0 -0
  208. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_viewer.py +0 -0
  209. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_documentation_code_fences.py +0 -0
  210. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_drop_cap_authoring.py +0 -0
  211. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_dutmal_compose_authoring.py +0 -0
  212. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_edit_fidelity_corpus.py +0 -0
  213. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_edit_plan.py +0 -0
  214. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_editor_surface_inventory.py +0 -0
  215. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_engine_surface_6_0.py +0 -0
  216. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_error_contract.py +0 -0
  217. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_error_messages.py +0 -0
  218. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_evalplan_core_primitives.py +0 -0
  219. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_existing_document_format_editing.py +0 -0
  220. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_field_marks.py +0 -0
  221. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_field_authoring.py +0 -0
  222. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fields.py +0 -0
  223. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fill_core_primitives.py +0 -0
  224. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fit.py +0 -0
  225. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fit_integration.py +0 -0
  226. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  227. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_loop.py +0 -0
  228. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_regressions.py +0 -0
  229. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_gap_closure_tools.py +0 -0
  230. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_header_compat_authoring.py +0 -0
  231. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_highlight_authoring.py +0 -0
  232. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_hp_tab_support.py +0 -0
  233. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  234. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_id_generator_range.py +0 -0
  235. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_id_integrity.py +0 -0
  236. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_image_object_workflow.py +0 -0
  237. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_index_mark.py +0 -0
  238. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ingest.py +0 -0
  239. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_inline_models.py +0 -0
  240. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_instid_attribute_case.py +0 -0
  241. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  242. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_integration_roundtrip.py +0 -0
  243. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_cache_scope.py +0 -0
  244. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_lint.py +0 -0
  245. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_preview.py +0 -0
  246. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_library_boundary_closure.py +0 -0
  247. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_llms_txt.py +0 -0
  248. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_mail_merge_xlsx.py +0 -0
  249. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_markdown_export.py +0 -0
  250. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_master_page_authoring.py +0 -0
  251. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_memo_and_style_editing.py +0 -0
  252. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_memo_shape_authoring.py +0 -0
  253. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_moved_surface_hints.py +0 -0
  254. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_mutation_report.py +0 -0
  255. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_namespace_handling.py +0 -0
  256. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_namespace_hygiene.py +0 -0
  257. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_native_edit_contract.py +0 -0
  258. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_new_features.py +0 -0
  259. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_note_authoring_contract.py +0 -0
  260. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_fields.py +0 -0
  261. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_media.py +0 -0
  262. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_notes.py +0 -0
  263. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_page.py +0 -0
  264. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_refs.py +0 -0
  265. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_shapes.py +0 -0
  266. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_tables.py +0 -0
  267. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_tracking.py +0 -0
  268. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_opc_package.py +0 -0
  269. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_open_safety_corpus.py +0 -0
  270. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_outline_numbering.py +0 -0
  271. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_oxml_modularization.py +0 -0
  272. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_oxml_parsing.py +0 -0
  273. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_packaging_license_metadata.py +0 -0
  274. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_packaging_py_typed.py +0 -0
  275. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_page_number_control.py +0 -0
  276. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_para_pr_dedup.py +0 -0
  277. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_keep_together.py +0 -0
  278. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_property_version_switch.py +0 -0
  279. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_section_management.py +0 -0
  280. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_part_hierarchy_read_models.py +0 -0
  281. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_polygon_authoring.py +0 -0
  282. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_public_artifact_hygiene.py +0 -0
  283. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_public_runtime_boundary.py +0 -0
  284. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_read_fidelity.py +0 -0
  285. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_recover_broken_zip.py +0 -0
  286. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_redline_authoring.py +0 -0
  287. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_redline_verify.py +0 -0
  288. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_release_workflow_safety.py +0 -0
  289. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_repair_repack.py +0 -0
  290. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_report_utils.py +0 -0
  291. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_repr_snapshots.py +0 -0
  292. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_return_contract.py +0 -0
  293. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_roundtrip_fidelity.py +0 -0
  294. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_roundtrip_fidelity_corpus.py +0 -0
  295. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_row_heights.py +0 -0
  296. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_run_choice_atom_authoring.py +0 -0
  297. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_safe_edit_example.py +0 -0
  298. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_save_pipeline.py +0 -0
  299. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_section_headers.py +0 -0
  300. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_section_properties_real_roundtrip.py +0 -0
  301. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_set_paragraph_format_keep.py +0 -0
  302. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_shape_geometry_contract.py +0 -0
  303. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_shape_open_safety_risk_check.py +0 -0
  304. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_skeleton_template_ids.py +0 -0
  305. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_split_cell.py +0 -0
  306. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_split_merged_cell.py +0 -0
  307. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_stable_surface.py +0 -0
  308. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_style_resolution.py +0 -0
  309. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tab_definition_version_switch.py +0 -0
  310. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_cell_equalize.py +0 -0
  311. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_cleanup.py +0 -0
  312. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_label.py +0 -0
  313. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_navigation.py +0 -0
  314. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch.py +0 -0
  315. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch_dryrun.py +0 -0
  316. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch_m105.py +0 -0
  317. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_split_merge.py +0 -0
  318. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tables_default_border.py +0 -0
  319. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_template_analyzer_enrichment.py +0 -0
  320. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_text_extractor_annotations.py +0 -0
  321. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_text_extractor_run_choice_atoms.py +0 -0
  322. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_title_mark.py +0 -0
  323. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_toc_author.py +0 -0
  324. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_toc_fidelity.py +0 -0
  325. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tracked_overlap_render_smoke.py +0 -0
  326. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_validation_severity.py +0 -0
  327. {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_validator_comment_nodes.py +0 -0
  328. {python_hwpx-6.4.0 → python_hwpx-6.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: 6.4.0
3
+ Version: 6.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 = "6.4.0"
7
+ version = "6.5.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -78,6 +78,11 @@ _LATEX_TO_EQEDIT.update(
78
78
  "\\forall": "FORALL",
79
79
  "\\iint": "dint",
80
80
  "\\iiint": "tint",
81
+ # ``triangle`` is accepted when reading (observed in real documents) but
82
+ # only the documented uppercase spelling is emitted when authoring: the
83
+ # lowercase form has not been render-verified, and lowercase ``forall``
84
+ # already turned out to render as literal text.
85
+ "\\triangle": "TRIANGLE",
81
86
  # Common LaTeX aliases sharing a verified target.
82
87
  "\\le": "leq",
83
88
  "\\ge": "geq",
@@ -0,0 +1,135 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """LaTeX → MathML conversion via the optional ``latex2mathml`` dependency.
3
+
4
+ MathML keeps the preview self-contained: browsers render ``<math>`` natively, so
5
+ no script or font bundle is shipped. ``latex2mathml`` is an optional extra
6
+ (``python-hwpx[preview]``); when it is absent the caller fails closed to a LaTeX
7
+ code block rather than dropping the equation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import Callable
14
+
15
+ from .tokens import EQEDIT_MATHML_OPERATOR_COMMANDS
16
+
17
+ # ``False`` marks a resolved-but-unavailable converter; ``None`` means "not yet
18
+ # probed" so the import is attempted lazily on first use.
19
+ _converter: Callable[[str], str] | bool | None = None
20
+ _LATEX_WORD_COMMAND_RE = re.compile(r"\\[A-Za-z]+")
21
+ _TEXT_COMMAND = "\\text{"
22
+
23
+
24
+ class MathMLUnavailableError(RuntimeError):
25
+ """Raised when ``latex2mathml`` is not installed."""
26
+
27
+
28
+ def _load_converter() -> Callable[[str], str] | bool:
29
+ global _converter
30
+ cached = _converter
31
+ if cached is not None:
32
+ return cached
33
+ try:
34
+ from latex2mathml.converter import convert
35
+ except ImportError:
36
+ _converter = False
37
+ return False
38
+ _converter = convert
39
+ return convert
40
+
41
+
42
+ def latex2mathml_available() -> bool:
43
+ """Return ``True`` when the optional ``latex2mathml`` extra is importable."""
44
+
45
+ return _load_converter() is not False
46
+
47
+
48
+ def latex_to_mathml(latex: str) -> str:
49
+ """Convert a LaTeX fragment to an inline ``<math>`` MathML string.
50
+
51
+ Raises:
52
+ MathMLUnavailableError: when ``latex2mathml`` is not installed.
53
+ ValueError: when ``latex2mathml`` cannot parse the fragment.
54
+ """
55
+
56
+ convert = _load_converter()
57
+ if convert is False:
58
+ raise MathMLUnavailableError(
59
+ "latex2mathml is required for MathML rendering; install python-hwpx[preview]"
60
+ )
61
+ assert not isinstance(convert, bool) # narrowed for type-checkers
62
+ try:
63
+ return convert(latex)
64
+ except MathMLUnavailableError:
65
+ raise
66
+ except Exception as exc: # latex2mathml raises bare Exception on bad input
67
+ raise ValueError(f"latex2mathml could not render fragment: {exc}") from exc
68
+
69
+
70
+ def eqedit_latex_to_mathml(latex: str) -> str:
71
+ """Convert EqEdit-derived LaTeX while preserving known token roles.
72
+
73
+ ``latex2mathml`` classifies plain ``\\triangle`` as an identifier. EqEdit's
74
+ token map identifies it as a mathematical symbol, so the MathML-only input
75
+ receives an operator wrapper before conversion. The returned/public LaTeX
76
+ is not rewritten, and serialized MathML is never patched afterward.
77
+ """
78
+
79
+ def annotate_role(match: re.Match[str]) -> str:
80
+ command = match.group(0)
81
+ if command in EQEDIT_MATHML_OPERATOR_COMMANDS:
82
+ return rf"\mathop{{{command}}}"
83
+ return command
84
+
85
+ return latex_to_mathml(
86
+ "".join(
87
+ segment if literal else _LATEX_WORD_COMMAND_RE.sub(annotate_role, segment)
88
+ for segment, literal in _split_text_literals(latex)
89
+ )
90
+ )
91
+
92
+
93
+ def _split_text_literals(latex: str) -> list[tuple[str, bool]]:
94
+ """Split ``latex`` into ``(segment, is_text_literal)`` pieces.
95
+
96
+ Quoted EqEdit literals become ``\\text{...}``; their content is prose, so
97
+ the role annotation must not rewrite a ``\\triangle`` typed inside quotes.
98
+ Braces inside the literal are balanced; an unterminated group is passed
99
+ through untouched so ``latex2mathml`` reports it instead of this splitter.
100
+ """
101
+
102
+ pieces: list[tuple[str, bool]] = []
103
+ cursor = 0
104
+ while True:
105
+ start = latex.find(_TEXT_COMMAND, cursor)
106
+ if start < 0:
107
+ break
108
+ depth = 0
109
+ end = -1
110
+ for index in range(start + len(_TEXT_COMMAND) - 1, len(latex)):
111
+ char = latex[index]
112
+ if char == "{":
113
+ depth += 1
114
+ elif char == "}":
115
+ depth -= 1
116
+ if depth == 0:
117
+ end = index + 1
118
+ break
119
+ if end < 0:
120
+ break
121
+ if start > cursor:
122
+ pieces.append((latex[cursor:start], False))
123
+ pieces.append((latex[start:end], True))
124
+ cursor = end
125
+ if cursor < len(latex):
126
+ pieces.append((latex[cursor:], False))
127
+ return pieces
128
+
129
+
130
+ __all__ = [
131
+ "MathMLUnavailableError",
132
+ "eqedit_latex_to_mathml",
133
+ "latex2mathml_available",
134
+ "latex_to_mathml",
135
+ ]
@@ -18,7 +18,7 @@ from dataclasses import dataclass
18
18
  import html
19
19
 
20
20
  from .eqedit import EquationConversionError, eqedit_to_latex
21
- from .mathml import MathMLUnavailableError, latex_to_mathml
21
+ from .mathml import MathMLUnavailableError, eqedit_latex_to_mathml
22
22
 
23
23
  # Fidelity labels surfaced in the preview (Constitution IX, honest reporting).
24
24
  LABEL_MATHML = "수식 MathML 렌더"
@@ -63,7 +63,7 @@ def render_equation(script: str) -> EquationRender:
63
63
  return _fallback_block("script", LABEL_SCRIPT, script, latex=None)
64
64
 
65
65
  try:
66
- mathml = latex_to_mathml(latex)
66
+ mathml = eqedit_latex_to_mathml(latex)
67
67
  except MathMLUnavailableError:
68
68
  return _fallback_block("latex", LABEL_LATEX_NO_LIB, latex, latex=latex)
69
69
  except ValueError:
@@ -116,6 +116,11 @@ OPERATORS: dict[str, str] = {
116
116
  "partial": r"\partial",
117
117
  "nabla": r"\nabla",
118
118
  "angle": r"\angle",
119
+ # Hancom's symbol guide spells this token in uppercase, while legacy
120
+ # documents also contain the exact lowercase form. Mixed-case variants are
121
+ # intentionally not accepted because the reader uses exact token lookup.
122
+ "triangle": r"\triangle",
123
+ "TRIANGLE": r"\triangle",
119
124
  "cdots": r"\cdots",
120
125
  "ldots": r"\ldots",
121
126
  "vdots": r"\vdots",
@@ -254,11 +259,17 @@ DELIMITERS: dict[str, str] = {
254
259
  # Structural keywords handled directly by the parser (not simple substitution).
255
260
  STRUCTURAL = frozenset({"over", "atop", "sqrt", "root", "of", "LEFT", "RIGHT", "left", "right"})
256
261
 
262
+ # LaTeX commands emitted from EqEdit operator tokens that latex2mathml otherwise
263
+ # classifies as identifiers. The render boundary uses this metadata only while
264
+ # producing MathML; the public LaTeX remains the canonical command above.
265
+ EQEDIT_MATHML_OPERATOR_COMMANDS = frozenset({OPERATORS["triangle"]})
266
+
257
267
 
258
268
  __all__ = [
259
269
  "ACCENTS",
260
270
  "BIG_OPERATORS",
261
271
  "DELIMITERS",
272
+ "EQEDIT_MATHML_OPERATOR_COMMANDS",
262
273
  "FUNCTIONS",
263
274
  "GREEK",
264
275
  "MATRIX_ENVIRONMENTS",
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Small helpers for a style-preserving paragraph text edit."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import xml.etree.ElementTree as ET
7
+
8
+ from ..errors import HwpxValueError
9
+ from ._document_primitives import _is_tab_control_element
10
+ from .namespaces import tag_local_name
11
+
12
+
13
+ def plain_text_nodes_for_edit(runs: list[ET.Element]) -> list[ET.Element]:
14
+ nodes: list[ET.Element] = []
15
+ for run in runs:
16
+ for child in run:
17
+ if tag_local_name(child.tag) == "t":
18
+ if len(child):
19
+ raise HwpxValueError(
20
+ "mixed text markup cannot be edited safely",
21
+ code="paragraph-mixed-text-unsupported",
22
+ )
23
+ nodes.append(child)
24
+ elif tag_local_name(child.tag) == "tab" or _is_tab_control_element(child):
25
+ raise HwpxValueError(
26
+ "tab controls require an explicit run target",
27
+ code="paragraph-tab-target-required",
28
+ )
29
+ return nodes
30
+
31
+
32
+ def edit_node_candidates(
33
+ nodes: list[ET.Element], prefix: int, end: int
34
+ ) -> list[tuple[ET.Element, int]]:
35
+ offset = 0
36
+ candidates: list[tuple[ET.Element, int]] = []
37
+ for node in nodes:
38
+ length = len(node.text or "")
39
+ if offset <= prefix and end <= offset + length:
40
+ candidates.append((node, offset))
41
+ offset += length
42
+ return candidates
@@ -40,6 +40,7 @@ from .note_authoring import (
40
40
  _paragraph_endnotes,
41
41
  _paragraph_footnotes,
42
42
  )
43
+ from ._paragraph_text_edit import edit_node_candidates, plain_text_nodes_for_edit
43
44
  from .objects import (
44
45
  HwpxOxmlInlineObject,
45
46
  _create_picture_element,
@@ -336,6 +337,42 @@ class HwpxOxmlParagraph:
336
337
  _clear_paragraph_layout_cache(self.element)
337
338
  self.section.mark_dirty()
338
339
 
340
+ def set_text_preserving_runs(self, value: str) -> None:
341
+ """Replace one unambiguous text span without changing run boundaries.
342
+
343
+ The edit must fit inside one plain ``hp:t`` node. This covers a
344
+ correction within a styled run while retaining every other run and
345
+ control; cross-style replacements require an explicit run target.
346
+ """
347
+ from ..errors import HwpxValueError
348
+
349
+ nodes = plain_text_nodes_for_edit(self._run_elements())
350
+ old = "".join(node.text or "" for node in nodes)
351
+ if old == value:
352
+ return
353
+ if not nodes:
354
+ raise HwpxValueError("paragraph has no plain text node", code="paragraph-text-missing")
355
+ prefix = 0
356
+ while prefix < min(len(old), len(value)) and old[prefix] == value[prefix]:
357
+ prefix += 1
358
+ suffix = 0
359
+ while (suffix < len(old) - prefix and suffix < len(value) - prefix
360
+ and old[len(old) - suffix - 1] == value[len(value) - suffix - 1]):
361
+ suffix += 1
362
+ end = len(old) - suffix
363
+ replacement = value[prefix:len(value) - suffix if suffix else len(value)]
364
+ candidates = edit_node_candidates(nodes, prefix, end)
365
+ if len(candidates) != 1:
366
+ raise HwpxValueError(
367
+ "text edit crosses run boundaries or has ambiguous insertion style",
368
+ code="paragraph-text-style-ambiguous",
369
+ )
370
+ node, offset = candidates[0]
371
+ current = node.text or ""
372
+ node.text = _sanitize_text(current[:prefix - offset] + replacement + current[end - offset:])
373
+ _clear_paragraph_layout_cache(self.element)
374
+ self.section.mark_dirty()
375
+
339
376
  def clear_text(self) -> None:
340
377
  """Remove all text content while preserving styles and non-text elements.
341
378
 
@@ -464,6 +464,30 @@ class HwpxOxmlTable:
464
464
  ")"
465
465
  )
466
466
 
467
+ @property
468
+ def height(self) -> int:
469
+ """Declared table height in HWP units."""
470
+ size = self.element.find(f"{_HP}sz")
471
+ return int(size.get("height", "0")) if size is not None else 0
472
+
473
+ @property
474
+ def treat_as_char(self) -> bool:
475
+ """Whether Hancom lays out this table as one inline character."""
476
+ position = self.element.find(f"{_HP}pos")
477
+ return position is not None and position.get("treatAsChar", "1") == "1"
478
+
479
+ def set_treat_as_char(self, value: bool) -> None:
480
+ """Choose inline layout or flowing table layout across pages."""
481
+ position = self.element.find(f"{_HP}pos")
482
+ if position is None:
483
+ from ..errors import HwpxValueError
484
+
485
+ raise HwpxValueError("table has no hp:pos", code="table-position-missing")
486
+ desired = "1" if value else "0"
487
+ if position.get("treatAsChar") != desired:
488
+ position.set("treatAsChar", desired)
489
+ self.mark_dirty()
490
+
467
491
  # --- caption (hp:caption) -----------------------------------------
468
492
 
469
493
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 6.4.0
3
+ Version: 6.5.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -78,6 +78,7 @@ src/hwpx/opc/xml_utils.py
78
78
  src/hwpx/oxml/__init__.py
79
79
  src/hwpx/oxml/_document_impl.py
80
80
  src/hwpx/oxml/_document_primitives.py
81
+ src/hwpx/oxml/_paragraph_text_edit.py
81
82
  src/hwpx/oxml/body.py
82
83
  src/hwpx/oxml/canonical_defaults.py
83
84
  src/hwpx/oxml/color.py
@@ -306,6 +307,7 @@ tests/test_table_cell_equalize.py
306
307
  tests/test_table_cleanup.py
307
308
  tests/test_table_label.py
308
309
  tests/test_table_navigation.py
310
+ tests/test_table_page_flow.py
309
311
  tests/test_table_patch.py
310
312
  tests/test_table_patch_dryrun.py
311
313
  tests/test_table_patch_m105.py
@@ -222,6 +222,7 @@ class TestLatexToEqedit:
222
222
  (r"$x + 1$", "x + 1"),
223
223
  (r"\le \ge \ne", "leq geq neq"),
224
224
  (r"a \times b \cdot c \div d", "a times b cdot c div d"),
225
+ (r"\triangle P_1 P_2 Q", "TRIANGLE P _{1} P _{2} Q"),
225
226
  (r"\lim_{x \to 0} \frac{1}{x}", "lim _{x -> 0} {1} over {x}"),
226
227
  (r"a \to b", "a -> b"),
227
228
  (r"x \rightarrow y , u \leftarrow v", "x -> y , u leftarrow v"),
@@ -274,6 +275,7 @@ class TestRoundtripStability:
274
275
  r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}",
275
276
  r"\log_{2} x + \ln y",
276
277
  r"a \times b \pm c \mp d",
278
+ r"\triangle P_1 P_2 Q",
277
279
  r"\infty + \partial + \nabla",
278
280
  ]
279
281
 
@@ -112,6 +112,31 @@ def test_times_and_relations() -> None:
112
112
  assert eqedit_to_latex("a >= b") == r"a \geq b"
113
113
 
114
114
 
115
+ @pytest.mark.parametrize("token", ["triangle", "TRIANGLE"])
116
+ def test_triangle_symbol_uses_exact_evidence_backed_tokens(token: str) -> None:
117
+ assert eqedit_to_latex(f"{token} P_1 P_2 Q") == r"\triangle P_1 P_2 Q"
118
+
119
+
120
+ @pytest.mark.parametrize(
121
+ "script",
122
+ [
123
+ "Triangle PQR",
124
+ "triAngle PQR",
125
+ "triangles PQR",
126
+ "mytriangle PQR",
127
+ "triangleidentifier PQR",
128
+ "rmtriangle PQR",
129
+ "rm text",
130
+ ],
131
+ )
132
+ def test_triangle_support_does_not_split_identifiers_or_rm_text(script: str) -> None:
133
+ assert eqedit_to_latex(script) == script
134
+
135
+
136
+ def test_existing_angle_symbol_remains_distinct_from_triangle() -> None:
137
+ assert eqedit_to_latex("angle ABC") == r"\angle ABC"
138
+
139
+
115
140
  def test_parentheses_and_braces_passthrough() -> None:
116
141
  assert eqedit_to_latex("( a + b )") == "( a + b )"
117
142
  assert eqedit_to_latex("LEFT ( x RIGHT )") == r"\left( x \right)"
@@ -3,8 +3,11 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ from xml.etree import ElementTree
7
+
6
8
  import pytest
7
9
 
10
+ from hwpx.equation.mathml import _split_text_literals
8
11
  from hwpx.equation import (
9
12
  LABEL_LATEX_ERROR,
10
13
  LABEL_LATEX_NO_LIB,
@@ -25,6 +28,37 @@ def test_success_path_renders_inline_mathml() -> None:
25
28
  assert result.latex == r"\frac{\alpha}{\beta} + \pi"
26
29
 
27
30
 
31
+ @pytest.mark.parametrize("token", ["triangle", "TRIANGLE"])
32
+ def test_triangle_renders_as_mathml_operator_without_changing_latex(token: str) -> None:
33
+ pytest.importorskip("latex2mathml")
34
+ result = render_equation(f"{token} P_1 P_2 Q")
35
+ assert result.mode == "mathml"
36
+ assert result.latex == r"\triangle P_1 P_2 Q"
37
+ root = ElementTree.fromstring(result.html)
38
+ namespace = "{http://www.w3.org/1998/Math/MathML}"
39
+ triangle_operators = [
40
+ operator
41
+ for operator in root.iter(f"{namespace}mo")
42
+ if "△" in "".join(operator.itertext())
43
+ ]
44
+ assert len(triangle_operators) == 1
45
+ assert "triangle" not in "".join(root.itertext()).lower()
46
+
47
+
48
+ def test_quoted_triangle_literal_stays_text_and_is_not_annotated() -> None:
49
+ pytest.importorskip("latex2mathml")
50
+ # A quoted EqEdit literal becomes \text{...}; its content is prose and must
51
+ # not receive the operator wrapper (review on #93).
52
+ result = render_equation(r'"\triangle" + x')
53
+ assert result.mode == "mathml"
54
+ root = ElementTree.fromstring(result.html)
55
+ namespace = "{http://www.w3.org/1998/Math/MathML}"
56
+ texts = ["".join(node.itertext()) for node in root.iter(f"{namespace}mtext")]
57
+ assert texts == [r"\triangle"]
58
+ assert "mathop" not in result.html
59
+ assert "△" not in result.html
60
+
61
+
28
62
  def test_eqedit_failure_falls_back_to_original_script_block() -> None:
29
63
  # A brace/frac bomb exceeds the depth guard -> EqEdit->LaTeX fails.
30
64
  bomb = "{" * 200 + "x" + "}" * 200
@@ -67,3 +101,16 @@ def test_no_mode_ever_returns_empty_html() -> None:
67
101
  def test_latex2mathml_available_reflects_installation() -> None:
68
102
  # In the test extra latex2mathml is installed, so this is True.
69
103
  assert latex2mathml_available() is True
104
+
105
+
106
+ @pytest.mark.parametrize(
107
+ ("latex", "expected"),
108
+ [
109
+ (r"\triangle + x", [(r"\triangle + x", False)]),
110
+ (r"\text{\triangle} + x", [(r"\text{\triangle}", True), (" + x", False)]),
111
+ (r"a \text{b {c} d} \triangle", [("a ", False), (r"\text{b {c} d}", True), (r" \triangle", False)]),
112
+ (r"\text{open", [(r"\text{open", False)]),
113
+ ],
114
+ )
115
+ def test_split_text_literals_keeps_quoted_content_opaque(latex: str, expected: list[tuple[str, bool]]) -> None:
116
+ assert _split_text_literals(latex) == expected
@@ -9,6 +9,7 @@ belong in this suite.
9
9
  from __future__ import annotations
10
10
 
11
11
  import json
12
+ import subprocess
12
13
  import sys
13
14
  from collections.abc import Callable
14
15
  from io import BytesIO
@@ -19,7 +20,6 @@ import pytest
19
20
 
20
21
  from hwpx import HwpxDocument
21
22
 
22
-
23
23
  CONTRACT_PATH = Path(__file__).parent / "data" / "golden_api_contract.json"
24
24
  Scenario = Callable[[Path], None]
25
25
 
@@ -278,9 +278,28 @@ def test_golden_api_contract_is_complete() -> None:
278
278
 
279
279
  @pytest.mark.parametrize("scenario_id", CONTRACT["scenarios"])
280
280
  def test_core_only_golden_scenario(scenario_id: str, tmp_path: Path) -> None:
281
- SCENARIOS[scenario_id](tmp_path)
282
- assert not any(
283
- module_name == prefix or module_name.startswith(f"{prefix}.")
284
- for prefix in CONTRACT["forbiddenImportPrefixes"]
285
- for module_name in sys.modules
281
+ # A fresh interpreter tests core-only imports even when another collected
282
+ # test imported automation into the pytest process.
283
+ code = """
284
+ import importlib.util
285
+ import json
286
+ import sys
287
+ from pathlib import Path
288
+ spec = importlib.util.spec_from_file_location('golden_api_scenarios', sys.argv[1])
289
+ module = importlib.util.module_from_spec(spec)
290
+ sys.modules[spec.name] = module
291
+ spec.loader.exec_module(module)
292
+ module.SCENARIOS[sys.argv[2]](Path(sys.argv[3]))
293
+ assert not any(
294
+ name == prefix or name.startswith(prefix + '.')
295
+ for prefix in module.CONTRACT['forbiddenImportPrefixes']
296
+ for name in sys.modules
297
+ )
298
+ """
299
+ result = subprocess.run(
300
+ [sys.executable, "-c", code, str(Path(__file__).resolve()), scenario_id, str(tmp_path)],
301
+ capture_output=True,
302
+ text=True,
303
+ check=False,
286
304
  )
305
+ assert result.returncode == 0, result.stdout + result.stderr
@@ -298,7 +298,8 @@ def test_real_tree_gate_runs_from_a_gitless_source_copy(tmp_path: Path) -> None:
298
298
  # (oxml/note_authoring.py) — restores real headroom (1452/1600)
299
299
  # instead of the 5 lines left after train 48/㊽b's PATH delegator.
300
300
  # 전부 module-ownership.json 에 개별 rationale 과 함께 등재돼 있다.
301
- assert report["classifiedFiles"] == 147
301
+ # +1: format-level plain-text edit helpers extracted from paragraph.py.
302
+ assert report["classifiedFiles"] == 148
302
303
 
303
304
 
304
305
  def test_gitless_cli_reproduces_literal_dynamic_import_failure_without_mutating_source(
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from hwpx.document import HwpxDocument
4
+
5
+
6
+ def test_table_treat_as_char_is_explicit_and_persists() -> None:
7
+ with HwpxDocument.new() as document:
8
+ table = document.add_table(2, 1)
9
+ assert table.treat_as_char is True
10
+ original_height = table.height
11
+ table.set_treat_as_char(False)
12
+ assert table.treat_as_char is False
13
+ reopened = HwpxDocument.open(document.to_bytes())
14
+ try:
15
+ assert reopened.tables.all[0].treat_as_char is False
16
+ assert reopened.tables.all[0].height == original_height
17
+ finally:
18
+ reopened.close()
@@ -1,69 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
- """LaTeX → MathML conversion via the optional ``latex2mathml`` dependency.
3
-
4
- MathML keeps the preview self-contained: browsers render ``<math>`` natively, so
5
- no script or font bundle is shipped. ``latex2mathml`` is an optional extra
6
- (``python-hwpx[preview]``); when it is absent the caller fails closed to a LaTeX
7
- code block rather than dropping the equation.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from typing import Callable
13
-
14
- # ``False`` marks a resolved-but-unavailable converter; ``None`` means "not yet
15
- # probed" so the import is attempted lazily on first use.
16
- _converter: Callable[[str], str] | bool | None = None
17
-
18
-
19
- class MathMLUnavailableError(RuntimeError):
20
- """Raised when ``latex2mathml`` is not installed."""
21
-
22
-
23
- def _load_converter() -> Callable[[str], str] | bool:
24
- global _converter
25
- cached = _converter
26
- if cached is not None:
27
- return cached
28
- try:
29
- from latex2mathml.converter import convert
30
- except ImportError:
31
- _converter = False
32
- return False
33
- _converter = convert
34
- return convert
35
-
36
-
37
- def latex2mathml_available() -> bool:
38
- """Return ``True`` when the optional ``latex2mathml`` extra is importable."""
39
-
40
- return _load_converter() is not False
41
-
42
-
43
- def latex_to_mathml(latex: str) -> str:
44
- """Convert a LaTeX fragment to an inline ``<math>`` MathML string.
45
-
46
- Raises:
47
- MathMLUnavailableError: when ``latex2mathml`` is not installed.
48
- ValueError: when ``latex2mathml`` cannot parse the fragment.
49
- """
50
-
51
- convert = _load_converter()
52
- if convert is False:
53
- raise MathMLUnavailableError(
54
- "latex2mathml is required for MathML rendering; install python-hwpx[preview]"
55
- )
56
- assert not isinstance(convert, bool) # narrowed for type-checkers
57
- try:
58
- return convert(latex)
59
- except MathMLUnavailableError:
60
- raise
61
- except Exception as exc: # latex2mathml raises bare Exception on bad input
62
- raise ValueError(f"latex2mathml could not render fragment: {exc}") from exc
63
-
64
-
65
- __all__ = [
66
- "MathMLUnavailableError",
67
- "latex2mathml_available",
68
- "latex_to_mathml",
69
- ]
File without changes
File without changes
File without changes
File without changes