python-hwpx 2.19.0__tar.gz → 2.20.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 (270) hide show
  1. {python_hwpx-2.19.0/src/python_hwpx.egg-info → python_hwpx-2.20.0}/PKG-INFO +1 -1
  2. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/pyproject.toml +2 -2
  3. python_hwpx-2.20.0/src/hwpx/tools/toc_author.py +291 -0
  4. python_hwpx-2.20.0/src/hwpx/tools/toc_fidelity.py +410 -0
  5. python_hwpx-2.20.0/src/hwpx/visual/_refresh_hwpx_mac.applescript +162 -0
  6. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/_render_hwpx_mac.applescript +5 -1
  7. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/oracle.py +37 -0
  8. {python_hwpx-2.19.0 → python_hwpx-2.20.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  9. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/python_hwpx.egg-info/SOURCES.txt +5 -0
  10. python_hwpx-2.20.0/tests/test_toc_author.py +178 -0
  11. python_hwpx-2.20.0/tests/test_toc_fidelity.py +205 -0
  12. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/LICENSE +0 -0
  13. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/NOTICE +0 -0
  14. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/README.md +0 -0
  15. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/setup.cfg +0 -0
  16. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/__init__.py +0 -0
  17. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/authoring.py +0 -0
  18. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/builder/__init__.py +0 -0
  19. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/builder/core.py +0 -0
  20. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/builder/report.py +0 -0
  21. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/__init__.py +0 -0
  22. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/badges.py +0 -0
  23. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
  24. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
  25. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
  26. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
  27. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/corpus.py +0 -0
  28. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/report.py +0 -0
  29. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
  30. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/conformance/runner.py +0 -0
  31. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  32. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/__init__.py +0 -0
  33. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/_support.py +0 -0
  34. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/composer.py +0 -0
  35. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/harvest.py +0 -0
  36. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/plan.py +0 -0
  37. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profile.py +0 -0
  38. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
  39. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
  40. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
  41. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
  42. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
  43. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
  44. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
  45. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
  46. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
  47. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
  48. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
  49. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
  50. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
  51. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
  52. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
  53. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
  54. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
  55. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
  56. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
  57. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
  58. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
  59. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/profile.json +0 -0
  60. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
  61. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/design/validator.py +0 -0
  62. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/document.py +0 -0
  63. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/__init__.py +0 -0
  64. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/compose.py +0 -0
  65. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/ir.py +0 -0
  66. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/measure.py +0 -0
  67. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/parser.py +0 -0
  68. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/exam/profile.py +0 -0
  69. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fill.py +0 -0
  70. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/__init__.py +0 -0
  71. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/apply.py +0 -0
  72. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/engine.py +0 -0
  73. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/measure.py +0 -0
  74. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/policy.py +0 -0
  75. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/report.py +0 -0
  76. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/seal.py +0 -0
  77. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/form_fit/wordbox.py +0 -0
  78. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/layout/__init__.py +0 -0
  79. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/layout/lint.py +0 -0
  80. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/layout/report.py +0 -0
  81. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/opc/package.py +0 -0
  82. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/opc/relationships.py +0 -0
  83. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/opc/security.py +0 -0
  84. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/opc/xml_utils.py +0 -0
  85. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/__init__.py +0 -0
  86. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/_document_impl.py +0 -0
  87. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/body.py +0 -0
  88. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  89. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/common.py +0 -0
  90. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/document.py +0 -0
  91. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/header.py +0 -0
  92. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/header_part.py +0 -0
  93. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/memo.py +0 -0
  94. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/namespaces.py +0 -0
  95. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/numbering.py +0 -0
  96. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/objects.py +0 -0
  97. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/paragraph.py +0 -0
  98. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/parser.py +0 -0
  99. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/run.py +0 -0
  100. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/schema.py +0 -0
  101. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/section.py +0 -0
  102. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/simple_parts.py +0 -0
  103. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/table.py +0 -0
  104. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/oxml/utils.py +0 -0
  105. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/package.py +0 -0
  106. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/patch.py +0 -0
  107. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/presets/__init__.py +0 -0
  108. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/presets/proposal.py +0 -0
  109. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/py.typed +0 -0
  110. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/quality/__init__.py +0 -0
  111. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/quality/ledger.py +0 -0
  112. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/quality/policy.py +0 -0
  113. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/quality/report.py +0 -0
  114. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/quality/save_pipeline.py +0 -0
  115. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/template_formfit.py +0 -0
  116. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/templates.py +0 -0
  117. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/__init__.py +0 -0
  118. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  119. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  120. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/advanced_generators.py +0 -0
  121. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/archive_cli.py +0 -0
  122. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/doc_diff.py +0 -0
  123. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/exporter.py +0 -0
  124. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
  125. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
  126. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
  127. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/generator.py +0 -0
  128. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
  129. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/fuzz/runner.py +0 -0
  130. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/generic_inventory.py +0 -0
  131. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/id_integrity.py +0 -0
  132. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/idempotence.py +0 -0
  133. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/ir_equality.py +0 -0
  134. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/layout_preview.py +0 -0
  135. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/mail_merge.py +0 -0
  136. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/markdown_export.py +0 -0
  137. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/object_finder.py +0 -0
  138. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/official_lint.py +0 -0
  139. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/package_reconcile.py +0 -0
  140. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/package_validator.py +0 -0
  141. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/page_guard.py +0 -0
  142. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/pii.py +0 -0
  143. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/read_fidelity.py +0 -0
  144. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/recover.py +0 -0
  145. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/redline.py +0 -0
  146. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/repair.py +0 -0
  147. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/report_parser.py +0 -0
  148. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/report_utils.py +0 -0
  149. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  150. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/style_profile.py +0 -0
  151. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/table_cleanup.py +0 -0
  152. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/table_compute.py +0 -0
  153. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/table_navigation.py +0 -0
  154. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/template_analyzer.py +0 -0
  155. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  156. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/text_extractor.py +0 -0
  157. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/tools/validator.py +0 -0
  158. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/__init__.py +0 -0
  159. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
  160. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
  161. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/detectors.py +0 -0
  162. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/diff.py +0 -0
  163. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/masks.py +0 -0
  164. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/hwpx/visual/report.py +0 -0
  165. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  166. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  167. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  168. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  169. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_advanced_generators.py +0 -0
  170. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_authoring_profile_routing.py +0 -0
  171. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
  172. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_authoring_render_check.py +0 -0
  173. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_builder_core.py +0 -0
  174. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_builder_plan_v2.py +0 -0
  175. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_builder_vertical_slice.py +0 -0
  176. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_comment_node_robustness.py +0 -0
  177. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_conformance.py +0 -0
  178. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_corpus_read_fidelity.py +0 -0
  179. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_coverage_promotion.py +0 -0
  180. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_coverage_targets.py +0 -0
  181. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_design_builder.py +0 -0
  182. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_deviations_registry.py +0 -0
  183. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_doc_diff.py +0 -0
  184. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_document_context_manager.py +0 -0
  185. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_document_formatting.py +0 -0
  186. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_document_plan.py +0 -0
  187. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_document_plan_computed_fields.py +0 -0
  188. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_document_save_api.py +0 -0
  189. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_compose.py +0 -0
  190. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_compose_oracle.py +0 -0
  191. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_fixtures.py +0 -0
  192. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_ir.py +0 -0
  193. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_measure.py +0 -0
  194. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_parser.py +0 -0
  195. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_exam_profile.py +0 -0
  196. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_existing_document_format_editing.py +0 -0
  197. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fields.py +0 -0
  198. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fill_split_run.py +0 -0
  199. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fit.py +0 -0
  200. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fit_integration.py +0 -0
  201. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fit_seal.py +0 -0
  202. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fit_seal_placement.py +0 -0
  203. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_form_fit_wordbox.py +0 -0
  204. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_fuzz_loop.py +0 -0
  205. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_fuzz_regressions.py +0 -0
  206. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_gap_closure_tools.py +0 -0
  207. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_government_report_preset.py +0 -0
  208. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_government_table_profile.py +0 -0
  209. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_hp_tab_support.py +0 -0
  210. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  211. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_id_generator_range.py +0 -0
  212. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_id_integrity.py +0 -0
  213. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_image_object_workflow.py +0 -0
  214. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_inline_models.py +0 -0
  215. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  216. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_integration_roundtrip.py +0 -0
  217. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_kordoc_absorption.py +0 -0
  218. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_layout_lint.py +0 -0
  219. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_layout_preview.py +0 -0
  220. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_mail_merge_fit.py +0 -0
  221. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_mail_merge_table_compute.py +0 -0
  222. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_mail_merge_xlsx.py +0 -0
  223. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_markdown_export.py +0 -0
  224. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_memo_and_style_editing.py +0 -0
  225. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_namespace_handling.py +0 -0
  226. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_new_features.py +0 -0
  227. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_official_document_style.py +0 -0
  228. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_official_lint_gongmun_gate.py +0 -0
  229. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_official_lint_tableaware.py +0 -0
  230. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_opc_package.py +0 -0
  231. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_open_safety_corpus.py +0 -0
  232. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_openrate.py +0 -0
  233. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_oxml_parsing.py +0 -0
  234. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_packaging_license_metadata.py +0 -0
  235. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_packaging_py_typed.py +0 -0
  236. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_paragraph_keep_together.py +0 -0
  237. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_paragraph_section_management.py +0 -0
  238. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_pii.py +0 -0
  239. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_pii_structural.py +0 -0
  240. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_pii_wiring.py +0 -0
  241. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_proposal_preset.py +0 -0
  242. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_question_split_detector.py +0 -0
  243. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_read_fidelity.py +0 -0
  244. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_recover_broken_zip.py +0 -0
  245. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_redline_authoring.py +0 -0
  246. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_redline_verify.py +0 -0
  247. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_repair_repack.py +0 -0
  248. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_report_parser.py +0 -0
  249. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_report_utils.py +0 -0
  250. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_repr_snapshots.py +0 -0
  251. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_rhwp_t1_gates.py +0 -0
  252. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_rhwp_t2_verification.py +0 -0
  253. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_roundtrip_fidelity.py +0 -0
  254. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_save_pipeline.py +0 -0
  255. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_save_pipeline_no_bypass.py +0 -0
  256. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_section_headers.py +0 -0
  257. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_set_paragraph_format_keep.py +0 -0
  258. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_skeleton_template_ids.py +0 -0
  259. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_split_merged_cell.py +0 -0
  260. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_style_profile.py +0 -0
  261. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_table_cleanup.py +0 -0
  262. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_table_navigation.py +0 -0
  263. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_tables_default_border.py +0 -0
  264. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_template_analyzer_enrichment.py +0 -0
  265. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_template_formfit.py +0 -0
  266. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_text_extractor_annotations.py +0 -0
  267. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_validation_severity.py +0 -0
  268. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_validator_comment_nodes.py +0 -0
  269. {python_hwpx-2.19.0 → python_hwpx-2.20.0}/tests/test_version_metadata.py +0 -0
  270. {python_hwpx-2.19.0 → python_hwpx-2.20.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.19.0
3
+ Version: 2.20.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.19.0"
7
+ version = "2.20.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -81,7 +81,7 @@ include = ["hwpx*"]
81
81
  "hwpx" = ["py.typed"]
82
82
  "hwpx.tools" = ["_schemas/*.xsd"]
83
83
  "hwpx.data" = ["Skeleton.hwpx"]
84
- "hwpx.visual" = ["_render_hwpx.ps1", "_render_hwpx_mac.applescript", "_hancom_open_rate.ps1"]
84
+ "hwpx.visual" = ["_render_hwpx.ps1", "_render_hwpx_mac.applescript", "_refresh_hwpx_mac.applescript", "_hancom_open_rate.ps1"]
85
85
  "hwpx.design" = [
86
86
  "profiles/*/profile.json",
87
87
  "profiles/*/template.hwpx",
@@ -0,0 +1,291 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Author Hancom-native TOC and page cross-reference fields (M7 / S-062 P2).
3
+
4
+ Emits the exact field contract reverse-engineered from an owner-authored
5
+ Hancom document (``specs/009-native-toc-xrefs/evidence/
6
+ p0-native-toc-xml-contract.md``; gold pair vendored in
7
+ ``tests/fixtures/m7_toc_gold/``):
8
+
9
+ * ``<hp:fieldBegin type="TABLEOFCONTENTS">`` + ``TableOfContents:set`` Command
10
+ DSL wrapping generated entry paragraphs (HYPERLINK fields whose single
11
+ ``<hp:t>`` nests the dot-leader ``hp:tab``; page number in the tab's tail),
12
+ * ``<hp:fieldBegin type="CROSSREF">`` page references whose cached result is
13
+ the plain ``hp:t`` run between fieldBegin and fieldEnd (Hancom recomputes
14
+ these automatically on edit/save — P0 measured),
15
+ * anchors that are plain paragraph ``id`` attributes; TOC-targeted headings
16
+ therefore need document-unique ids (:func:`ensure_paragraph_anchor_id`).
17
+
18
+ Honest semantics (measured): entry page numbers emitted here are ESTIMATES —
19
+ Hancom only recomputes the TOC block on an explicit 차례 새로 고침, which the
20
+ P3 oracle triggers and verifies. CROSSREF caches self-heal on edit/save.
21
+ The parameter spelling ``Fiexde`` replicates Hancom's own output verbatim.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from typing import Any, Sequence
27
+ from uuid import uuid4
28
+
29
+ # live section paragraphs are lxml elements — new nodes must match
30
+ from lxml import etree as ET
31
+
32
+ from hwpx.document import HwpxDocument
33
+
34
+ _HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
35
+ _NON_UNIQUE_PARA_ID = "2147483648"
36
+ _OUTLINE_NAME_RE = re.compile(r"^(?:개요|Outline)\s*(\d+)$")
37
+ #: Measured TOC Command (gold contract). ContentsMake:uint:31 = the bitmask the
38
+ #: Hancom dialog emitted (title+table+figure+equation blocks); other values are
39
+ #: unmeasured, so v1 replicates the captured value verbatim.
40
+ _TOC_COMMAND = (
41
+ "TableOfContents:set:140:ContentsMake:uint:31 ContentsStyles:wstring:0: "
42
+ "ContentsLevel:int:{level} ContentsAutoTabRight:int:0 "
43
+ "ContentsLeader:int:{leader} ContentsHyperlink:bool:{hyperlink} "
44
+ )
45
+
46
+
47
+ def _rand_id() -> str:
48
+ return str(uuid4().int & 0x7FFFFFFF)
49
+
50
+
51
+ def _existing_paragraph_ids(doc: HwpxDocument) -> set[str]:
52
+ ids: set[str] = set()
53
+ for section in doc.oxml.sections:
54
+ for p_el in section.element.iter(f"{_HP}p"):
55
+ pid = p_el.get("id")
56
+ if pid:
57
+ ids.add(pid)
58
+ return ids
59
+
60
+
61
+ def ensure_paragraph_anchor_id(doc: HwpxDocument, paragraph: Any) -> str:
62
+ """Make ``paragraph`` addressable as a field anchor (``?#<id>``).
63
+
64
+ Hancom resolves TOC/CROSSREF targets by the paragraph ``id`` attribute, so
65
+ an anchor id must be unique in the document (generated paragraphs may all
66
+ carry the shared ``2147483648`` constant)."""
67
+ element = paragraph.element
68
+ pid = element.get("id")
69
+ ids = _existing_paragraph_ids(doc)
70
+ duplicated = pid and sum(1 for section in doc.oxml.sections
71
+ for p in section.element.iter(f"{_HP}p")
72
+ if p.get("id") == pid) > 1
73
+ if not pid or pid == _NON_UNIQUE_PARA_ID or duplicated:
74
+ new_id = _rand_id()
75
+ while new_id in ids:
76
+ new_id = _rand_id()
77
+ element.set("id", new_id)
78
+ pid = new_id
79
+ return pid
80
+
81
+
82
+ def outline_heading_paragraphs(doc: HwpxDocument) -> list[tuple[Any, int, str]]:
83
+ """Return ``(paragraph, level, text)`` for 개요/Outline-styled paragraphs."""
84
+ levels: dict[str, int] = {}
85
+ for style_id, style in (doc.styles or {}).items():
86
+ name = getattr(style, "name", "") or ""
87
+ match = _OUTLINE_NAME_RE.match(name.strip())
88
+ if match:
89
+ levels[str(style_id)] = int(match.group(1))
90
+ result: list[tuple[Any, int, str]] = []
91
+ for section in doc.sections:
92
+ for paragraph in section.paragraphs:
93
+ style_ref = paragraph.element.get("styleIDRef")
94
+ if style_ref in levels and (paragraph.text or "").strip():
95
+ result.append((paragraph, levels[style_ref], paragraph.text.strip()))
96
+ return result
97
+
98
+
99
+ # ── low-level field emission ─────────────────────────────────────────
100
+ def _param(parent: ET.Element, kind: str, name: str, value: str, *, preserve: bool = False) -> None:
101
+ el = ET.SubElement(parent, f"{_HP}{kind}", {"name": name})
102
+ if preserve:
103
+ el.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
104
+ el.text = value
105
+
106
+
107
+ def _field_begin(run: ET.Element, *, ftype: str, field_id: str, editable: str, dirty: str) -> ET.Element:
108
+ ctrl = ET.SubElement(run, f"{_HP}ctrl")
109
+ begin = ET.SubElement(
110
+ ctrl,
111
+ f"{_HP}fieldBegin",
112
+ {
113
+ "id": field_id,
114
+ "type": ftype,
115
+ "name": "",
116
+ "editable": editable,
117
+ "dirty": dirty,
118
+ "zorder": "-1",
119
+ "fieldid": _rand_id(),
120
+ "metaTag": "",
121
+ },
122
+ )
123
+ return begin
124
+
125
+
126
+ def _field_end(run: ET.Element, field_id: str) -> None:
127
+ ctrl = ET.SubElement(run, f"{_HP}ctrl")
128
+ ET.SubElement(ctrl, f"{_HP}fieldEnd", {"beginIDRef": field_id})
129
+
130
+
131
+ def _entry_paragraph(numbered_title: str, page: int, target_id: str, char_pr: str = "0") -> ET.Element:
132
+ """One generated TOC entry: HYPERLINK field wrapping ``title<tab/>page``."""
133
+ p = ET.Element(f"{_HP}p", {
134
+ "id": _rand_id(), "paraPrIDRef": "0", "styleIDRef": "0",
135
+ "pageBreak": "0", "columnBreak": "0", "merged": "0",
136
+ })
137
+ field_id = _rand_id()
138
+ run1 = ET.SubElement(p, f"{_HP}run", {"charPrIDRef": char_pr})
139
+ begin = _field_begin(run1, ftype="HYPERLINK", field_id=field_id, editable="0", dirty="1")
140
+ params = ET.SubElement(begin, f"{_HP}parameters", {"cnt": "5", "name": ""})
141
+ _param(params, "integerParam", "Prop", "0")
142
+ _param(params, "stringParam", "Command", f"?#{target_id};0;1;0;")
143
+ _param(params, "stringParam", "Category", "HWPHYPERLINK_TYPE_HWP")
144
+ _param(params, "stringParam", "TargetType", "HWPHYPERLINK_TARGET_OUTLINE")
145
+ _param(params, "stringParam", "DocOpenType", "HWPHYPERLINK_JUMP_CURRENTTAB")
146
+
147
+ run2 = ET.SubElement(p, f"{_HP}run", {"charPrIDRef": char_pr})
148
+ t = ET.SubElement(run2, f"{_HP}t")
149
+ t.text = numbered_title
150
+ tab = ET.SubElement(t, f"{_HP}tab", {"width": "34032", "leader": "3", "type": "2"})
151
+ tab.tail = str(page)
152
+
153
+ run3 = ET.SubElement(p, f"{_HP}run", {"charPrIDRef": char_pr})
154
+ _field_end(run3, field_id)
155
+ return p
156
+
157
+
158
+ def mark_toc_dirty(doc: HwpxDocument) -> int:
159
+ """Set ``dirty="1"`` on every TABLEOFCONTENTS field — the measured
160
+ re-number trigger: Hancom regenerates a dirty TOC (entries, styles, page
161
+ numbers) when it next opens the document. Call after edits that shift
162
+ pagination. Returns the number of fields marked."""
163
+ count = 0
164
+ for section in doc.oxml.sections:
165
+ for begin in section.element.iter(f"{_HP}fieldBegin"):
166
+ if begin.get("type") == "TABLEOFCONTENTS":
167
+ begin.set("dirty", "1")
168
+ count += 1
169
+ if count:
170
+ section.mark_dirty()
171
+ return count
172
+
173
+
174
+ def add_native_toc(
175
+ doc: HwpxDocument,
176
+ *,
177
+ at_index: int = 0,
178
+ title: str = "<제목 차례>",
179
+ level: int = 2,
180
+ leader: int = 3,
181
+ hyperlink: bool = True,
182
+ dirty: bool = True,
183
+ headings: Sequence[Any] | None = None,
184
+ ) -> dict[str, Any]:
185
+ """Insert a Hancom-native TABLEOFCONTENTS field region at ``at_index``.
186
+
187
+ Entries are generated from ``headings`` (paragraph wrappers) or, when
188
+ omitted, auto-detected 개요/Outline-styled paragraphs. Emitted entry page
189
+ numbers are naive estimates; with ``dirty=True`` (default, measured
190
+ semantics) Hancom regenerates the whole region — correct entries, styles,
191
+ and page numbers — on its next open, so the first thing a user sees is a
192
+ TOC Hancom itself computed. Returns a summary dict.
193
+
194
+ Collection note (measured): Hancom collects outline-styled paragraphs and
195
+ — via the ``ContentsStyles:wstring:0:`` command — style-0 (바탕글)
196
+ paragraphs too; give body text a non-collected style (e.g. 본문, style 1)
197
+ or it will appear as TOC entries after regeneration.
198
+ """
199
+ if headings is None:
200
+ detected = outline_heading_paragraphs(doc)
201
+ else:
202
+ detected = [(p, 1, (p.text or "").strip()) for p in headings]
203
+ if not detected:
204
+ raise ValueError("no outline headings found to build a TOC from")
205
+
206
+ anchors: list[tuple[str, int, str]] = []
207
+ for paragraph, lvl, text in detected:
208
+ anchor = ensure_paragraph_anchor_id(doc, paragraph)
209
+ anchors.append((anchor, lvl, text))
210
+
211
+ section = doc.oxml.sections[0]
212
+ toc_field_id = _rand_id()
213
+
214
+ # opening paragraph: [ctrl fieldBegin TABLEOFCONTENTS][t title]
215
+ open_p = ET.Element(f"{_HP}p", {
216
+ "id": _rand_id(), "paraPrIDRef": "0", "styleIDRef": "0",
217
+ "pageBreak": "0", "columnBreak": "0", "merged": "0",
218
+ })
219
+ run = ET.SubElement(open_p, f"{_HP}run", {"charPrIDRef": "0"})
220
+ begin = _field_begin(
221
+ run, ftype="TABLEOFCONTENTS", field_id=toc_field_id,
222
+ editable="1", dirty="1" if dirty else "0",
223
+ )
224
+ params = ET.SubElement(begin, f"{_HP}parameters", {"cnt": "2", "name": ""})
225
+ _param(params, "integerParam", "Prop", "8")
226
+ _param(
227
+ params, "stringParam", "Command",
228
+ _TOC_COMMAND.format(level=level, leader=leader, hyperlink=1 if hyperlink else 0),
229
+ preserve=True,
230
+ )
231
+ t = ET.SubElement(run, f"{_HP}t")
232
+ t.text = title
233
+
234
+ entry_elements = [
235
+ _entry_paragraph(f"{i}. {text}", 1, anchor)
236
+ for i, (anchor, _lvl, text) in enumerate(anchors, start=1)
237
+ ]
238
+
239
+ # closing paragraph: [ctrl fieldEnd]
240
+ close_p = ET.Element(f"{_HP}p", {
241
+ "id": _rand_id(), "paraPrIDRef": "0", "styleIDRef": "0",
242
+ "pageBreak": "0", "columnBreak": "0", "merged": "0",
243
+ })
244
+ close_run = ET.SubElement(close_p, f"{_HP}run", {"charPrIDRef": "0"})
245
+ _field_end(close_run, toc_field_id)
246
+
247
+ section.insert_paragraphs(at_index, [open_p, *entry_elements, close_p])
248
+ return {
249
+ "tocFieldId": toc_field_id,
250
+ "entryCount": len(anchors),
251
+ "anchors": [a for a, _l, _t in anchors],
252
+ "cachedPagesAreEstimates": True,
253
+ }
254
+
255
+
256
+ def add_page_crossref(
257
+ doc: HwpxDocument,
258
+ paragraph: Any,
259
+ target_paragraph: Any,
260
+ *,
261
+ cached_page: int = 1,
262
+ ) -> dict[str, Any]:
263
+ """Append a Hancom-native page CROSSREF (상호참조 → 쪽 번호) to ``paragraph``.
264
+
265
+ The cached result run is a best-effort estimate; Hancom recomputes CROSSREF
266
+ caches automatically on edit/save (P0 measured on the gold pair: 2→3)."""
267
+ target_id = ensure_paragraph_anchor_id(doc, target_paragraph)
268
+ field_id = _rand_id()
269
+ p_el = paragraph.element
270
+
271
+ run1 = ET.SubElement(p_el, f"{_HP}run", {"charPrIDRef": "0"})
272
+ begin = _field_begin(run1, ftype="CROSSREF", field_id=field_id, editable="0", dirty="0")
273
+ params = ET.SubElement(begin, f"{_HP}parameters", {"cnt": "8", "name": ""})
274
+ _param(params, "booleanParam", "Fiexde", "1") # sic — Hancom's own spelling
275
+ _param(params, "integerParam", "Prop", "0")
276
+ _param(params, "stringParam", "Command", f"?#{target_id};5;0;0;0;")
277
+ _param(params, "stringParam", "RefPath", f"?#{target_id};")
278
+ _param(params, "stringParam", "RefType", "TARGET_OUTLINE")
279
+ _param(params, "stringParam", "RefContentType", "OBJECT_TYPE_PAGE")
280
+ _param(params, "booleanParam", "RefHyperLink", "false")
281
+ _param(params, "stringParam", "RefOpenType", "HWPHYPERLINK_JUMP_CURRENTTAB")
282
+
283
+ run2 = ET.SubElement(p_el, f"{_HP}run", {"charPrIDRef": "0"})
284
+ t = ET.SubElement(run2, f"{_HP}t")
285
+ t.text = str(cached_page)
286
+
287
+ run3 = ET.SubElement(p_el, f"{_HP}run", {"charPrIDRef": "0"})
288
+ _field_end(run3, field_id)
289
+
290
+ paragraph.section.mark_dirty()
291
+ return {"fieldId": field_id, "targetId": target_id, "cachedPage": cached_page}