python-hwpx 2.15.0__tar.gz → 2.17.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 (256) hide show
  1. {python_hwpx-2.15.0/src/python_hwpx.egg-info → python_hwpx-2.17.0}/PKG-INFO +1 -1
  2. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/pyproject.toml +1 -1
  3. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/authoring.py +200 -2
  4. python_hwpx-2.17.0/src/hwpx/design/profiles/home_notice/fragments/body.xml +1 -0
  5. python_hwpx-2.17.0/src/hwpx/design/profiles/home_notice/fragments/heading.xml +1 -0
  6. python_hwpx-2.17.0/src/hwpx/design/profiles/home_notice/fragments/title.xml +1 -0
  7. python_hwpx-2.17.0/src/hwpx/design/profiles/home_notice/profile.json +24 -0
  8. python_hwpx-2.17.0/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  9. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/document.py +119 -2
  10. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/__init__.py +14 -0
  11. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/_document_impl.py +252 -6
  12. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/body.py +154 -5
  13. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/header.py +199 -3
  14. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/official_lint.py +111 -6
  15. python_hwpx-2.17.0/src/hwpx/tools/redline.py +186 -0
  16. {python_hwpx-2.15.0 → python_hwpx-2.17.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  17. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/SOURCES.txt +13 -0
  18. python_hwpx-2.17.0/tests/test_authoring_profile_routing.py +148 -0
  19. python_hwpx-2.17.0/tests/test_authoring_profile_routing_oracle.py +84 -0
  20. python_hwpx-2.17.0/tests/test_authoring_render_check.py +56 -0
  21. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_memo_and_style_editing.py +8 -1
  22. python_hwpx-2.17.0/tests/test_official_lint_gongmun_gate.py +61 -0
  23. python_hwpx-2.17.0/tests/test_official_lint_tableaware.py +21 -0
  24. python_hwpx-2.17.0/tests/test_redline_authoring.py +155 -0
  25. python_hwpx-2.17.0/tests/test_redline_verify.py +102 -0
  26. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/LICENSE +0 -0
  27. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/NOTICE +0 -0
  28. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/README.md +0 -0
  29. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/setup.cfg +0 -0
  30. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/__init__.py +0 -0
  31. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/builder/__init__.py +0 -0
  32. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/builder/core.py +0 -0
  33. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/builder/report.py +0 -0
  34. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/__init__.py +0 -0
  35. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/badges.py +0 -0
  36. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  37. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  38. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  39. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  40. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus.py +0 -0
  41. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/report.py +0 -0
  42. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  43. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/conformance/runner.py +0 -0
  44. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  45. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/__init__.py +0 -0
  46. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/_support.py +0 -0
  47. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/composer.py +0 -0
  48. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/harvest.py +0 -0
  49. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/plan.py +0 -0
  50. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profile.py +0 -0
  51. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  52. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  53. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  54. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  55. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  56. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  57. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  58. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  59. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  60. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  61. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  62. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  63. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  64. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  65. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  66. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  67. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  68. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  69. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/design/validator.py +0 -0
  70. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/__init__.py +0 -0
  71. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/compose.py +0 -0
  72. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/ir.py +0 -0
  73. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/measure.py +0 -0
  74. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/parser.py +0 -0
  75. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/exam/profile.py +0 -0
  76. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fill.py +0 -0
  77. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/__init__.py +0 -0
  78. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/apply.py +0 -0
  79. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/engine.py +0 -0
  80. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/measure.py +0 -0
  81. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/policy.py +0 -0
  82. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/report.py +0 -0
  83. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/seal.py +0 -0
  84. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/wordbox.py +0 -0
  85. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/layout/__init__.py +0 -0
  86. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/layout/lint.py +0 -0
  87. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/layout/report.py +0 -0
  88. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/opc/package.py +0 -0
  89. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/opc/relationships.py +0 -0
  90. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/opc/security.py +0 -0
  91. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/opc/xml_utils.py +0 -0
  92. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  93. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/common.py +0 -0
  94. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/document.py +0 -0
  95. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/header_part.py +0 -0
  96. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/memo.py +0 -0
  97. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/namespaces.py +0 -0
  98. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/numbering.py +0 -0
  99. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/objects.py +0 -0
  100. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/paragraph.py +0 -0
  101. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/parser.py +0 -0
  102. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/run.py +0 -0
  103. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/schema.py +0 -0
  104. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/section.py +0 -0
  105. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/simple_parts.py +0 -0
  106. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/table.py +0 -0
  107. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/oxml/utils.py +0 -0
  108. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/package.py +0 -0
  109. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/patch.py +0 -0
  110. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/presets/__init__.py +0 -0
  111. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/presets/proposal.py +0 -0
  112. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/py.typed +0 -0
  113. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/quality/__init__.py +0 -0
  114. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/quality/ledger.py +0 -0
  115. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/quality/policy.py +0 -0
  116. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/quality/report.py +0 -0
  117. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/quality/save_pipeline.py +0 -0
  118. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/template_formfit.py +0 -0
  119. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/templates.py +0 -0
  120. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/__init__.py +0 -0
  121. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  122. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  123. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/advanced_generators.py +0 -0
  124. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/archive_cli.py +0 -0
  125. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/doc_diff.py +0 -0
  126. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/exporter.py +0 -0
  127. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  128. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  129. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  130. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  131. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  132. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  133. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/generic_inventory.py +0 -0
  134. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/id_integrity.py +0 -0
  135. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/idempotence.py +0 -0
  136. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/ir_equality.py +0 -0
  137. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/layout_preview.py +0 -0
  138. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/mail_merge.py +0 -0
  139. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/markdown_export.py +0 -0
  140. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/object_finder.py +0 -0
  141. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/package_reconcile.py +0 -0
  142. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/package_validator.py +0 -0
  143. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/page_guard.py +0 -0
  144. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/recover.py +0 -0
  145. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/repair.py +0 -0
  146. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/report_parser.py +0 -0
  147. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/report_utils.py +0 -0
  148. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  149. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/style_profile.py +0 -0
  150. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_cleanup.py +0 -0
  151. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_compute.py +0 -0
  152. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_navigation.py +0 -0
  153. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/template_analyzer.py +0 -0
  154. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  155. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/text_extractor.py +0 -0
  156. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/tools/validator.py +0 -0
  157. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/__init__.py +0 -0
  158. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  159. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  160. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/detectors.py +0 -0
  161. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/diff.py +0 -0
  162. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/masks.py +0 -0
  163. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/oracle.py +0 -0
  164. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/hwpx/visual/report.py +0 -0
  165. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  166. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  167. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  168. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  169. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_advanced_generators.py +0 -0
  170. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_builder_core.py +0 -0
  171. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_builder_plan_v2.py +0 -0
  172. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_builder_vertical_slice.py +0 -0
  173. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_comment_node_robustness.py +0 -0
  174. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_conformance.py +0 -0
  175. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_coverage_promotion.py +0 -0
  176. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_coverage_targets.py +0 -0
  177. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_design_builder.py +0 -0
  178. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_deviations_registry.py +0 -0
  179. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_doc_diff.py +0 -0
  180. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_document_context_manager.py +0 -0
  181. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_document_formatting.py +0 -0
  182. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_document_plan.py +0 -0
  183. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_document_plan_computed_fields.py +0 -0
  184. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_document_save_api.py +0 -0
  185. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_compose.py +0 -0
  186. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_compose_oracle.py +0 -0
  187. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_fixtures.py +0 -0
  188. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_ir.py +0 -0
  189. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_measure.py +0 -0
  190. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_parser.py +0 -0
  191. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_exam_profile.py +0 -0
  192. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_existing_document_format_editing.py +0 -0
  193. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fields.py +0 -0
  194. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fill_split_run.py +0 -0
  195. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fit.py +0 -0
  196. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fit_integration.py +0 -0
  197. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fit_seal.py +0 -0
  198. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fit_seal_placement.py +0 -0
  199. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_form_fit_wordbox.py +0 -0
  200. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_fuzz_loop.py +0 -0
  201. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_fuzz_regressions.py +0 -0
  202. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_gap_closure_tools.py +0 -0
  203. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_government_report_preset.py +0 -0
  204. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_government_table_profile.py +0 -0
  205. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_hp_tab_support.py +0 -0
  206. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  207. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_id_generator_range.py +0 -0
  208. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_id_integrity.py +0 -0
  209. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_image_object_workflow.py +0 -0
  210. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_inline_models.py +0 -0
  211. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  212. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_integration_roundtrip.py +0 -0
  213. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_kordoc_absorption.py +0 -0
  214. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_layout_lint.py +0 -0
  215. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_layout_preview.py +0 -0
  216. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_mail_merge_fit.py +0 -0
  217. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_mail_merge_table_compute.py +0 -0
  218. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_mail_merge_xlsx.py +0 -0
  219. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_markdown_export.py +0 -0
  220. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_namespace_handling.py +0 -0
  221. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_new_features.py +0 -0
  222. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_official_document_style.py +0 -0
  223. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_opc_package.py +0 -0
  224. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_open_safety_corpus.py +0 -0
  225. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_oxml_parsing.py +0 -0
  226. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_packaging_license_metadata.py +0 -0
  227. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_packaging_py_typed.py +0 -0
  228. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_paragraph_keep_together.py +0 -0
  229. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_paragraph_section_management.py +0 -0
  230. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_proposal_preset.py +0 -0
  231. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_question_split_detector.py +0 -0
  232. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_recover_broken_zip.py +0 -0
  233. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_repair_repack.py +0 -0
  234. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_report_parser.py +0 -0
  235. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_report_utils.py +0 -0
  236. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_repr_snapshots.py +0 -0
  237. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_rhwp_t1_gates.py +0 -0
  238. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_rhwp_t2_verification.py +0 -0
  239. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_roundtrip_fidelity.py +0 -0
  240. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_save_pipeline.py +0 -0
  241. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  242. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_section_headers.py +0 -0
  243. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_set_paragraph_format_keep.py +0 -0
  244. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_skeleton_template_ids.py +0 -0
  245. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_split_merged_cell.py +0 -0
  246. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_style_profile.py +0 -0
  247. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_table_cleanup.py +0 -0
  248. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_table_navigation.py +0 -0
  249. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_tables_default_border.py +0 -0
  250. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_template_analyzer_enrichment.py +0 -0
  251. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_template_formfit.py +0 -0
  252. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_text_extractor_annotations.py +0 -0
  253. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_validation_severity.py +0 -0
  254. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_validator_comment_nodes.py +0 -0
  255. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_version_metadata.py +0 -0
  256. {python_hwpx-2.15.0 → python_hwpx-2.17.0}/tests/test_visual_oracle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.15.0
3
+ Version: 2.17.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 = "2.15.0"
7
+ version = "2.17.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -874,6 +874,138 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
874
874
  return issues
875
875
 
876
876
 
877
+ # --- M3 (S-057) document-type -> design profile routing ------------------------
878
+ # Maps a plan's document_type (Korean label or profile id) to a committed
879
+ # hwpx.design profile. When it resolves, create_document_from_plan composes from
880
+ # the harvested, Hancom-opens-clean profile skeleton instead of the from-scratch
881
+ # builder. Unknown types keep the legacy from-scratch path (regression-safe).
882
+ _DOCTYPE_TO_PROFILE = {
883
+ "공문": "official_notice",
884
+ "공문서": "official_notice",
885
+ "official_notice": "official_notice",
886
+ "보고서": "report",
887
+ "report": "report",
888
+ "government_report": "report",
889
+ "가정통신문": "home_notice",
890
+ "home_notice": "home_notice",
891
+ }
892
+ _DOCTYPE_METADATA_KEYS = (
893
+ "document_type",
894
+ "문서 유형",
895
+ "문서유형",
896
+ "문서 종류",
897
+ "문서종류",
898
+ "documentType",
899
+ )
900
+ # 결문 (closing block) fields in their canonical render order.
901
+ _GYEOLMUN_FIELDS = (
902
+ ("issuer", "발신명의"),
903
+ ("productionNumber", "생산등록번호"),
904
+ ("enforcementDate", "시행일"),
905
+ ("disclosure", "공개구분"),
906
+ )
907
+
908
+
909
+ def _plan_document_type(plan: Any) -> str:
910
+ """Read the plan's document type from metadata (preferred) or top level."""
911
+
912
+ if not isinstance(plan, Mapping):
913
+ return ""
914
+ metadata = plan.get("metadata")
915
+ metadata = metadata if isinstance(metadata, Mapping) else {}
916
+ for key in _DOCTYPE_METADATA_KEYS:
917
+ value = metadata.get(key) or plan.get(key)
918
+ if value:
919
+ return str(value).strip()
920
+ return ""
921
+
922
+
923
+ def _resolve_design_profile(plan: Any) -> str | None:
924
+ """Return a committed design profile id for the plan's document_type, or None."""
925
+
926
+ raw = _plan_document_type(plan)
927
+ if not raw:
928
+ return None
929
+ from hwpx import design as _design
930
+
931
+ profile_id = _DOCTYPE_TO_PROFILE.get(raw)
932
+ if profile_id and profile_id in _design.available_profiles():
933
+ return profile_id
934
+ return None
935
+
936
+
937
+ def _bridge_to_design_plan(plan: Mapping[str, Any], profile_id: str):
938
+ """Lower a document_plan mapping onto a :class:`hwpx.design.plan.DocumentPlan`.
939
+
940
+ Heading level 1 -> ``heading`` role, level >= 2 -> ``subheading``; paragraphs
941
+ and bullet items -> ``body``; tables -> an ``info`` table block. 결문 메타
942
+ fields are appended as trailing ``body`` blocks in canonical order (P0 proved
943
+ these survive a Hancom render).
944
+ """
945
+
946
+ from hwpx.design.plan import Block as _Block, DocumentPlan as _DesignPlan
947
+
948
+ blocks: list = []
949
+ for raw in plan.get("blocks") or []:
950
+ if not isinstance(raw, Mapping):
951
+ continue
952
+ block_type = str(raw.get("type") or "paragraph")
953
+ if block_type == "heading":
954
+ level = int(raw.get("level") or 1)
955
+ role = "heading" if level <= 1 else "subheading"
956
+ blocks.append(_Block(type="paragraph", role=role, text=str(raw.get("text") or "")))
957
+ elif block_type == "paragraph":
958
+ blocks.append(_Block(type="paragraph", role="body", text=str(raw.get("text") or "")))
959
+ elif block_type == "bullets":
960
+ for item in raw.get("items") or []:
961
+ blocks.append(_Block(type="paragraph", role="body", text=str(item)))
962
+ elif block_type == "table":
963
+ raw_cols = list(raw.get("columns") or raw.get("header") or [])
964
+ if raw_cols and isinstance(raw_cols[0], Mapping):
965
+ # document_plan schema: columns=[{key,label}], rows=[{key: value}]
966
+ keys = [str(c.get("key") or c.get("label") or "") for c in raw_cols]
967
+ columns = [str(c.get("label") or c.get("key") or "") for c in raw_cols]
968
+ rows = []
969
+ for row in raw.get("rows") or []:
970
+ if isinstance(row, Mapping):
971
+ rows.append([str(row.get(k, "")) for k in keys])
972
+ elif isinstance(row, (list, tuple)):
973
+ rows.append([str(c) for c in row])
974
+ else:
975
+ columns = [str(c) for c in raw_cols]
976
+ rows = [[str(c) for c in row] for row in (raw.get("rows") or [])]
977
+ blocks.append(_Block(type="table", role="info", columns=columns, rows=rows))
978
+ # page_break / memo: no design role -> skipped
979
+ gyeolmun = plan.get("gyeolmun")
980
+ if isinstance(gyeolmun, Mapping):
981
+ for key, label in _GYEOLMUN_FIELDS:
982
+ value = gyeolmun.get(key)
983
+ if value:
984
+ blocks.append(_Block(type="paragraph", role="body", text=f"{label} {value}"))
985
+ return _DesignPlan(profile=profile_id, title=str(plan.get("title") or ""), blocks=blocks)
986
+
987
+
988
+ def _korean_proofing_status(plan: Any, normalized_plan: "DocumentPlan | None") -> str:
989
+ """Honest 맞춤법/공공언어 status (Constitution V/IX) — never asserts 'passed'.
990
+
991
+ No free offline Korean spell/spacing oracle exists, so the default is
992
+ ``unverified``. If the plan signals an LLM self-proof pass it is labelled
993
+ ``llm_proofed_not_oracle_verified`` — proofed, but NOT oracle-verified.
994
+ """
995
+
996
+ metadata: Mapping[str, Any] = {}
997
+ if isinstance(plan, Mapping) and isinstance(plan.get("metadata"), Mapping):
998
+ metadata = plan["metadata"]
999
+ elif normalized_plan is not None:
1000
+ metadata = normalized_plan.metadata
1001
+ signal = str(
1002
+ metadata.get("korean_proofing") or metadata.get("korean_proofing_status") or ""
1003
+ ).strip().lower()
1004
+ if signal in {"llm", "llm_proofed", "llm-proofed", "llm_proofed_not_oracle_verified"}:
1005
+ return "llm_proofed_not_oracle_verified"
1006
+ return "unverified"
1007
+
1008
+
877
1009
  def create_document_from_plan(
878
1010
  plan: Mapping[str, Any] | DocumentPlan,
879
1011
  *,
@@ -881,6 +1013,19 @@ def create_document_from_plan(
881
1013
  ) -> HwpxDocument:
882
1014
  """Create a formatted HWPX document from a declarative document plan."""
883
1015
 
1016
+ if isinstance(plan, Mapping):
1017
+ profile_id = _resolve_design_profile(plan)
1018
+ if profile_id is not None:
1019
+ from hwpx import design as _design
1020
+
1021
+ design_plan = _bridge_to_design_plan(plan, profile_id)
1022
+ data, result = _design.compose_bytes(design_plan, production=True)
1023
+ if not result.ok:
1024
+ raise ValueError(
1025
+ f"profile compose failed for {profile_id!r}: {result.errors}"
1026
+ )
1027
+ return HwpxDocument.open(data)
1028
+
884
1029
  normalized = normalize_document_plan(plan)
885
1030
  if normalized.builder_document is not None:
886
1031
  return normalized.builder_document.lower()
@@ -952,8 +1097,15 @@ def inspect_document_authoring_quality(
952
1097
  *,
953
1098
  plan: Mapping[str, Any] | DocumentPlan | None = None,
954
1099
  quality_profile: str | Mapping[str, Any] | None = None,
1100
+ verify_render: bool = False,
955
1101
  ) -> dict[str, Any]:
956
- """Return deterministic structural quality evidence for generated HWPX."""
1102
+ """Return deterministic structural quality evidence for generated HWPX.
1103
+
1104
+ When *verify_render* is true AND a Mac Hancom oracle is reachable, the
1105
+ document is rendered and ``render_checked``/``visual_complete`` become real
1106
+ receipts. Otherwise ``render_checked`` is ``False`` and ``visual_complete``
1107
+ is ``"unverified"`` — never a silent true (Constitution V).
1108
+ """
957
1109
 
958
1110
  normalized_plan: DocumentPlan | None = None
959
1111
  plan_validation: dict[str, Any] | None = None
@@ -981,6 +1133,32 @@ def inspect_document_authoring_quality(
981
1133
  package_report = validate_package(path if path is not None else package_payload)
982
1134
  document_report = document.validate()
983
1135
  reopened = _can_reopen(path, package_payload)
1136
+ render_checked = False
1137
+ visual_complete: Any = "unverified"
1138
+ if verify_render:
1139
+ from hwpx.visual import oracle as _oracle
1140
+
1141
+ _mac = _oracle.MacHancomOracle()
1142
+ if _mac.available():
1143
+ import tempfile as _tf
1144
+
1145
+ with _tf.TemporaryDirectory() as _tmp:
1146
+ _hwpx = Path(_tmp) / "render_check.hwpx"
1147
+ _hwpx.write_bytes(package_payload)
1148
+ _pdf = Path(_tmp) / "render_check.pdf"
1149
+ _rendered = _mac.render_pdf(str(_hwpx), str(_pdf))
1150
+ if _rendered and Path(_rendered).exists():
1151
+ try:
1152
+ import fitz as _fitz
1153
+
1154
+ _doc = _fitz.open(_rendered)
1155
+ _has_text = any(pg.get_text().strip() for pg in _doc)
1156
+ _doc.close()
1157
+ render_checked = bool(_has_text)
1158
+ visual_complete = render_checked
1159
+ except Exception:
1160
+ render_checked = False
1161
+ visual_complete = "unverified"
984
1162
  non_empty_texts = [
985
1163
  (paragraph.text or "").strip()
986
1164
  for paragraph in document.paragraphs
@@ -1034,11 +1212,29 @@ def inspect_document_authoring_quality(
1034
1212
  and not profiles["operating_plan"].get("pass", False)
1035
1213
  ):
1036
1214
  gaps.append("operating plan quality failed")
1215
+
1216
+ document_type = ""
1217
+ if isinstance(plan, Mapping):
1218
+ document_type = _plan_document_type(plan)
1219
+ elif normalized_plan is not None:
1220
+ document_type = str(normalized_plan.metadata.get("document_type", "") or "")
1221
+ gongmun_structure: dict[str, Any] | None = None
1222
+ if _DOCTYPE_TO_PROFILE.get(document_type.strip()) == "official_notice":
1223
+ from hwpx.tools.official_lint import (
1224
+ inspect_official_document_style as _gongmun_lint,
1225
+ )
1226
+
1227
+ gongmun_structure = _gongmun_lint(document, document_type="공문")
1228
+ if not gongmun_structure.get("structure_pass", True):
1229
+ gaps.append("공문 structure gate failed")
1230
+ korean_proofing_status = _korean_proofing_status(plan, normalized_plan)
1037
1231
  return {
1038
1232
  "report_version": AUTHORING_REPORT_VERSION,
1039
1233
  "schemaVersion": DOCUMENT_PLAN_SCHEMA_VERSION,
1040
1234
  "plan_validation": plan_validation,
1041
1235
  "pass": not gaps,
1236
+ "korean_proofing_status": korean_proofing_status,
1237
+ "gongmun_structure": gongmun_structure,
1042
1238
  "block_counts": _block_counts(normalized_plan),
1043
1239
  "document": {
1044
1240
  "paragraph_count": len(document.paragraphs),
@@ -1046,6 +1242,8 @@ def inspect_document_authoring_quality(
1046
1242
  "table_count": table_count,
1047
1243
  "page_break_count": page_break_count,
1048
1244
  },
1245
+ "render_checked": render_checked,
1246
+ "visual_complete": visual_complete,
1049
1247
  "validation": {
1050
1248
  "reopened": reopened,
1051
1249
  "validate_package": {
@@ -1064,7 +1262,7 @@ def inspect_document_authoring_quality(
1064
1262
  "style_token_usage": style_usage,
1065
1263
  "recovery": recovery,
1066
1264
  "profiles": profiles,
1067
- "visual_review_required": bool(gates.get("visualReviewRequired", True)),
1265
+ "visual_review_required": bool(gates.get("visualReviewRequired", True)) and not render_checked,
1068
1266
  "gaps": gaps,
1069
1267
  }
1070
1268
  finally:
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="26"><hp:t>{{body}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="1" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="25"><hp:t>{{heading}}</hp:t></hp:run></hp:p>
@@ -0,0 +1 @@
1
+ <hp:p xmlns:hp="http://www.hancom.co.kr/hwpml/2011/paragraph" id="2147483648" paraPrIDRef="25" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0"><hp:run charPrIDRef="17"><hp:t>{{title}}</hp:t></hp:run></hp:p>
@@ -0,0 +1,24 @@
1
+ {
2
+ "schemaVersion": "hwpx.design.profile.v1",
3
+ "id": "home_notice",
4
+ "template": "template.hwpx",
5
+ "fragments": {
6
+ "title": "fragments/title.xml",
7
+ "heading": "fragments/heading.xml",
8
+ "body": "fragments/body.xml"
9
+ },
10
+ "page": {
11
+ "width": 59528,
12
+ "height": 84186,
13
+ "orientation": "WIDELY",
14
+ "margins": {
15
+ "left": 4251,
16
+ "right": 4251,
17
+ "top": 1417,
18
+ "bottom": 0
19
+ }
20
+ },
21
+ "char_pr_count": 54,
22
+ "style_coverage_threshold": 0.98,
23
+ "source_basename": "ganghwa_records.hwpx"
24
+ }
@@ -116,6 +116,7 @@ _FORM_FIELD_PARAM_NAMES = {
116
116
  "guide",
117
117
  }
118
118
  _TEXT_ILLEGAL = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]")
119
+ _TRACKED_TEXT_ILLEGAL = re.compile(r"[\x00-\x08\x09\x0b\x0c\x0d\x0e-\x1f\ufffe\uffff]")
119
120
 
120
121
 
121
122
  def _local_name(node_or_tag: Any) -> str:
@@ -131,6 +132,10 @@ def _sanitize_field_text(value: str) -> str:
131
132
  return _TEXT_ILLEGAL.sub("", value)
132
133
 
133
134
 
135
+ def _sanitize_tracked_text(value: str) -> str:
136
+ return _TRACKED_TEXT_ILLEGAL.sub("", value)
137
+
138
+
134
139
  def _field_type_tokens(*values: str | None) -> set[str]:
135
140
  tokens: set[str] = set()
136
141
  for value in values:
@@ -558,6 +563,111 @@ class HwpxDocument:
558
563
 
559
564
  return self._root.track_change_author(author_id_ref)
560
565
 
566
+ def add_track_change(
567
+ self,
568
+ change_type: str,
569
+ *,
570
+ author_name: str = "AI Agent",
571
+ date: str | None = None,
572
+ ) -> int:
573
+ """Add tracked-change header metadata and return the new change id."""
574
+
575
+ return self._root.add_track_change(
576
+ change_type,
577
+ author_name=author_name,
578
+ date=date,
579
+ )
580
+
581
+ @staticmethod
582
+ def _paragraph_has_deletable_text(
583
+ paragraph: HwpxOxmlParagraph,
584
+ match: str | None,
585
+ ) -> bool:
586
+ for run in paragraph.runs:
587
+ model = run.to_model()
588
+ run_text = "".join(span.text for span in model.text_spans)
589
+ if match is None:
590
+ if run_text:
591
+ return True
592
+ elif match in run_text:
593
+ return True
594
+ return False
595
+
596
+ def add_tracked_insert(
597
+ self,
598
+ paragraph: HwpxOxmlParagraph,
599
+ text: str,
600
+ *,
601
+ author: str = "AI Agent",
602
+ date: str | None = None,
603
+ char_pr_id_ref: str | int | None = None,
604
+ ) -> int:
605
+ """Append tracked inserted *text* to *paragraph* and return its change id."""
606
+
607
+ sanitized = _sanitize_tracked_text(text)
608
+ if not sanitized:
609
+ raise ValueError("tracked insert text must be non-empty")
610
+ change_id = self.add_track_change("Insert", author_name=author, date=date)
611
+ mark_id = self._root.next_track_change_mark_id()
612
+ paragraph.add_tracked_insert(
613
+ sanitized,
614
+ change_id=change_id,
615
+ mark_id=mark_id,
616
+ char_pr_id_ref=char_pr_id_ref,
617
+ )
618
+ return change_id
619
+
620
+ def add_tracked_delete(
621
+ self,
622
+ paragraph: HwpxOxmlParagraph,
623
+ *,
624
+ match: str | None = None,
625
+ author: str = "AI Agent",
626
+ date: str | None = None,
627
+ ) -> int:
628
+ """Wrap paragraph text or the first matching substring in delete marks."""
629
+
630
+ if match == "":
631
+ raise ValueError("match must be a non-empty string")
632
+ if not self._paragraph_has_deletable_text(paragraph, match):
633
+ if match is None:
634
+ raise ValueError("paragraph contains no text to delete")
635
+ raise ValueError("match text was not found in the paragraph")
636
+
637
+ change_id = self.add_track_change("Delete", author_name=author, date=date)
638
+ mark_id = self._root.next_track_change_mark_id()
639
+ paragraph.add_tracked_delete(
640
+ change_id=change_id,
641
+ first_mark_id=mark_id,
642
+ match=match,
643
+ )
644
+ return change_id
645
+
646
+ def add_tracked_replace(
647
+ self,
648
+ paragraph: HwpxOxmlParagraph,
649
+ old: str,
650
+ new: str,
651
+ *,
652
+ author: str = "AI Agent",
653
+ date: str | None = None,
654
+ ) -> tuple[int, int]:
655
+ """Represent a replacement as tracked delete of *old* plus tracked insert of *new*."""
656
+
657
+ delete_change_id = self.add_tracked_delete(
658
+ paragraph,
659
+ match=old,
660
+ author=author,
661
+ date=date,
662
+ )
663
+ insert_change_id = self.add_tracked_insert(
664
+ paragraph,
665
+ new,
666
+ author=author,
667
+ date=date,
668
+ )
669
+ return delete_change_id, insert_change_id
670
+
561
671
  @property
562
672
  def memos(self) -> list[HwpxOxmlMemo]:
563
673
  """Return all memo entries declared in every section."""
@@ -659,7 +769,11 @@ class HwpxDocument:
659
769
  _append_element(parameters, f"{_HP}integerParam", {"name": "Number"}).text = str(max(1, number))
660
770
  _append_element(parameters, f"{_HP}stringParam", {"name": "CreateDateTime"}).text = created_value
661
771
  _append_element(parameters, f"{_HP}stringParam", {"name": "Author"}).text = author_value
662
- _append_element(parameters, f"{_HP}stringParam", {"name": "MemoShapeID"}).text = memo_shape_id
772
+ # Hancom's own files use ``MemoShapeIDRef`` (65535 = the built-in default memo
773
+ # shape) — an empty/absent ref leaves the memo box unlinked.
774
+ _append_element(parameters, f"{_HP}stringParam", {"name": "MemoShapeIDRef"}).text = (
775
+ memo_shape_id or "65535"
776
+ )
663
777
 
664
778
  sub_list = _append_element(
665
779
  field_begin,
@@ -684,7 +798,10 @@ class HwpxDocument:
684
798
  },
685
799
  )
686
800
  sub_run = _append_element(sub_para, f"{_HP}run", {"charPrIDRef": char_ref})
687
- _append_element(sub_run, f"{_HP}t").text = memo.id or field_value
801
+ # The MEMO field's subList holds the comment TEXT — this is what Hancom shows
802
+ # in the margin memo box. (Previously this emitted ``memo.id``, so Hancom
803
+ # rendered the numeric id instead of the comment.)
804
+ _append_element(sub_run, f"{_HP}t").text = memo.text or ""
688
805
 
689
806
  run_end = paragraph_element.makeelement(f"{_HP}run", {"charPrIDRef": char_ref})
690
807
  ctrl_end = _append_element(run_end, f"{_HP}ctrl")
@@ -10,10 +10,13 @@ from .body import (
10
10
  Run,
11
11
  Section,
12
12
  TextSpan,
13
+ append_tracked_insert_to_run,
14
+ create_track_change_mark,
13
15
  parse_paragraph_element,
14
16
  parse_run_element,
15
17
  parse_section_element,
16
18
  parse_text_span,
19
+ wrap_tracked_delete_in_span,
17
20
  )
18
21
  from .common import GenericElement, parse_generic_element
19
22
 
@@ -74,7 +77,9 @@ from .header import (
74
77
  TrackChangeAuthorList,
75
78
  TrackChangeConfig,
76
79
  TrackChangeList,
80
+ format_track_change_date,
77
81
  memo_shape_from_attributes,
82
+ normalize_track_change_type,
78
83
  parse_begin_num,
79
84
  parse_bullet,
80
85
  parse_bullet_para_head,
@@ -98,6 +103,8 @@ from .header import (
98
103
  parse_track_change_author,
99
104
  parse_track_change_authors,
100
105
  parse_track_changes,
106
+ track_change_author_to_xml,
107
+ track_change_to_xml,
101
108
  )
102
109
  from .parser import element_to_model, parse_header_xml, parse_section_xml
103
110
  from .schema import load_schema
@@ -173,7 +180,10 @@ __all__ = [
173
180
  "TrackChangeList",
174
181
  "TextSpan",
175
182
  "XmlSource",
183
+ "append_tracked_insert_to_run",
184
+ "create_track_change_mark",
176
185
  "element_to_model",
186
+ "format_track_change_date",
177
187
  "load_schema",
178
188
  "parse_begin_num",
179
189
  "parse_bullet",
@@ -202,9 +212,13 @@ __all__ = [
202
212
  "parse_track_change_author",
203
213
  "parse_track_change_authors",
204
214
  "parse_track_changes",
215
+ "normalize_track_change_type",
205
216
  "parse_section_element",
206
217
  "parse_section_xml",
207
218
  "parse_text_span",
219
+ "track_change_author_to_xml",
220
+ "track_change_to_xml",
221
+ "wrap_tracked_delete_in_span",
208
222
  ]
209
223
 
210
224
  logger = logging.getLogger(__name__)