python-hwpx 2.16.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.16.0/src/python_hwpx.egg-info → python_hwpx-2.17.0}/PKG-INFO +1 -1
  2. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/pyproject.toml +1 -1
  3. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/document.py +119 -2
  4. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/__init__.py +14 -0
  5. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/_document_impl.py +252 -6
  6. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/body.py +154 -5
  7. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/header.py +199 -3
  8. python_hwpx-2.17.0/src/hwpx/tools/redline.py +186 -0
  9. {python_hwpx-2.16.0 → python_hwpx-2.17.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  10. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/SOURCES.txt +3 -0
  11. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_memo_and_style_editing.py +8 -1
  12. python_hwpx-2.17.0/tests/test_redline_authoring.py +155 -0
  13. python_hwpx-2.17.0/tests/test_redline_verify.py +102 -0
  14. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/LICENSE +0 -0
  15. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/NOTICE +0 -0
  16. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/README.md +0 -0
  17. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/setup.cfg +0 -0
  18. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/__init__.py +0 -0
  19. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/authoring.py +0 -0
  20. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/builder/__init__.py +0 -0
  21. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/builder/core.py +0 -0
  22. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/builder/report.py +0 -0
  23. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/__init__.py +0 -0
  24. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/badges.py +0 -0
  25. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  26. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  27. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  28. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  29. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/corpus.py +0 -0
  30. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/report.py +0 -0
  31. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  32. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/conformance/runner.py +0 -0
  33. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  34. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/__init__.py +0 -0
  35. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/_support.py +0 -0
  36. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/composer.py +0 -0
  37. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/harvest.py +0 -0
  38. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/plan.py +0 -0
  39. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profile.py +0 -0
  40. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  41. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  42. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  43. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  44. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  45. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  46. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  47. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  48. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  49. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  50. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  51. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  52. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  53. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  54. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  55. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  56. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  57. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  58. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  59. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  60. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  61. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  62. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  63. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/design/validator.py +0 -0
  64. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/__init__.py +0 -0
  65. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/compose.py +0 -0
  66. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/ir.py +0 -0
  67. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/measure.py +0 -0
  68. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/parser.py +0 -0
  69. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/exam/profile.py +0 -0
  70. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fill.py +0 -0
  71. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/__init__.py +0 -0
  72. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/apply.py +0 -0
  73. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/engine.py +0 -0
  74. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/measure.py +0 -0
  75. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/policy.py +0 -0
  76. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/report.py +0 -0
  77. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/seal.py +0 -0
  78. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/form_fit/wordbox.py +0 -0
  79. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/layout/__init__.py +0 -0
  80. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/layout/lint.py +0 -0
  81. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/layout/report.py +0 -0
  82. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/opc/package.py +0 -0
  83. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/opc/relationships.py +0 -0
  84. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/opc/security.py +0 -0
  85. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/opc/xml_utils.py +0 -0
  86. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  87. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/common.py +0 -0
  88. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/document.py +0 -0
  89. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/header_part.py +0 -0
  90. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/memo.py +0 -0
  91. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/namespaces.py +0 -0
  92. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/numbering.py +0 -0
  93. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/objects.py +0 -0
  94. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/paragraph.py +0 -0
  95. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/parser.py +0 -0
  96. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/run.py +0 -0
  97. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/schema.py +0 -0
  98. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/section.py +0 -0
  99. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/simple_parts.py +0 -0
  100. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/table.py +0 -0
  101. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/oxml/utils.py +0 -0
  102. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/package.py +0 -0
  103. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/patch.py +0 -0
  104. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/presets/__init__.py +0 -0
  105. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/presets/proposal.py +0 -0
  106. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/py.typed +0 -0
  107. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/quality/__init__.py +0 -0
  108. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/quality/ledger.py +0 -0
  109. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/quality/policy.py +0 -0
  110. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/quality/report.py +0 -0
  111. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/quality/save_pipeline.py +0 -0
  112. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/template_formfit.py +0 -0
  113. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/templates.py +0 -0
  114. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/__init__.py +0 -0
  115. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  116. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  117. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/advanced_generators.py +0 -0
  118. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/archive_cli.py +0 -0
  119. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/doc_diff.py +0 -0
  120. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/exporter.py +0 -0
  121. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  122. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  123. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  124. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  125. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  126. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  127. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/generic_inventory.py +0 -0
  128. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/id_integrity.py +0 -0
  129. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/idempotence.py +0 -0
  130. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/ir_equality.py +0 -0
  131. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/layout_preview.py +0 -0
  132. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/mail_merge.py +0 -0
  133. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/markdown_export.py +0 -0
  134. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/object_finder.py +0 -0
  135. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/official_lint.py +0 -0
  136. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/package_reconcile.py +0 -0
  137. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/package_validator.py +0 -0
  138. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/page_guard.py +0 -0
  139. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/recover.py +0 -0
  140. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/repair.py +0 -0
  141. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/report_parser.py +0 -0
  142. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/report_utils.py +0 -0
  143. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  144. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/style_profile.py +0 -0
  145. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_cleanup.py +0 -0
  146. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_compute.py +0 -0
  147. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/table_navigation.py +0 -0
  148. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/template_analyzer.py +0 -0
  149. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  150. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/text_extractor.py +0 -0
  151. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/tools/validator.py +0 -0
  152. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/__init__.py +0 -0
  153. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  154. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
  155. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/detectors.py +0 -0
  156. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/diff.py +0 -0
  157. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/masks.py +0 -0
  158. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/oracle.py +0 -0
  159. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/hwpx/visual/report.py +0 -0
  160. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  161. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  162. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  163. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  164. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_advanced_generators.py +0 -0
  165. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_authoring_profile_routing.py +0 -0
  166. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  167. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_authoring_render_check.py +0 -0
  168. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_builder_core.py +0 -0
  169. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_builder_plan_v2.py +0 -0
  170. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_builder_vertical_slice.py +0 -0
  171. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_comment_node_robustness.py +0 -0
  172. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_conformance.py +0 -0
  173. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_coverage_promotion.py +0 -0
  174. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_coverage_targets.py +0 -0
  175. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_design_builder.py +0 -0
  176. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_deviations_registry.py +0 -0
  177. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_doc_diff.py +0 -0
  178. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_document_context_manager.py +0 -0
  179. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_document_formatting.py +0 -0
  180. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_document_plan.py +0 -0
  181. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_document_plan_computed_fields.py +0 -0
  182. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_document_save_api.py +0 -0
  183. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_compose.py +0 -0
  184. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_compose_oracle.py +0 -0
  185. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_fixtures.py +0 -0
  186. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_ir.py +0 -0
  187. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_measure.py +0 -0
  188. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_parser.py +0 -0
  189. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_exam_profile.py +0 -0
  190. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_existing_document_format_editing.py +0 -0
  191. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fields.py +0 -0
  192. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fill_split_run.py +0 -0
  193. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fit.py +0 -0
  194. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fit_integration.py +0 -0
  195. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fit_seal.py +0 -0
  196. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fit_seal_placement.py +0 -0
  197. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_form_fit_wordbox.py +0 -0
  198. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_fuzz_loop.py +0 -0
  199. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_fuzz_regressions.py +0 -0
  200. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_gap_closure_tools.py +0 -0
  201. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_government_report_preset.py +0 -0
  202. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_government_table_profile.py +0 -0
  203. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_hp_tab_support.py +0 -0
  204. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  205. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_id_generator_range.py +0 -0
  206. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_id_integrity.py +0 -0
  207. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_image_object_workflow.py +0 -0
  208. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_inline_models.py +0 -0
  209. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  210. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_integration_roundtrip.py +0 -0
  211. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_kordoc_absorption.py +0 -0
  212. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_layout_lint.py +0 -0
  213. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_layout_preview.py +0 -0
  214. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_mail_merge_fit.py +0 -0
  215. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_mail_merge_table_compute.py +0 -0
  216. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_mail_merge_xlsx.py +0 -0
  217. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_markdown_export.py +0 -0
  218. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_namespace_handling.py +0 -0
  219. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_new_features.py +0 -0
  220. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_official_document_style.py +0 -0
  221. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  222. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_official_lint_tableaware.py +0 -0
  223. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_opc_package.py +0 -0
  224. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_open_safety_corpus.py +0 -0
  225. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_oxml_parsing.py +0 -0
  226. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_packaging_license_metadata.py +0 -0
  227. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_packaging_py_typed.py +0 -0
  228. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_paragraph_keep_together.py +0 -0
  229. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_paragraph_section_management.py +0 -0
  230. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_proposal_preset.py +0 -0
  231. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_question_split_detector.py +0 -0
  232. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_recover_broken_zip.py +0 -0
  233. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_repair_repack.py +0 -0
  234. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_report_parser.py +0 -0
  235. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_report_utils.py +0 -0
  236. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_repr_snapshots.py +0 -0
  237. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_rhwp_t1_gates.py +0 -0
  238. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_rhwp_t2_verification.py +0 -0
  239. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_roundtrip_fidelity.py +0 -0
  240. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_save_pipeline.py +0 -0
  241. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  242. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_section_headers.py +0 -0
  243. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_set_paragraph_format_keep.py +0 -0
  244. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_skeleton_template_ids.py +0 -0
  245. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_split_merged_cell.py +0 -0
  246. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_style_profile.py +0 -0
  247. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_table_cleanup.py +0 -0
  248. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_table_navigation.py +0 -0
  249. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_tables_default_border.py +0 -0
  250. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_template_analyzer_enrichment.py +0 -0
  251. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_template_formfit.py +0 -0
  252. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_text_extractor_annotations.py +0 -0
  253. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_validation_severity.py +0 -0
  254. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_validator_comment_nodes.py +0 -0
  255. {python_hwpx-2.16.0 → python_hwpx-2.17.0}/tests/test_version_metadata.py +0 -0
  256. {python_hwpx-2.16.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.16.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.16.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"
@@ -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__)
@@ -17,6 +17,7 @@ from . import body
17
17
  from .common import GenericElement
18
18
  from .header import (
19
19
  Bullet,
20
+ Header,
20
21
  MemoProperties,
21
22
  MemoShape,
22
23
  ParagraphProperty,
@@ -26,10 +27,14 @@ from .header import (
26
27
  memo_shape_from_attributes,
27
28
  parse_bullets,
28
29
  parse_border_fills,
30
+ parse_header_element,
29
31
  parse_paragraph_properties,
30
32
  parse_styles,
33
+ parse_track_change_config,
31
34
  parse_track_change_authors,
32
35
  parse_track_changes,
36
+ track_change_author_to_xml,
37
+ track_change_to_xml,
33
38
  )
34
39
  from .namespaces import (
35
40
  HWPML_COMPAT_ROOT_NAMESPACES,
@@ -1566,8 +1571,11 @@ class HwpxOxmlRun:
1566
1571
  def apply_model(self, model: "body.Run") -> None:
1567
1572
  new_node = body.serialize_run(model)
1568
1573
  xml_bytes = LET.tostring(new_node)
1569
- replacement = ET.fromstring(xml_bytes)
1570
1574
  parent = self.paragraph.element
1575
+ if isinstance(parent, LET._Element):
1576
+ replacement = LET.fromstring(xml_bytes)
1577
+ else:
1578
+ replacement = ET.fromstring(xml_bytes)
1571
1579
  run_children = list(parent)
1572
1580
  index = run_children.index(self.element)
1573
1581
  parent.remove(self.element)
@@ -3598,8 +3606,11 @@ class HwpxOxmlParagraph:
3598
3606
  def apply_model(self, model: "body.Paragraph") -> None:
3599
3607
  new_node = body.serialize_paragraph(model)
3600
3608
  xml_bytes = LET.tostring(new_node)
3601
- replacement = ET.fromstring(xml_bytes)
3602
3609
  parent = self.section.element
3610
+ if isinstance(parent, LET._Element):
3611
+ replacement = LET.fromstring(xml_bytes)
3612
+ else:
3613
+ replacement = ET.fromstring(xml_bytes)
3603
3614
  paragraph_children = list(parent)
3604
3615
  index = paragraph_children.index(self.element)
3605
3616
  parent.remove(self.element)
@@ -3628,6 +3639,99 @@ class HwpxOxmlParagraph:
3628
3639
  """Return the runs contained in this paragraph."""
3629
3640
  return [HwpxOxmlRun(run, self) for run in self._run_elements()]
3630
3641
 
3642
+ def _last_text_run_for_tracked_insert(
3643
+ self,
3644
+ *,
3645
+ char_pr_id_ref: str | int | None = None,
3646
+ ) -> HwpxOxmlRun:
3647
+ desired_char = None if char_pr_id_ref is None else str(char_pr_id_ref)
3648
+ runs = self.runs
3649
+
3650
+ if desired_char is not None:
3651
+ for run in reversed(runs):
3652
+ if run.char_pr_id_ref != desired_char:
3653
+ continue
3654
+ if run.to_model().text_spans:
3655
+ return run
3656
+ return self.add_run("", char_pr_id_ref=desired_char)
3657
+
3658
+ for run in reversed(runs):
3659
+ if run.to_model().text_spans:
3660
+ return run
3661
+ return self.add_run("", char_pr_id_ref=self.char_pr_id_ref or "0")
3662
+
3663
+ def add_tracked_insert(
3664
+ self,
3665
+ text: str,
3666
+ *,
3667
+ change_id: int,
3668
+ mark_id: int,
3669
+ char_pr_id_ref: str | int | None = None,
3670
+ ) -> None:
3671
+ sanitized = _sanitize_text(text)
3672
+ if not sanitized:
3673
+ raise ValueError("tracked insert text must be non-empty")
3674
+
3675
+ run = self._last_text_run_for_tracked_insert(char_pr_id_ref=char_pr_id_ref)
3676
+ model = run.to_model()
3677
+ body.append_tracked_insert_to_run(
3678
+ model,
3679
+ sanitized,
3680
+ tc_id=change_id,
3681
+ mark_id=mark_id,
3682
+ )
3683
+ run.apply_model(model)
3684
+
3685
+ def add_tracked_delete(
3686
+ self,
3687
+ *,
3688
+ change_id: int,
3689
+ first_mark_id: int,
3690
+ match: str | None = None,
3691
+ ) -> int:
3692
+ if match == "":
3693
+ raise ValueError("match must be a non-empty string")
3694
+
3695
+ next_mark_id = first_mark_id
3696
+ if match is not None:
3697
+ for run in self.runs:
3698
+ model = run.to_model()
3699
+ if match not in "".join(span.text for span in model.text_spans):
3700
+ continue
3701
+ for span in model.text_spans:
3702
+ if body.wrap_tracked_delete_in_span(
3703
+ span,
3704
+ tc_id=change_id,
3705
+ mark_id=next_mark_id,
3706
+ match=match,
3707
+ ):
3708
+ run.apply_model(model)
3709
+ return next_mark_id + 1
3710
+ raise ValueError("match crosses inline markup and cannot be wrapped safely")
3711
+ raise ValueError("match text was not found in the paragraph")
3712
+
3713
+ modified = False
3714
+ for run in self.runs:
3715
+ model = run.to_model()
3716
+ changed = False
3717
+ for span in model.text_spans:
3718
+ if not span.text:
3719
+ continue
3720
+ body.wrap_tracked_delete_in_span(
3721
+ span,
3722
+ tc_id=change_id,
3723
+ mark_id=next_mark_id,
3724
+ )
3725
+ next_mark_id += 1
3726
+ changed = True
3727
+ if changed:
3728
+ run.apply_model(model)
3729
+ modified = True
3730
+
3731
+ if not modified:
3732
+ raise ValueError("paragraph contains no text to delete")
3733
+ return next_mark_id
3734
+
3631
3735
  @property
3632
3736
  def text(self) -> str:
3633
3737
  """Return the concatenated textual content of this paragraph."""
@@ -4710,6 +4814,15 @@ class HwpxOxmlHeader:
4710
4814
  def attach_document(self, document: "HwpxOxmlDocument") -> None:
4711
4815
  self._document = document
4712
4816
 
4817
+ def to_model(self) -> Header:
4818
+ return parse_header_element(self._convert_to_lxml(self._element))
4819
+
4820
+ @staticmethod
4821
+ def _coerce_serialized_child(parent: ET.Element, child: LET._Element) -> ET.Element:
4822
+ if isinstance(parent, LET._Element):
4823
+ return child
4824
+ return ET.fromstring(LET.tostring(child, encoding="utf-8"))
4825
+
4713
4826
  def _begin_num_element(self, create: bool = False) -> ET.Element | None:
4714
4827
  element = self._element.find(f"{_HH}beginNum")
4715
4828
  if element is None and create:
@@ -5383,18 +5496,116 @@ class HwpxOxmlHeader:
5383
5496
  return None
5384
5497
  return ref_list.find(f"{_HH}styles")
5385
5498
 
5386
- def _track_changes_element(self) -> ET.Element | None:
5387
- ref_list = self._ref_list_element()
5499
+ def _track_changes_element(self, create: bool = False) -> ET.Element | None:
5500
+ ref_list = self._ref_list_element(create=create)
5388
5501
  if ref_list is None:
5389
5502
  return None
5390
5503
  return ref_list.find(f"{_HH}trackChanges")
5391
5504
 
5392
- def _track_change_authors_element(self) -> ET.Element | None:
5393
- ref_list = self._ref_list_element()
5505
+ def _track_changes_element_or_create(self) -> ET.Element:
5506
+ ref_list = self._ref_list_element(create=True)
5507
+ if ref_list is None: # pragma: no cover - defensive branch
5508
+ raise RuntimeError("failed to create <refList> element")
5509
+ element = ref_list.find(f"{_HH}trackChanges")
5510
+ if element is None:
5511
+ element = ref_list.makeelement(f"{_HH}trackChanges", {"itemCnt": "0"})
5512
+ ref_list.append(element)
5513
+ self.mark_dirty()
5514
+ return element
5515
+
5516
+ def _track_change_authors_element(self, create: bool = False) -> ET.Element | None:
5517
+ ref_list = self._ref_list_element(create=create)
5394
5518
  if ref_list is None:
5395
5519
  return None
5396
5520
  return ref_list.find(f"{_HH}trackChangeAuthors")
5397
5521
 
5522
+ def _track_change_authors_element_or_create(self) -> ET.Element:
5523
+ ref_list = self._ref_list_element(create=True)
5524
+ if ref_list is None: # pragma: no cover - defensive branch
5525
+ raise RuntimeError("failed to create <refList> element")
5526
+ element = ref_list.find(f"{_HH}trackChangeAuthors")
5527
+ if element is None:
5528
+ element = ref_list.makeelement(f"{_HH}trackChangeAuthors", {"itemCnt": "0"})
5529
+ ref_list.append(element)
5530
+ self.mark_dirty()
5531
+ return element
5532
+
5533
+ def _track_change_config_element(self, create: bool = False) -> ET.Element | None:
5534
+ for child in self._element:
5535
+ if tag_local_name(child.tag) in {"trackchageConfig", "trackchangeConfig"}:
5536
+ return child
5537
+ if not create:
5538
+ return None
5539
+ element = self._element.makeelement(f"{_HH}trackchageConfig", {"flags": "0"})
5540
+ self._element.append(element)
5541
+ self.mark_dirty()
5542
+ return element
5543
+
5544
+ @property
5545
+ def track_change_config(self):
5546
+ element = self._track_change_config_element()
5547
+ if element is None:
5548
+ return None
5549
+ return parse_track_change_config(self._convert_to_lxml(element))
5550
+
5551
+ def add_track_change(
5552
+ self,
5553
+ change_type: str,
5554
+ *,
5555
+ author_name: str = "AI Agent",
5556
+ date: str | None = None,
5557
+ ) -> int:
5558
+ model = self.to_model()
5559
+ change_id = model.add_track_change(
5560
+ change_type,
5561
+ author_name=author_name,
5562
+ date=date,
5563
+ )
5564
+ if model.ref_list is None or model.ref_list.track_changes is None:
5565
+ raise RuntimeError("failed to create tracked-change metadata")
5566
+
5567
+ change = next(
5568
+ candidate
5569
+ for candidate in model.ref_list.track_changes.changes
5570
+ if candidate.id == change_id
5571
+ )
5572
+ changes_element = self._track_changes_element_or_create()
5573
+ changes_element.append(
5574
+ self._coerce_serialized_child(changes_element, track_change_to_xml(change))
5575
+ )
5576
+ changes_element.set("itemCnt", str(model.ref_list.track_changes.item_cnt or 0))
5577
+
5578
+ authors = model.ref_list.track_change_authors
5579
+ if authors is not None and change.author_id is not None:
5580
+ author = authors.author_by_id(change.author_id)
5581
+ if author is not None:
5582
+ authors_element = self._track_change_authors_element_or_create()
5583
+ existing_ids = {
5584
+ child.get("id")
5585
+ for child in authors_element.findall(f"{_HH}trackChangeAuthor")
5586
+ }
5587
+ author_ids = {str(author.id)} if author.id is not None else set()
5588
+ if author.raw_id is not None:
5589
+ author_ids.add(author.raw_id)
5590
+ if not existing_ids.intersection(author_ids):
5591
+ authors_element.append(
5592
+ self._coerce_serialized_child(
5593
+ authors_element,
5594
+ track_change_author_to_xml(author),
5595
+ )
5596
+ )
5597
+ authors_element.set("itemCnt", str(authors.item_cnt or 0))
5598
+
5599
+ config_element = self._track_change_config_element(create=True)
5600
+ if config_element is None: # pragma: no cover - defensive branch
5601
+ raise RuntimeError("failed to create <trackchageConfig> element")
5602
+ flags = 1
5603
+ if model.track_change_config is not None and model.track_change_config.flags is not None:
5604
+ flags = model.track_change_config.flags
5605
+ config_element.set("flags", str(flags | 1))
5606
+ self.mark_dirty()
5607
+ return change_id
5608
+
5398
5609
  def find_basic_border_fill_id(self) -> str | None:
5399
5610
  element = self._border_fills_element()
5400
5611
  if element is None:
@@ -6341,6 +6552,41 @@ class HwpxOxmlDocument:
6341
6552
  ) -> TrackChangeAuthor | None:
6342
6553
  return HwpxOxmlHeader._lookup_by_id(self.track_change_authors, author_id_ref)
6343
6554
 
6555
+ def add_track_change(
6556
+ self,
6557
+ change_type: str,
6558
+ *,
6559
+ author_name: str = "AI Agent",
6560
+ date: str | None = None,
6561
+ ) -> int:
6562
+ if not self._headers:
6563
+ raise ValueError("document does not contain any headers")
6564
+ return self._headers[0].add_track_change(
6565
+ change_type,
6566
+ author_name=author_name,
6567
+ date=date,
6568
+ )
6569
+
6570
+ def next_track_change_mark_id(self) -> int:
6571
+ max_id = 0
6572
+ for section in self._sections:
6573
+ for element in section.element.iter():
6574
+ if tag_local_name(element.tag) not in {
6575
+ "insertBegin",
6576
+ "insertEnd",
6577
+ "deleteBegin",
6578
+ "deleteEnd",
6579
+ }:
6580
+ continue
6581
+ raw_id = element.get("Id")
6582
+ if raw_id is None:
6583
+ continue
6584
+ try:
6585
+ max_id = max(max_id, int(raw_id))
6586
+ except ValueError:
6587
+ continue
6588
+ return max_id + 1
6589
+
6344
6590
  @property
6345
6591
  def paragraphs(self) -> list[HwpxOxmlParagraph]:
6346
6592
  paragraphs: list[HwpxOxmlParagraph] = []