python-hwpx 6.2.1__tar.gz → 6.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 (322) hide show
  1. {python_hwpx-6.2.1/src/python_hwpx.egg-info → python_hwpx-6.3.0}/PKG-INFO +1 -1
  2. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/pyproject.toml +2 -2
  3. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/_document_primitives.py +6 -2
  4. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/body.py +4 -1
  5. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/memo.py +3 -1
  6. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/note_authoring.py +1 -1
  7. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/table_patch.py +29 -0
  8. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/markdown_export.py +1 -1
  9. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/object_finder.py +1 -1
  10. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/package_validator.py +78 -8
  11. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/text_extractor.py +1 -1
  12. {python_hwpx-6.2.1 → python_hwpx-6.3.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  13. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/SOURCES.txt +3 -0
  14. python_hwpx-6.3.0/tests/test_instid_attribute_case.py +100 -0
  15. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_layout_lint.py +82 -0
  16. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_release_workflow_safety.py +8 -8
  17. python_hwpx-6.3.0/tests/test_roundtrip_fidelity_corpus.py +47 -0
  18. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_patch.py +67 -0
  19. python_hwpx-6.3.0/tests/test_tracked_overlap_render_smoke.py +112 -0
  20. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/LICENSE +0 -0
  21. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/NOTICE +0 -0
  22. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/README.md +0 -0
  23. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/setup.cfg +0 -0
  24. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/__init__.py +0 -0
  25. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/__init__.py +0 -0
  26. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/_legacy.py +0 -0
  27. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/_resolve.py +0 -0
  28. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/_units.py +0 -0
  29. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/fields.py +0 -0
  30. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/headings.py +0 -0
  31. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/highlight.py +0 -0
  32. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/layout.py +0 -0
  33. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/media.py +0 -0
  34. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/memos.py +0 -0
  35. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/__init__.py +0 -0
  36. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/_base.py +0 -0
  37. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/fields.py +0 -0
  38. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/media.py +0 -0
  39. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/notes.py +0 -0
  40. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/page.py +0 -0
  41. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/parts.py +0 -0
  42. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/refs.py +0 -0
  43. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/shapes.py +0 -0
  44. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/styles.py +0 -0
  45. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/tables.py +0 -0
  46. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/text.py +0 -0
  47. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/ns/tracking.py +0 -0
  48. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/persistence.py +0 -0
  49. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/shapes.py +0 -0
  50. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/_document/tracked.py +0 -0
  51. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/body_patch.py +0 -0
  52. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/capabilities.py +0 -0
  53. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  54. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
  55. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
  56. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
  57. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/support-matrix.md +0 -0
  58. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/document.py +0 -0
  59. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/__init__.py +0 -0
  60. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/authoring.py +0 -0
  61. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/eqedit.py +0 -0
  62. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/mathml.py +0 -0
  63. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/render.py +0 -0
  64. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/equation/tokens.py +0 -0
  65. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/errors.py +0 -0
  66. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/experimental.py +0 -0
  67. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/__init__.py +0 -0
  68. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/apply.py +0 -0
  69. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/engine.py +0 -0
  70. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/measure.py +0 -0
  71. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/policy.py +0 -0
  72. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/form_fit/report.py +0 -0
  73. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/ingest/__init__.py +0 -0
  74. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/ingest/base.py +0 -0
  75. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  76. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/layout/__init__.py +0 -0
  77. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/layout/lint.py +0 -0
  78. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/layout/report.py +0 -0
  79. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/model.py +0 -0
  80. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/mutation_report.py +0 -0
  81. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/__init__.py +0 -0
  82. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/binary_item.py +0 -0
  83. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/checkbox.py +0 -0
  84. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/form_field.py +0 -0
  85. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/highlight.py +0 -0
  86. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/results.py +0 -0
  87. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/objects/tracked.py +0 -0
  88. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/opc/package.py +0 -0
  89. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/opc/relationships.py +0 -0
  90. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/opc/security.py +0 -0
  91. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/opc/xml_utils.py +0 -0
  92. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/__init__.py +0 -0
  93. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/_document_impl.py +0 -0
  94. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  95. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/color.py +0 -0
  96. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/common.py +0 -0
  97. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/document.py +0 -0
  98. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/document_metadata.py +0 -0
  99. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/document_parts.py +0 -0
  100. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/drop_cap.py +0 -0
  101. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/dutmal_compose.py +0 -0
  102. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/field_marks.py +0 -0
  103. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/header.py +0 -0
  104. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/header_compat.py +0 -0
  105. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/header_part.py +0 -0
  106. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/history_part.py +0 -0
  107. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/master_page.py +0 -0
  108. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/master_page_authoring.py +0 -0
  109. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/namespaces.py +0 -0
  110. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/numbering.py +0 -0
  111. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/numbering_kinds.py +0 -0
  112. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/objects.py +0 -0
  113. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/paragraph.py +0 -0
  114. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/parser.py +0 -0
  115. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/run.py +0 -0
  116. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/schema.py +0 -0
  117. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/section.py +0 -0
  118. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/section_format.py +0 -0
  119. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/section_layout.py +0 -0
  120. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/section_story.py +0 -0
  121. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/settings.py +0 -0
  122. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/simple_parts.py +0 -0
  123. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/table.py +0 -0
  124. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/utils.py +0 -0
  125. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/oxml/version_part.py +0 -0
  126. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/package.py +0 -0
  127. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/patch.py +0 -0
  128. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/plan/__init__.py +0 -0
  129. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/plan/_execute.py +0 -0
  130. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/plan/_model.py +0 -0
  131. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/plan/_schema.py +0 -0
  132. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/plan/_validate.py +0 -0
  133. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/py.typed +0 -0
  134. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/__init__.py +0 -0
  135. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/ledger.py +0 -0
  136. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/policy.py +0 -0
  137. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/rendering.py +0 -0
  138. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/report.py +0 -0
  139. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/quality/save_pipeline.py +0 -0
  140. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/templates.py +0 -0
  141. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/__init__.py +0 -0
  142. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  143. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  144. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/archive_cli.py +0 -0
  145. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/doc_diff.py +0 -0
  146. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/document_merge.py +0 -0
  147. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/document_viewer.py +0 -0
  148. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/exporter.py +0 -0
  149. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/generic_inventory.py +0 -0
  150. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/id_integrity.py +0 -0
  151. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/idempotence.py +0 -0
  152. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/ir_equality.py +0 -0
  153. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/layout_preview.py +0 -0
  154. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/mail_merge.py +0 -0
  155. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/package_reconcile.py +0 -0
  156. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/page_guard.py +0 -0
  157. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/read_fidelity.py +0 -0
  158. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/recover.py +0 -0
  159. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/redline.py +0 -0
  160. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/repair.py +0 -0
  161. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/report_utils.py +0 -0
  162. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  163. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/table_cleanup.py +0 -0
  164. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/table_navigation.py +0 -0
  165. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/template_analyzer.py +0 -0
  166. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  167. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/toc_author.py +0 -0
  168. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  169. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/hwpx/tools/validator.py +0 -0
  170. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  171. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  172. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  173. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  174. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_add_control.py +0 -0
  175. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_add_heading.py +0 -0
  176. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  177. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_arc_authoring.py +0 -0
  178. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_authoring_defaults.py +0 -0
  179. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_authoring_quality_corpus.py +0 -0
  180. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_blind_eval_fixture.py +0 -0
  181. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_body_patch.py +0 -0
  182. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_border_fill_image_gradient.py +0 -0
  183. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_build_element_census.py +0 -0
  184. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_byte_patch_identity.py +0 -0
  185. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_capabilities_surface.py +0 -0
  186. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_cell_line_spacing.py +0 -0
  187. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_chart_authoring.py +0 -0
  188. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_check_box_authoring.py +0 -0
  189. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_column_break.py +0 -0
  190. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_comment_node_robustness.py +0 -0
  191. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_container_authoring.py +0 -0
  192. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_contract_docs_sync.py +0 -0
  193. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_corpus_read_fidelity.py +0 -0
  194. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_coverage_ledger.py +0 -0
  195. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_coverage_promotion.py +0 -0
  196. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_coverage_targets.py +0 -0
  197. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_deviations_registry.py +0 -0
  198. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_doc_diff.py +0 -0
  199. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_context_manager.py +0 -0
  200. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_facade_surface.py +0 -0
  201. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_formatting.py +0 -0
  202. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_merge.py +0 -0
  203. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_metadata.py +0 -0
  204. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  205. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_save_api.py +0 -0
  206. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_document_viewer.py +0 -0
  207. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_documentation_code_fences.py +0 -0
  208. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_drop_cap_authoring.py +0 -0
  209. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_dutmal_compose_authoring.py +0 -0
  210. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_edit_plan.py +0 -0
  211. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_editor_surface_inventory.py +0 -0
  212. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_engine_surface_6_0.py +0 -0
  213. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_equation_authoring.py +0 -0
  214. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_equation_converter.py +0 -0
  215. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_equation_render.py +0 -0
  216. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_error_contract.py +0 -0
  217. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_error_messages.py +0 -0
  218. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_evalplan_core_primitives.py +0 -0
  219. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_existing_document_format_editing.py +0 -0
  220. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_field_marks.py +0 -0
  221. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_form_field_authoring.py +0 -0
  222. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_form_fields.py +0 -0
  223. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_form_fill_core_primitives.py +0 -0
  224. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_form_fit.py +0 -0
  225. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_form_fit_integration.py +0 -0
  226. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  227. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_fuzz_loop.py +0 -0
  228. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_fuzz_regressions.py +0 -0
  229. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_gap_closure_tools.py +0 -0
  230. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_golden_api.py +0 -0
  231. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_header_compat_authoring.py +0 -0
  232. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_highlight_authoring.py +0 -0
  233. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_hp_tab_support.py +0 -0
  234. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  235. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_id_generator_range.py +0 -0
  236. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_id_integrity.py +0 -0
  237. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_image_object_workflow.py +0 -0
  238. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_index_mark.py +0 -0
  239. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ingest.py +0 -0
  240. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_inline_models.py +0 -0
  241. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  242. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_integration_roundtrip.py +0 -0
  243. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_layout_cache_scope.py +0 -0
  244. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_layout_preview.py +0 -0
  245. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_library_boundary_closure.py +0 -0
  246. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_llms_txt.py +0 -0
  247. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_mail_merge_xlsx.py +0 -0
  248. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_markdown_export.py +0 -0
  249. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_master_page_authoring.py +0 -0
  250. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_memo_and_style_editing.py +0 -0
  251. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_memo_shape_authoring.py +0 -0
  252. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_moved_surface_hints.py +0 -0
  253. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_mutation_report.py +0 -0
  254. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_namespace_handling.py +0 -0
  255. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_namespace_hygiene.py +0 -0
  256. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_new_features.py +0 -0
  257. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_note_authoring_contract.py +0 -0
  258. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_fields.py +0 -0
  259. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_media.py +0 -0
  260. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_notes.py +0 -0
  261. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_page.py +0 -0
  262. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_refs.py +0 -0
  263. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_shapes.py +0 -0
  264. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_tables.py +0 -0
  265. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_ns_tracking.py +0 -0
  266. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_opc_package.py +0 -0
  267. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_open_safety_corpus.py +0 -0
  268. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_outline_numbering.py +0 -0
  269. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_oxml_modularization.py +0 -0
  270. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_oxml_parsing.py +0 -0
  271. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_packaging_license_metadata.py +0 -0
  272. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_packaging_py_typed.py +0 -0
  273. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_page_number_control.py +0 -0
  274. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_para_pr_dedup.py +0 -0
  275. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_paragraph_keep_together.py +0 -0
  276. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_paragraph_property_version_switch.py +0 -0
  277. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_paragraph_section_management.py +0 -0
  278. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_part_hierarchy_read_models.py +0 -0
  279. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_polygon_authoring.py +0 -0
  280. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_product_boundary.py +0 -0
  281. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_public_artifact_hygiene.py +0 -0
  282. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_public_runtime_boundary.py +0 -0
  283. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_read_fidelity.py +0 -0
  284. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_recover_broken_zip.py +0 -0
  285. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_redline_authoring.py +0 -0
  286. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_redline_verify.py +0 -0
  287. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_repair_repack.py +0 -0
  288. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_report_utils.py +0 -0
  289. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_repr_snapshots.py +0 -0
  290. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_return_contract.py +0 -0
  291. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_roundtrip_fidelity.py +0 -0
  292. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_row_heights.py +0 -0
  293. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_run_choice_atom_authoring.py +0 -0
  294. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_save_pipeline.py +0 -0
  295. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_section_headers.py +0 -0
  296. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_section_properties_real_roundtrip.py +0 -0
  297. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_set_paragraph_format_keep.py +0 -0
  298. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_shape_geometry_contract.py +0 -0
  299. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_shape_open_safety_risk_check.py +0 -0
  300. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_skeleton_template_ids.py +0 -0
  301. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_split_cell.py +0 -0
  302. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_split_merged_cell.py +0 -0
  303. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_stable_surface.py +0 -0
  304. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_style_resolution.py +0 -0
  305. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_tab_definition_version_switch.py +0 -0
  306. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_cell_equalize.py +0 -0
  307. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_cleanup.py +0 -0
  308. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_label.py +0 -0
  309. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_navigation.py +0 -0
  310. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_patch_dryrun.py +0 -0
  311. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_patch_m105.py +0 -0
  312. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_table_split_merge.py +0 -0
  313. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_tables_default_border.py +0 -0
  314. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_template_analyzer_enrichment.py +0 -0
  315. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_text_extractor_annotations.py +0 -0
  316. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_text_extractor_run_choice_atoms.py +0 -0
  317. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_title_mark.py +0 -0
  318. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_toc_author.py +0 -0
  319. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_toc_fidelity.py +0 -0
  320. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_validation_severity.py +0 -0
  321. {python_hwpx-6.2.1 → python_hwpx-6.3.0}/tests/test_validator_comment_nodes.py +0 -0
  322. {python_hwpx-6.2.1 → python_hwpx-6.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: 6.2.1
3
+ Version: 6.3.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools==83.0.0", "wheel==0.47.0"]
2
+ requires = ["setuptools==84.0.0", "wheel==0.48.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "6.2.1"
7
+ version = "6.3.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -200,8 +200,12 @@ def _refresh_copied_paragraph_subtree_ids(paragraph: ET.Element) -> None:
200
200
  }:
201
201
  node.set("id", _object_id())
202
202
 
203
- if "instId" in node.attrib:
204
- node.set("instId", _object_id())
203
+ # 스키마·실한컴 산출물의 속성명은 소문자 "instid"다. 과거 이 코드가
204
+ # 방출한 "instId"(카멜케이스)를 실은 파일도 재발급 대상에 남긴다 —
205
+ # 속성명 자체는 보존(바이트 보수), 값만 재발급.
206
+ for inst_attr in ("instid", "instId"):
207
+ if inst_attr in node.attrib:
208
+ node.set(inst_attr, _object_id())
205
209
 
206
210
 
207
211
  def _clone_paragraph_element(paragraph: ET.Element) -> ET.Element:
@@ -32,7 +32,10 @@ INLINE_OBJECT_NAMES = {
32
32
  "ole",
33
33
  "chart",
34
34
  "video",
35
- "audio",
35
+ # "audio"는 제외 — 실한컴(Windows 한글 2024 실측)은 소리 삽입을
36
+ # hp:ole(EMBEDDED/ICON)로 방출하고 hp:audio를 생성하지 않으며, 보유
37
+ # 실코퍼스 표본도 0건이다. 실물 미검증 요소를 인라인 읽기 지원으로
38
+ # 분류하지 않는다(빈도 0 = 읽기측 불투명 보존만). python-hwpx#89.
36
39
  "textart",
37
40
  }
38
41
 
@@ -277,7 +277,9 @@ class HwpxOxmlNote:
277
277
 
278
278
  @property
279
279
  def inst_id(self) -> str | None:
280
- return self.element.get("instId")
280
+ # 정본 속성명은 "instid"(스키마·실한컴). "instId"는 과거 우리 저작이
281
+ # 방출한 카멜케이스 산출물 호환용 폴백.
282
+ return self.element.get("instid") or self.element.get("instId")
281
283
 
282
284
  @property
283
285
  def text(self) -> str:
@@ -128,7 +128,7 @@ def _paragraph_add_note(
128
128
  {
129
129
  "number": str(number),
130
130
  "suffixChar": str(ord(suffix[0])) if suffix else "41",
131
- "instId": _object_id(),
131
+ "instid": _object_id(),
132
132
  },
133
133
  )
134
134
  sublist_attrs = _default_sublist_attributes()
@@ -1053,6 +1053,34 @@ def _delete_rows(table: str, del_rows: Iterable[int]) -> str:
1053
1053
  return _rebuild(prefix, rows, suffix, rowcnt=len(rows))
1054
1054
 
1055
1055
 
1056
+ def _reorder_rows(table: str, order: Sequence[int]) -> str:
1057
+ """물리 행 재배열 — 편집기 [표>정렬…] 대응의 엔진 프리미티브.
1058
+
1059
+ *order*는 현재 물리 행 인덱스의 완전 순열: 새 ``i``번째 행 = 기존
1060
+ ``order[i]``. 행 문자열을 통째로 옮기고 각 셀의 ``rowAddr``만 목적지
1061
+ 인덱스로 재기입한다(행 높이·서식·문단은 행과 함께 이동). 수직 병합
1062
+ (rowSpan>1)이 하나라도 있으면 refuse — 병합 블록을 가로지르는 재배열은
1063
+ 의미가 정의되지 않는다. 정렬 키 비교(어떤 순서로 놓을지)는 호출자 몫이다.
1064
+ """
1065
+ _guard_flat(table)
1066
+ prefix, rows, suffix = _parse_table(table)
1067
+ if sorted(order) != list(range(len(rows))):
1068
+ raise TableStructureError(
1069
+ f"reorder_rows: order must be a permutation of 0..{len(rows) - 1}, got {list(order)}"
1070
+ )
1071
+ for row in rows:
1072
+ for tc in _S_TC.findall(row):
1073
+ if (_si(tc, "cellSpan", "rowSpan") or 1) > 1:
1074
+ raise TableStructureError(
1075
+ "reorder_rows: vertical merge (rowSpan>1) present — refuse"
1076
+ )
1077
+ new_rows = [
1078
+ _map_cells(rows[src], lambda tc, dest=dest: _ss(tc, "cellAddr", "rowAddr", dest))
1079
+ for dest, src in enumerate(order)
1080
+ ]
1081
+ return _rebuild(prefix, new_rows, suffix)
1082
+
1083
+
1056
1084
  _PARA_ID_RE = re.compile(r'(<hp:p\b[^>]*\bid=")(\d+)(")')
1057
1085
 
1058
1086
 
@@ -1389,6 +1417,7 @@ def _widths_arg(o: Mapping[str, Any]) -> dict[int, int]:
1389
1417
  _STRUCT_OPS = {
1390
1418
  "delete_column": lambda t, o: _collapse_empty_rows(_delete_columns(t, o["cols"] if "cols" in o else [o["col"]])),
1391
1419
  "delete_row": lambda t, o: _delete_rows(t, o["rows"] if "rows" in o else [o["row"]]),
1420
+ "reorder_rows": lambda t, o: _reorder_rows(t, [int(x) for x in o["order"]]),
1392
1421
  "insert_row_by_clone": lambda t, o: _insert_row_by_clone(t, o["ref_row"], int(o.get("count", 1))),
1393
1422
  "insert_block_by_clone": lambda t, o: _insert_block_by_clone(t, int(o["ref_rows"][0]), int(o["ref_rows"][1]), int(o.get("count", 1))),
1394
1423
  "set_column_widths": lambda t, o: _set_column_widths(t, _widths_arg(o)),
@@ -261,7 +261,7 @@ def _md_handle_ctrl_child(child, state: _MdParagraphState, base_cp, chars, doc=N
261
261
  def _md_handle_note_child(
262
262
  child, tag: str, state: _MdParagraphState, base_cp, chars, doc, notes_out: list | None
263
263
  ) -> None:
264
- inst_id = child.get("instId", "")
264
+ inst_id = child.get("instid") or child.get("instId", "")
265
265
  kind = "fn" if tag == "footNote" else "en"
266
266
  marker = f"[^{kind}{inst_id}]"
267
267
  if state.link_url is not None:
@@ -317,7 +317,7 @@ class ObjectFinder:
317
317
  ) -> AnnotationMatch:
318
318
  path = describe_element_path(element, parent_map)
319
319
  found = FoundElement(section=section, path=path, element=element)
320
- inst_id = element.get("instId") or ""
320
+ inst_id = element.get("instid") or element.get("instId") or ""
321
321
  behavior = options.footnote if kind == "footnote" else options.endnote
322
322
  if behavior == "inline":
323
323
  text = _resolve_note_text(
@@ -331,14 +331,17 @@ def _first_child_by_local(element: ET.Element, name: str) -> ET.Element | None:
331
331
  return None
332
332
 
333
333
 
334
- def _simple_paragraph_text_length(paragraph: ET.Element) -> int | None:
335
- """Return visible text length for plain text-only paragraphs.
334
+ def _simple_paragraph_text(paragraph: ET.Element) -> str | None:
335
+ """Return visible text for plain text-only paragraphs (``None`` = 판정 불가).
336
336
 
337
337
  Paragraphs containing fields, shapes, tables, or other embedded controls are
338
338
  skipped to avoid guessing how a specific editor counts their layout units.
339
+ Single-position control characters (tab/linebreak/hyphen/nbspace) are
340
+ represented as one space — a deliberate width underestimate so downstream
341
+ width judgments stay conservative.
339
342
  """
340
343
 
341
- total = 0
344
+ pieces: list[str] = []
342
345
  for child in paragraph:
343
346
  child_name = _local_name(child).lower()
344
347
  if child_name == "linesegarray":
@@ -348,12 +351,73 @@ def _simple_paragraph_text_length(paragraph: ET.Element) -> int | None:
348
351
  for run_child in child:
349
352
  run_child_name = _local_name(run_child).lower()
350
353
  if run_child_name == "t":
351
- total += len("".join(run_child.itertext()))
354
+ pieces.append("".join(run_child.itertext()))
352
355
  elif run_child_name in {"tab", "linebreak", "hyphen", "nbspace"}:
353
- total += 1
356
+ pieces.append(" ")
354
357
  else:
355
358
  return None
356
- return total
359
+ return "".join(pieces)
360
+
361
+
362
+ def _simple_paragraph_text_length(paragraph: ET.Element) -> int | None:
363
+ text = _simple_paragraph_text(paragraph)
364
+ return None if text is None else len(text)
365
+
366
+
367
+ #: 꼬리 폭 판정 마진 — 추정 폭이 줄 폭의 이 배수를 넘을 때만 증명으로 취급.
368
+ _STALE_TAIL_WIDTH_FACTOR = 2.0
369
+ _STALE_TAIL_MIN_CHARS = 8
370
+
371
+
372
+ def _check_line_seg_tail_coverage(
373
+ issues: list[PackageValidationIssue],
374
+ part_name: str,
375
+ paragraph_index: int,
376
+ text: str,
377
+ line_segs: list[ET.Element],
378
+ ) -> None:
379
+ """Catch caches that lack lines for text grown past the last cached line.
380
+
381
+ The stale-cache direction the textpos range check cannot see: an edit made
382
+ the paragraph *longer* but an old ``<hp:linesegarray>`` survived, so every
383
+ cached ``textpos`` is still in range — yet the cache has too few lines and
384
+ Hancom renders the tail overlapped (the tracked-change overlap defect).
385
+ Provable-with-margin: the tail after the last cached line start must not
386
+ measure wider than that line's extent × ``_STALE_TAIL_WIDTH_FACTOR``,
387
+ with width estimated by the same conservative metrics the overflow lint
388
+ uses.
389
+ """
390
+
391
+ last: tuple[int, int, int] | None = None
392
+ for seg in line_segs:
393
+ try:
394
+ textpos = int(seg.get("textpos") or "")
395
+ horzsize = int(seg.get("horzsize") or "")
396
+ textheight = int(seg.get("textheight") or "")
397
+ except ValueError:
398
+ return
399
+ if last is None or textpos > last[0]:
400
+ last = (textpos, horzsize, textheight)
401
+ if last is None:
402
+ return
403
+ last_pos, horzsize, textheight = last
404
+ if horzsize <= 0 or textheight <= 0 or not 0 <= last_pos <= len(text):
405
+ return
406
+ tail = text[last_pos:]
407
+ if len(tail) < _STALE_TAIL_MIN_CHARS:
408
+ return
409
+ from hwpx.form_fit.measure import estimate_text_width
410
+
411
+ tail_width = estimate_text_width(tail, textheight / 100.0)
412
+ if tail_width > horzsize * _STALE_TAIL_WIDTH_FACTOR:
413
+ _error(
414
+ issues,
415
+ part_name,
416
+ f"paragraph {paragraph_index} lineseg cache lacks lines for its tail: "
417
+ f"{len(tail)} chars after the last cached line start (textpos={last_pos}) "
418
+ f"measure ~{tail_width / horzsize:.1f}x the cached line extent {horzsize} "
419
+ "— stale layout cache; Hancom reuses it and renders the tail overlapped",
420
+ )
357
421
 
358
422
 
359
423
  def _check_line_seg_text_positions(
@@ -367,15 +431,18 @@ def _check_line_seg_text_positions(
367
431
  for paragraph_index, paragraph in enumerate(
368
432
  element for element in root.iter() if _local_name(element) == "p"
369
433
  ):
370
- text_length = _simple_paragraph_text_length(paragraph)
371
- if text_length is None:
434
+ text = _simple_paragraph_text(paragraph)
435
+ if text is None:
372
436
  continue
437
+ text_length = len(text)
373
438
  for child in paragraph:
374
439
  if _local_name(child).lower() != "linesegarray":
375
440
  continue
441
+ line_segs: list[ET.Element] = []
376
442
  for line_seg in child:
377
443
  if _local_name(line_seg).lower() != "lineseg":
378
444
  continue
445
+ line_segs.append(line_seg)
379
446
  textpos_raw = line_seg.get("textpos")
380
447
  if textpos_raw is None:
381
448
  continue
@@ -396,6 +463,9 @@ def _check_line_seg_text_positions(
396
463
  f"{paragraph_index} has stale lineseg textpos={textpos} "
397
464
  f"beyond text length {text_length}",
398
465
  )
466
+ _check_line_seg_tail_coverage(
467
+ issues, part_name, paragraph_index, text, line_segs
468
+ )
399
469
 
400
470
 
401
471
  def _check_table_editor_acceptance(
@@ -483,7 +483,7 @@ class TextExtractor:
483
483
  return
484
484
 
485
485
  kind_name = "footnote" if kind == "footNote" else "endnote"
486
- inst_id = element.get("instId") or ""
486
+ inst_id = element.get("instid") or element.get("instId") or ""
487
487
 
488
488
  if option == "placeholder":
489
489
  fragments.append(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 6.2.1
3
+ Version: 6.3.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -230,6 +230,7 @@ tests/test_image_object_workflow.py
230
230
  tests/test_index_mark.py
231
231
  tests/test_ingest.py
232
232
  tests/test_inline_models.py
233
+ tests/test_instid_attribute_case.py
233
234
  tests/test_integration_hwpx_compatibility.py
234
235
  tests/test_integration_roundtrip.py
235
236
  tests/test_layout_cache_scope.py
@@ -283,6 +284,7 @@ tests/test_report_utils.py
283
284
  tests/test_repr_snapshots.py
284
285
  tests/test_return_contract.py
285
286
  tests/test_roundtrip_fidelity.py
287
+ tests/test_roundtrip_fidelity_corpus.py
286
288
  tests/test_row_heights.py
287
289
  tests/test_run_choice_atom_authoring.py
288
290
  tests/test_save_pipeline.py
@@ -312,6 +314,7 @@ tests/test_text_extractor_run_choice_atoms.py
312
314
  tests/test_title_mark.py
313
315
  tests/test_toc_author.py
314
316
  tests/test_toc_fidelity.py
317
+ tests/test_tracked_overlap_render_smoke.py
315
318
  tests/test_validation_severity.py
316
319
  tests/test_validator_comment_nodes.py
317
320
  tests/test_version_metadata.py
@@ -0,0 +1,100 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """instid 속성명 대소문자 회귀 (#88).
3
+
4
+ OWPML 스키마·실한컴 산출물의 인스턴스 ID 속성명은 소문자 ``instid``다
5
+ (픽스처 전수 재집계 instid 744 / instId 0). 과거 코드가 4곳에서 카멜케이스
6
+ ``instId``를 읽거나 써서: ① 문단 복제 재발급 분기가 사문화(중복 instid 복제),
7
+ ② 각주/미주 저작이 스키마에 없는 속성명을 방출, ③ 리더가 실한컴 파일에서
8
+ 항상 None을 봤다. 수리 계약: 쓰기는 ``instid``, 읽기는 ``instid`` 우선 +
9
+ 과거 자사 산출물(``instId``) 폴백, 복제 재발급은 양쪽 속성명 모두 값 재발급
10
+ (속성명 자체는 보존 — 바이트 보수).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import re
15
+
16
+ from hwpx import HwpxDocument
17
+ from hwpx.oxml._document_primitives import _clone_paragraph_element
18
+ from hwpx.oxml.memo import HwpxOxmlNote
19
+ from hwpx.tools.markdown_export import export_markdown
20
+
21
+ HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
22
+
23
+
24
+ def _footnote_elements(document: HwpxDocument):
25
+ for paragraph in document.paragraphs:
26
+ for element in paragraph.element.iter():
27
+ if element.tag == f"{HP}footNote":
28
+ yield element
29
+
30
+
31
+ def test_footnote_authoring_emits_schema_case_instid() -> None:
32
+ document = HwpxDocument.new()
33
+ paragraph = document.add_paragraph("본문")
34
+ paragraph.add_footnote("각주 본문")
35
+
36
+ notes = list(_footnote_elements(document))
37
+ assert notes, "각주가 저작되지 않았다"
38
+ for note in notes:
39
+ assert note.get("instid"), "스키마 정본 속성명 instid가 없다"
40
+ assert note.get("instId") is None, "카멜케이스 instId를 다시 방출했다"
41
+
42
+ data = document.to_bytes()
43
+ assert b'instId="' not in data
44
+
45
+
46
+ def test_clone_reissues_schema_case_instid() -> None:
47
+ document = HwpxDocument.new()
48
+ paragraph = document.add_paragraph("본문")
49
+ paragraph.add_footnote("각주 본문")
50
+ source = paragraph.element
51
+ original = next(iter(_footnote_elements(document))).get("instid")
52
+ assert original
53
+
54
+ cloned = _clone_paragraph_element(source)
55
+ cloned_note = next(
56
+ el for el in cloned.iter() if el.tag == f"{HP}footNote"
57
+ )
58
+ assert cloned_note.get("instid"), "복제본에서 instid가 사라졌다"
59
+ assert cloned_note.get("instid") != original, "복제본 instid가 재발급되지 않았다 (중복 인스턴스 ID)"
60
+
61
+
62
+ def test_clone_reissues_legacy_camelcase_value_but_keeps_name() -> None:
63
+ document = HwpxDocument.new()
64
+ paragraph = document.add_paragraph("본문")
65
+ run = paragraph.element.makeelement(f"{HP}run", {"charPrIDRef": "0"})
66
+ paragraph.element.append(run)
67
+ legacy = run.makeelement(f"{HP}footNote", {"instId": "9999"})
68
+ run.append(legacy)
69
+
70
+ cloned = _clone_paragraph_element(paragraph.element)
71
+ cloned_note = next(el for el in cloned.iter() if el.tag == f"{HP}footNote")
72
+ assert cloned_note.get("instId") not in (None, "9999"), "과거 자사 산출물의 instId도 값 재발급 대상"
73
+ assert cloned_note.get("instid") is None, "속성명은 보존한다 (바이트 보수)"
74
+
75
+
76
+ def test_note_reader_prefers_schema_case_with_legacy_fallback() -> None:
77
+ document = HwpxDocument.new()
78
+ paragraph = document.add_paragraph("본문")
79
+ element = paragraph.element.makeelement(f"{HP}footNote", {"instid": "11"})
80
+ assert HwpxOxmlNote(element, paragraph).inst_id == "11"
81
+
82
+ legacy = paragraph.element.makeelement(f"{HP}footNote", {"instId": "22"})
83
+ assert HwpxOxmlNote(legacy, paragraph).inst_id == "22"
84
+
85
+ both = paragraph.element.makeelement(
86
+ f"{HP}footNote", {"instid": "11", "instId": "22"}
87
+ )
88
+ assert HwpxOxmlNote(both, paragraph).inst_id == "11"
89
+
90
+
91
+ def test_markdown_note_marker_carries_authored_instid() -> None:
92
+ """저작(쓰기)과 export(읽기)가 같은 속성명으로 만나야 마커에 ID가 실린다."""
93
+
94
+ document = HwpxDocument.new()
95
+ paragraph = document.add_paragraph("본문")
96
+ paragraph.add_footnote("각주 본문")
97
+
98
+ markdown = export_markdown(HwpxDocument.open(document.to_bytes()))
99
+ match = re.search(r"\[\^fn(\d+)\]", markdown)
100
+ assert match, f"각주 마커에 instid가 비었다: {markdown!r}"
@@ -125,6 +125,88 @@ def test_seeded_stale_lineseg_is_caught():
125
125
  assert any(f.code == STALE_LINESEG_DETECTED for f in report.errors)
126
126
 
127
127
 
128
+ def _add_measured_lineseg(root, text: str, seg_attrs: list[dict[str, str]]) -> None:
129
+ """Attach a full-attribute cache (textpos/horzsize/textheight) to *text*'s paragraph."""
130
+
131
+ para = _text_paragraph(root, text)
132
+ ns = para.tag.rsplit("}", 1)[0].lstrip("{")
133
+ lsa = etree.SubElement(para, f"{{{ns}}}lineSegArray")
134
+ for attrs in seg_attrs:
135
+ etree.SubElement(lsa, f"{{{ns}}}lineSeg", attrs)
136
+
137
+
138
+ _A4_LINE = {"horzsize": "42520", "textheight": "1000"} # 10pt · 표준 본문 줄 폭
139
+
140
+
141
+ def test_grown_text_with_stale_single_line_cache_is_caught():
142
+ """#85 방향: 텍스트가 캐시 줄 수 너머로 자랐는데 textpos는 전부 유효한 경우.
143
+
144
+ 범위 검사(textpos > len)는 이 방향을 못 본다 — 꼬리 폭 판정이 잡아야 한다.
145
+ 120 한글자 문단에 1줄짜리 캐시: 꼬리 폭 ~120em ≫ 줄 폭 42520×2.
146
+ """
147
+
148
+ text = "가" * 120
149
+ doc = HwpxDocument.new()
150
+ doc.add_paragraph(text)
151
+ data = _inject_section0(
152
+ _bytes(doc),
153
+ lambda root: _add_measured_lineseg(root, text, [{"textpos": "0", **_A4_LINE}]),
154
+ )
155
+ report = lint_layout(data)
156
+ assert not report.ok
157
+ assert any(
158
+ f.code == STALE_LINESEG_DETECTED and "lacks lines" in f.message
159
+ for f in report.errors
160
+ )
161
+
162
+
163
+ def test_valid_multiline_cache_tail_passes():
164
+ """정상 다줄 캐시(한컴 저장 형태): 마지막 줄 이후 꼬리가 한 줄 안 → 침묵."""
165
+
166
+ text = "가" * 120
167
+ doc = HwpxDocument.new()
168
+ doc.add_paragraph(text)
169
+ segs = [
170
+ {"textpos": "0", **_A4_LINE},
171
+ {"textpos": "42", **_A4_LINE},
172
+ {"textpos": "84", **_A4_LINE},
173
+ ]
174
+ data = _inject_section0(
175
+ _bytes(doc), lambda root: _add_measured_lineseg(root, text, segs)
176
+ )
177
+ assert lint_layout(data).ok
178
+
179
+
180
+ def test_tracked_insert_prefix_artifact_replay():
181
+ """#85 실물 재현 쌍: 수리 전 산출물은 FAIL, 현행(수리 후) 산출물은 PASS.
182
+
183
+ 현행 코드로 변경추적 삽입을 하면 편집 문단의 캐시가 제거된다(a22342c) —
184
+ 그 산출물이 PASS 쪽. FAIL 쪽은 같은 편집에 수리 전처럼 옛 1줄 캐시를
185
+ 바이트에 되붙여 재구성한다.
186
+ """
187
+
188
+ base = "시행문 본문 첫 문단"
189
+ doc = HwpxDocument.new()
190
+ paragraph = doc.add_paragraph(base, char_pr_id_ref="0")
191
+ doc.tracking.insert(paragraph, " 추가로 붙는 안내 문구가 길게 이어진다 " + "안내" * 50, date="2026-08-19")
192
+
193
+ fixed = doc.to_bytes() # 수리 후: 캐시 없음
194
+ assert lint_layout(fixed).ok
195
+
196
+ edited_text = paragraph.text
197
+
198
+ def reattach(root): # 수리 전 재구성: 옛 1줄 캐시가 그대로 남았던 상태
199
+ _add_measured_lineseg(root, edited_text, [{"textpos": "0", **_A4_LINE}])
200
+
201
+ broken = _inject_section0(fixed, reattach)
202
+ report = lint_layout(broken)
203
+ assert not report.ok
204
+ assert any(
205
+ f.code == STALE_LINESEG_DETECTED and "lacks lines" in f.message
206
+ for f in report.errors
207
+ )
208
+
209
+
128
210
  # --------------------------------------------------------------------------- #
129
211
  # 2: dirty ↔ lineseg leak (ledger-gated).
130
212
  # --------------------------------------------------------------------------- #
@@ -33,7 +33,7 @@ SBOM_STEP = "Generate release SBOM"
33
33
  PYPI_ACTION = "pypa/gh-action-pypi-publish@"
34
34
  GITHUB_ACTION = "softprops/action-gh-release@"
35
35
  CHECKOUT_ACTION = (
36
- "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
36
+ "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"
37
37
  )
38
38
  EXPECTED_TRIGGER = {
39
39
  "push": {
@@ -97,7 +97,7 @@ mkdir -p release-artifacts
97
97
  EXPECTED_RELEASE_STEPS = (
98
98
  (
99
99
  "Checkout repository",
100
- "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0",
100
+ "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1",
101
101
  False,
102
102
  None,
103
103
  None,
@@ -106,7 +106,7 @@ EXPECTED_RELEASE_STEPS = (
106
106
  ),
107
107
  (
108
108
  "Set up Python",
109
- "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1",
109
+ "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97",
110
110
  False,
111
111
  {"python-version": "3.12"},
112
112
  None,
@@ -135,7 +135,7 @@ EXPECTED_RELEASE_STEPS = (
135
135
  (SBOM_STEP, None, True, None, None, None, None),
136
136
  (
137
137
  "Publish package to PyPI",
138
- "pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247",
138
+ "pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33",
139
139
  False,
140
140
  None,
141
141
  None,
@@ -644,12 +644,12 @@ def test_public_legacy_cap_is_a_required_fail_closed_job() -> None:
644
644
  " needs: legacy-cap\n"
645
645
  " runs-on: ubuntu-latest\n"
646
646
  " steps:\n"
647
- f" - uses: {CHECKOUT_ACTION} # v7.0.0\n",
647
+ f" - uses: {CHECKOUT_ACTION} # v7.0.1\n",
648
648
  " prepublish:\n"
649
649
  " needs: legacy-cap\n"
650
650
  " runs-on: ubuntu-latest\n"
651
651
  " steps:\n"
652
- f" - uses: {CHECKOUT_ACTION} # v7.0.0\n"
652
+ f" - uses: {CHECKOUT_ACTION} # v7.0.1\n"
653
653
  " with:\n"
654
654
  " ref: main\n",
655
655
  1,
@@ -857,8 +857,8 @@ def test_core_release_build_inputs_and_remote_provenance_are_frozen() -> None:
857
857
  (ROOT / "pyproject.toml").read_text(encoding="utf-8")
858
858
  )
859
859
  assert pyproject["build-system"]["requires"] == [
860
- "setuptools==83.0.0",
861
- "wheel==0.47.0",
860
+ "setuptools==84.0.0",
861
+ "wheel==0.48.0",
862
862
  ]
863
863
  assert _workflow_safety_failures(RELEASE.read_text(encoding="utf-8")) == []
864
864
 
@@ -0,0 +1,47 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """왕복 충실도 로컬 절반의 상주 회귀 — 실질 구조 변화 0을 매 스위트가 재검한다.
3
+
4
+ `scripts/roundtrip_fidelity.py`의 분모(실한컴 provenance 증명 파일 전수)를
5
+ 무편집 재직렬화(open→to_bytes)하고, 분류가 무해 범주(byte-identical /
6
+ zip-container-only / cosmetic)에만 속하는지 단언한다. substantive-structural-
7
+ change가 하나라도 나오면 이 테스트가 릴리스 전에 막는다 — 공개 지표
8
+ (docs/corpus-metrics.md 왕복 충실도 절)의 "실질 변화 0"이 주장이 아니라
9
+ 게이트가 되게 하는 장치다. 실한컴 재개봉 판정(외부 관찰자 절반)은 별도
10
+ 배치(automation `scripts/roundtrip_reopen_mac.py`) 소관이다.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+
20
+ _spec = importlib.util.spec_from_file_location(
21
+ "roundtrip_fidelity", ROOT / "scripts" / "roundtrip_fidelity.py"
22
+ )
23
+ assert _spec and _spec.loader
24
+ rf = importlib.util.module_from_spec(_spec)
25
+ sys.modules[_spec.name] = rf # dataclass가 자기 모듈을 sys.modules에서 찾는다
26
+ _spec.loader.exec_module(rf)
27
+
28
+
29
+ def test_corpus_roundtrip_has_no_substantive_change(tmp_path, monkeypatch):
30
+ monkeypatch.setattr(rf, "OUTPUT_DIR", tmp_path / "roundtrip")
31
+
32
+ included, excluded = rf.discover_corpus()
33
+ assert included, "왕복 분모가 비었다 — 코퍼스 루트/자격 심사 확인"
34
+ # 자격 심사 정직성: 알려진 제외 3건(비실한컴 1 + 실한컴 거부 픽스처 2)은
35
+ # 반드시 사유와 함께 제외 목록에 있어야 한다.
36
+ excluded_paths = {e.rel_path for e in excluded}
37
+ assert set(rf.KNOWN_NON_ORACLE_EXCLUSIONS) <= excluded_paths
38
+
39
+ offenders: list[str] = []
40
+ for entry in included:
41
+ result = rf.measure_file(entry)
42
+ if result.overall_category.startswith("substantive"):
43
+ offenders.append(f"{entry.rel_path}: {result.overall_category}")
44
+ assert not offenders, (
45
+ "무편집 왕복에서 실질 구조 변화 발생 — 수리 전 릴리스 금지:\n"
46
+ + "\n".join(offenders)
47
+ )