python-hwpx 6.1.0__tar.gz → 6.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. {python_hwpx-6.1.0/src/python_hwpx.egg-info → python_hwpx-6.3.0}/PKG-INFO +1 -1
  2. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/pyproject.toml +2 -2
  3. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/parts.py +25 -0
  4. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/capabilities.py +49 -0
  5. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/support-matrix.md +6 -0
  6. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/_document_primitives.py +6 -2
  7. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/body.py +4 -1
  8. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/field_marks.py +103 -4
  9. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/header.py +19 -2
  10. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/header_compat.py +104 -15
  11. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/memo.py +3 -1
  12. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/note_authoring.py +1 -1
  13. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/paragraph.py +60 -0
  14. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/table_patch.py +29 -0
  15. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/markdown_export.py +1 -1
  16. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/object_finder.py +1 -1
  17. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/package_validator.py +78 -8
  18. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/text_extractor.py +1 -1
  19. {python_hwpx-6.1.0 → python_hwpx-6.3.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
  20. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/SOURCES.txt +4 -0
  21. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_deviations_registry.py +36 -0
  22. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_field_marks.py +128 -3
  23. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_header_compat_authoring.py +94 -0
  24. python_hwpx-6.3.0/tests/test_index_mark.py +158 -0
  25. python_hwpx-6.3.0/tests/test_instid_attribute_case.py +100 -0
  26. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_layout_lint.py +82 -0
  27. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_namespace_hygiene.py +7 -0
  28. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_release_workflow_safety.py +8 -8
  29. python_hwpx-6.3.0/tests/test_roundtrip_fidelity_corpus.py +47 -0
  30. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_patch.py +67 -0
  31. python_hwpx-6.3.0/tests/test_tracked_overlap_render_smoke.py +112 -0
  32. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/LICENSE +0 -0
  33. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/NOTICE +0 -0
  34. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/README.md +0 -0
  35. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/setup.cfg +0 -0
  36. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/__init__.py +0 -0
  37. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/__init__.py +0 -0
  38. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/_legacy.py +0 -0
  39. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/_resolve.py +0 -0
  40. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/_units.py +0 -0
  41. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/fields.py +0 -0
  42. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/headings.py +0 -0
  43. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/highlight.py +0 -0
  44. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/layout.py +0 -0
  45. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/media.py +0 -0
  46. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/memos.py +0 -0
  47. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/__init__.py +0 -0
  48. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/_base.py +0 -0
  49. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/fields.py +0 -0
  50. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/media.py +0 -0
  51. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/notes.py +0 -0
  52. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/page.py +0 -0
  53. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/refs.py +0 -0
  54. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/shapes.py +0 -0
  55. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/styles.py +0 -0
  56. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/tables.py +0 -0
  57. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/text.py +0 -0
  58. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/ns/tracking.py +0 -0
  59. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/persistence.py +0 -0
  60. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/shapes.py +0 -0
  61. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/_document/tracked.py +0 -0
  62. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/body_patch.py +0 -0
  63. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/data/Skeleton.hwpx +0 -0
  64. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
  65. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
  66. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
  67. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/document.py +0 -0
  68. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/__init__.py +0 -0
  69. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/authoring.py +0 -0
  70. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/eqedit.py +0 -0
  71. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/mathml.py +0 -0
  72. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/render.py +0 -0
  73. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/equation/tokens.py +0 -0
  74. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/errors.py +0 -0
  75. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/experimental.py +0 -0
  76. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/__init__.py +0 -0
  77. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/apply.py +0 -0
  78. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/engine.py +0 -0
  79. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/measure.py +0 -0
  80. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/policy.py +0 -0
  81. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/form_fit/report.py +0 -0
  82. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/ingest/__init__.py +0 -0
  83. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/ingest/base.py +0 -0
  84. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
  85. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/layout/__init__.py +0 -0
  86. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/layout/lint.py +0 -0
  87. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/layout/report.py +0 -0
  88. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/model.py +0 -0
  89. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/mutation_report.py +0 -0
  90. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/__init__.py +0 -0
  91. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/binary_item.py +0 -0
  92. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/checkbox.py +0 -0
  93. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/form_field.py +0 -0
  94. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/highlight.py +0 -0
  95. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/results.py +0 -0
  96. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/objects/tracked.py +0 -0
  97. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/opc/package.py +0 -0
  98. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/opc/relationships.py +0 -0
  99. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/opc/security.py +0 -0
  100. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/opc/xml_utils.py +0 -0
  101. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/__init__.py +0 -0
  102. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/_document_impl.py +0 -0
  103. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
  104. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/color.py +0 -0
  105. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/common.py +0 -0
  106. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/document.py +0 -0
  107. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/document_metadata.py +0 -0
  108. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/document_parts.py +0 -0
  109. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/drop_cap.py +0 -0
  110. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/dutmal_compose.py +0 -0
  111. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/header_part.py +0 -0
  112. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/history_part.py +0 -0
  113. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/master_page.py +0 -0
  114. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/master_page_authoring.py +0 -0
  115. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/namespaces.py +0 -0
  116. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/numbering.py +0 -0
  117. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/numbering_kinds.py +0 -0
  118. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/objects.py +0 -0
  119. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/parser.py +0 -0
  120. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/run.py +0 -0
  121. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/schema.py +0 -0
  122. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/section.py +0 -0
  123. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/section_format.py +0 -0
  124. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/section_layout.py +0 -0
  125. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/section_story.py +0 -0
  126. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/settings.py +0 -0
  127. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/simple_parts.py +0 -0
  128. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/table.py +0 -0
  129. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/utils.py +0 -0
  130. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/oxml/version_part.py +0 -0
  131. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/package.py +0 -0
  132. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/patch.py +0 -0
  133. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/plan/__init__.py +0 -0
  134. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/plan/_execute.py +0 -0
  135. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/plan/_model.py +0 -0
  136. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/plan/_schema.py +0 -0
  137. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/plan/_validate.py +0 -0
  138. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/py.typed +0 -0
  139. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/__init__.py +0 -0
  140. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/ledger.py +0 -0
  141. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/policy.py +0 -0
  142. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/rendering.py +0 -0
  143. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/report.py +0 -0
  144. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/quality/save_pipeline.py +0 -0
  145. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/templates.py +0 -0
  146. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/__init__.py +0 -0
  147. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
  148. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
  149. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/archive_cli.py +0 -0
  150. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/doc_diff.py +0 -0
  151. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/document_merge.py +0 -0
  152. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/document_viewer.py +0 -0
  153. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/exporter.py +0 -0
  154. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/generic_inventory.py +0 -0
  155. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/id_integrity.py +0 -0
  156. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/idempotence.py +0 -0
  157. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/ir_equality.py +0 -0
  158. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/layout_preview.py +0 -0
  159. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/mail_merge.py +0 -0
  160. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/package_reconcile.py +0 -0
  161. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/page_guard.py +0 -0
  162. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/read_fidelity.py +0 -0
  163. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/recover.py +0 -0
  164. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/redline.py +0 -0
  165. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/repair.py +0 -0
  166. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/report_utils.py +0 -0
  167. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
  168. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/table_cleanup.py +0 -0
  169. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/table_navigation.py +0 -0
  170. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/template_analyzer.py +0 -0
  171. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/text_extract_cli.py +0 -0
  172. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/toc_author.py +0 -0
  173. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/toc_fidelity.py +0 -0
  174. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/hwpx/tools/validator.py +0 -0
  175. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  176. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  177. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/requires.txt +0 -0
  178. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
  179. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_add_control.py +0 -0
  180. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_add_heading.py +0 -0
  181. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_adoption_metrics_snapshot.py +0 -0
  182. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_arc_authoring.py +0 -0
  183. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_authoring_defaults.py +0 -0
  184. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_authoring_quality_corpus.py +0 -0
  185. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_blind_eval_fixture.py +0 -0
  186. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_body_patch.py +0 -0
  187. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_border_fill_image_gradient.py +0 -0
  188. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_build_element_census.py +0 -0
  189. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_byte_patch_identity.py +0 -0
  190. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_capabilities_surface.py +0 -0
  191. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_cell_line_spacing.py +0 -0
  192. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_chart_authoring.py +0 -0
  193. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_check_box_authoring.py +0 -0
  194. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_column_break.py +0 -0
  195. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_comment_node_robustness.py +0 -0
  196. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_container_authoring.py +0 -0
  197. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_contract_docs_sync.py +0 -0
  198. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_corpus_read_fidelity.py +0 -0
  199. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_coverage_ledger.py +0 -0
  200. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_coverage_promotion.py +0 -0
  201. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_coverage_targets.py +0 -0
  202. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_doc_diff.py +0 -0
  203. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_context_manager.py +0 -0
  204. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_facade_surface.py +0 -0
  205. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_formatting.py +0 -0
  206. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_merge.py +0 -0
  207. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_metadata.py +0 -0
  208. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_ops_runtime_boundary.py +0 -0
  209. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_save_api.py +0 -0
  210. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_document_viewer.py +0 -0
  211. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_documentation_code_fences.py +0 -0
  212. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_drop_cap_authoring.py +0 -0
  213. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_dutmal_compose_authoring.py +0 -0
  214. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_edit_plan.py +0 -0
  215. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_editor_surface_inventory.py +0 -0
  216. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_engine_surface_6_0.py +0 -0
  217. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_equation_authoring.py +0 -0
  218. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_equation_converter.py +0 -0
  219. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_equation_render.py +0 -0
  220. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_error_contract.py +0 -0
  221. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_error_messages.py +0 -0
  222. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_evalplan_core_primitives.py +0 -0
  223. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_existing_document_format_editing.py +0 -0
  224. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_form_field_authoring.py +0 -0
  225. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_form_fields.py +0 -0
  226. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_form_fill_core_primitives.py +0 -0
  227. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_form_fit.py +0 -0
  228. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_form_fit_integration.py +0 -0
  229. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
  230. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_fuzz_loop.py +0 -0
  231. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_fuzz_regressions.py +0 -0
  232. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_gap_closure_tools.py +0 -0
  233. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_golden_api.py +0 -0
  234. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_highlight_authoring.py +0 -0
  235. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_hp_tab_support.py +0 -0
  236. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_hwpxlib_corpus_read.py +0 -0
  237. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_id_generator_range.py +0 -0
  238. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_id_integrity.py +0 -0
  239. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_image_object_workflow.py +0 -0
  240. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ingest.py +0 -0
  241. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_inline_models.py +0 -0
  242. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_integration_hwpx_compatibility.py +0 -0
  243. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_integration_roundtrip.py +0 -0
  244. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_layout_cache_scope.py +0 -0
  245. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_layout_preview.py +0 -0
  246. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_library_boundary_closure.py +0 -0
  247. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_llms_txt.py +0 -0
  248. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_mail_merge_xlsx.py +0 -0
  249. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_markdown_export.py +0 -0
  250. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_master_page_authoring.py +0 -0
  251. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_memo_and_style_editing.py +0 -0
  252. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_memo_shape_authoring.py +0 -0
  253. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_moved_surface_hints.py +0 -0
  254. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_mutation_report.py +0 -0
  255. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_namespace_handling.py +0 -0
  256. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_new_features.py +0 -0
  257. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_note_authoring_contract.py +0 -0
  258. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_fields.py +0 -0
  259. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_media.py +0 -0
  260. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_notes.py +0 -0
  261. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_page.py +0 -0
  262. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_refs.py +0 -0
  263. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_shapes.py +0 -0
  264. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_tables.py +0 -0
  265. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_ns_tracking.py +0 -0
  266. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_opc_package.py +0 -0
  267. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_open_safety_corpus.py +0 -0
  268. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_outline_numbering.py +0 -0
  269. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_oxml_modularization.py +0 -0
  270. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_oxml_parsing.py +0 -0
  271. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_packaging_license_metadata.py +0 -0
  272. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_packaging_py_typed.py +0 -0
  273. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_page_number_control.py +0 -0
  274. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_para_pr_dedup.py +0 -0
  275. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_paragraph_keep_together.py +0 -0
  276. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_paragraph_property_version_switch.py +0 -0
  277. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_paragraph_section_management.py +0 -0
  278. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_part_hierarchy_read_models.py +0 -0
  279. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_polygon_authoring.py +0 -0
  280. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_product_boundary.py +0 -0
  281. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_public_artifact_hygiene.py +0 -0
  282. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_public_runtime_boundary.py +0 -0
  283. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_read_fidelity.py +0 -0
  284. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_recover_broken_zip.py +0 -0
  285. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_redline_authoring.py +0 -0
  286. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_redline_verify.py +0 -0
  287. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_repair_repack.py +0 -0
  288. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_report_utils.py +0 -0
  289. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_repr_snapshots.py +0 -0
  290. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_return_contract.py +0 -0
  291. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_roundtrip_fidelity.py +0 -0
  292. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_row_heights.py +0 -0
  293. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_run_choice_atom_authoring.py +0 -0
  294. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_save_pipeline.py +0 -0
  295. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_section_headers.py +0 -0
  296. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_section_properties_real_roundtrip.py +0 -0
  297. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_set_paragraph_format_keep.py +0 -0
  298. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_shape_geometry_contract.py +0 -0
  299. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_shape_open_safety_risk_check.py +0 -0
  300. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_skeleton_template_ids.py +0 -0
  301. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_split_cell.py +0 -0
  302. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_split_merged_cell.py +0 -0
  303. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_stable_surface.py +0 -0
  304. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_style_resolution.py +0 -0
  305. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_tab_definition_version_switch.py +0 -0
  306. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_cell_equalize.py +0 -0
  307. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_cleanup.py +0 -0
  308. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_label.py +0 -0
  309. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_navigation.py +0 -0
  310. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_patch_dryrun.py +0 -0
  311. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_patch_m105.py +0 -0
  312. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_table_split_merge.py +0 -0
  313. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_tables_default_border.py +0 -0
  314. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_template_analyzer_enrichment.py +0 -0
  315. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_text_extractor_annotations.py +0 -0
  316. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_text_extractor_run_choice_atoms.py +0 -0
  317. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_title_mark.py +0 -0
  318. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_toc_author.py +0 -0
  319. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_toc_fidelity.py +0 -0
  320. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_validation_severity.py +0 -0
  321. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_validator_comment_nodes.py +0 -0
  322. {python_hwpx-6.1.0 → python_hwpx-6.3.0}/tests/test_version_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 6.1.0
3
+ Version: 6.3.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools==83.0.0", "wheel==0.47.0"]
2
+ requires = ["setuptools==84.0.0", "wheel==0.48.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "6.1.0"
7
+ version = "6.3.0"
8
8
  description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "Apache-2.0"
@@ -25,9 +25,11 @@ from ...errors import HwpxValueError
25
25
  from ...oxml._document_primitives import _HH
26
26
  from ...oxml.header_compat import (
27
27
  apply_paragraph_auto_spacing as _apply_paragraph_auto_spacing,
28
+ remove_license_mark as _remove_license_mark,
28
29
  set_compatible_document_target_program as _set_compatible_document_target_program,
29
30
  set_doc_option_link_info as _set_doc_option_link_info,
30
31
  set_layout_compatibility_flags as _set_layout_compatibility_flags,
32
+ set_license_mark as _set_license_mark,
31
33
  )
32
34
  from ._base import _Namespace
33
35
 
@@ -215,6 +217,29 @@ class PartsNamespace(_Namespace):
215
217
  footnote_inherit=footnote_inherit,
216
218
  )
217
219
 
220
+ def set_license_mark(
221
+ self,
222
+ *,
223
+ mark_type: str,
224
+ flag: int,
225
+ lang: int | None = None,
226
+ ) -> None:
227
+ """`hh:docOption/hh:licensemark` 설정("입력 > CCL 넣기…"가 남기는
228
+ 문서 수준 라이선스 레코드). 실측 gold는
229
+ `type="CCL" flag="0" lang="6"` — 스키마가 `type`을 `xs:unsignedInt`
230
+ 로 선언하는데도 실물은 문자열이라 `mark_type`이 `str`이다.
231
+
232
+ 눈에 보이는 CC 배지는 별개다 — 라이선스 증서 URL을 `href`로 단
233
+ 평범한 그림(`add_picture`)이고, 이 setter는 문서 수준 레코드만
234
+ 건드린다."""
235
+
236
+ _set_license_mark(self._primary_header(), mark_type=mark_type, flag=flag, lang=lang)
237
+
238
+ def remove_license_mark(self) -> bool:
239
+ """`hh:docOption/hh:licensemark`를 지운다. 실제로 지웠으면 True."""
240
+
241
+ return _remove_license_mark(self._primary_header())
242
+
218
243
  def set_paragraph_auto_spacing(
219
244
  self,
220
245
  para_pr_id: str | int,
@@ -432,6 +432,55 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
432
432
  "entry_points": ("hwpx.document:HwpxDocument",),
433
433
  "authoring_methods": (),
434
434
  },
435
+ {
436
+ "area": "index-mark",
437
+ "namespace": "doc.refs",
438
+ "matrix_row": "색인 표시",
439
+ # P4 — 팀장 실한컴 GUI 프로브 gold 2건(1키/2키) 역설계.
440
+ # HwpxOxmlParagraph.add_index_mark만 있다(HwpxDocument 루트에는
441
+ # 없음) — title-mark와 같은 이유로 authoring_methods=(). title-mark
442
+ # 와 같은 doc.refs 네임스페이스(둘 다 "본문 문단에 다는 색인/차례
443
+ # 계열 마크")지만 별도 영역이다: 겨냥하는 요소도(hp:indexmark vs
444
+ # hp:titleMark) 편집기 메뉴도 다르고, 한 영역에 묶으면 매트릭스
445
+ # 등급 하나로 두 기능의 검증 상태를 뭉개게 된다(toc-crossref의
446
+ # "혼합 지원 영역 오염" 방지 원칙).
447
+ "entry_points": ("hwpx.document:HwpxDocument",),
448
+ "authoring_methods": (),
449
+ },
450
+ {
451
+ "area": "mail-merge-field",
452
+ "namespace": "doc.fields",
453
+ "matrix_row": "메일머지 표시 필드",
454
+ # P4 — 팀장 실한컴 GUI 프로브 gold 역설계. 기존 "mail-merge"
455
+ # 영역에 합치지 않고 새로 만든 이유: 그 영역은 namespace=None ·
456
+ # 진입점이 순수 모듈 함수(hwpx.tools.mail_merge:merge_template_rows) ·
457
+ # 등급이 Edit(기존 hp:t 텍스트 치환, 새 요소를 안 만든다)인 반면
458
+ # 이쪽은 doc.fields 문단 메서드 · Create(hp:fieldBegin
459
+ # type="MAILMERGE"를 새로 만든다)다 — 합치면 한 영역이 두 진입
460
+ # 경로·두 등급을 갖는 "혼합 지원 영역 오염"이 된다(toc-crossref에서
461
+ # 하이퍼링크·책갈피를 떼어낸 것과 같은 판단). 행 제목도 기존 행이
462
+ # "placeholder 템플릿 배치 생성"으로 스코프를 명시하고 있어 이
463
+ # 기능을 담을 수 없다. date-time-proofreading-mark-fields와 같은
464
+ # field_marks.py·같은 hp:fieldBegin 계열이지만 그쪽에 합류시키지
465
+ # 않은 것도 같은 이유다 — 그 행은 "입력 > 필드" 계열 3종이고
466
+ # 메일머지 표시는 "도구 > 메일 머지" 메뉴다.
467
+ "entry_points": ("hwpx.document:HwpxDocument",),
468
+ "authoring_methods": (),
469
+ },
470
+ {
471
+ "area": "license-mark",
472
+ "namespace": "doc.parts",
473
+ "matrix_row": "라이선스 표시(CCL)",
474
+ # P4 — 팀장 실한컴 GUI 프로브 gold 역설계. doc.parts.set_license_
475
+ # mark/remove_license_mark는 set_* 이름이라 document-options-
476
+ # compatibility·document-metadata와 같은 이유로
477
+ # authoring_methods=(). 트레인㊺가 "CCL 전용 요소는 없다, 그림+
478
+ # 하이퍼링크 조합일 뿐"이라고 판정했던 자리를 실측이 뒤집었다 —
479
+ # hh:licensemark는 실제로 이 메뉴가 쓰는 전용 문서 수준 레코드다
480
+ # (배지 그림은 그 판정대로 별개의 hp:pic+href).
481
+ "entry_points": ("hwpx.document:HwpxDocument",),
482
+ "authoring_methods": (),
483
+ },
435
484
  )
436
485
 
437
486
  #: 패키지에 동봉되는 계약 문서 이름 → 파일. MCP resources 표면의 원천.
@@ -39,6 +39,7 @@
39
39
  | 각주/미주 | Edit·Create·Render-verified | `add_footnote`·`add_endnote`; M6 읽기 경로에서 note 노출. 5.5.0에서 실한컴 gold 계약으로 방출 수리(본문 run 내 `hp:ctrl` 래핑·`number`/`suffixChar`·각주 본문 `autoNum`+스타일 15/16) — 옛 방출이 각주를 그리지 않던 실결함 종결, 리더는 실한컴산·구식 양형상 모두 수용(CHANGELOG [5.5.0]) |
40
40
  | 네이티브 목차(TOC)/상호참조 | Create·Render-verified | `hwpx.tools.toc_author.add_native_toc`·`mark_toc_dirty`·`toc_verify`; corpus-metrics「네이티브 목차」구조 15/15, 실한컴 재계산 후 페이지 정합 5/5 |
41
41
  | 차례 숨기기·제목 차례 표시 | Create(experimental)·Render-verified | `HwpxOxmlParagraph.add_title_mark(in_toc=)`(6.15 트레인, DEV-044). 스키마는 `hp:titleMark`를 `hp:t`의 선택군 자식으로 실제 선언하되(`ignore: xs:boolean, default="false"`) `xs:documentation`이 전혀 없어 의미는 미문서화 — 실코퍼스 14건(전부 `ignore="1"`, 차례 페이지 항목 문단의 헤딩 텍스트 미러 run 바로 앞)으로 구조는 6.13에 이미 확정됐으나, 캐럿이 있는 문단에 정확히 들어간다는 타겟팅 계약은 이 환경의 자동화 한계(캔버스 클릭·키 입력 모두 문서에 미도달)로 실측이 안 돼 저작을 보류하고 있었다. **6.15**: 팀장의 Windows 박스 COM `SetPos`+`MarkTitle`/`HideTitle` 3변형(`SetPos(0,2,0)`+`MarkTitle`→p2에만 `ignore="1"`, `SetPos(0,2,0)`+`HideTitle`→p2에만 `ignore="0"`, `SetPos(0,1,0)`+`MarkTitle`→p1에만 `ignore="1"`)이 타겟팅을 확정 — 마크는 항상 캐럿이 있는 문단의 첫 run 첫 `hp:t` 맨 앞, 그 문단 자신의 텍스트 바로 앞에 들어간다(이전 Mac 프로브의 절 정의 문단 착지는 캐럿 이동 불가에 따른 퇴화 현상이었음이 확인됨). 폴라리티는 이름의 직관과 반대(`in_toc=True`="제목 차례 표시"→`ignore="1"`, `in_toc=False`="차례 숨기기"→`ignore="0"`) — Mac GUI A/B와 Windows COM 양쪽에서 독립 재확인. 호출자가 대상 문단을 직접 지정하는 API 형태가 실 편집기의 "캐럿 문단" 타겟팅과 정확히 대응한다. 실한컴 렌더 검증: `docs/openrate/report-v21.json` authored-title-mark 스트라텀(극성 로테이션·단일/두-표제 문서·양쪽 표제 반대 극성 동시 표시·기존 charPr 합성 각 1건, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-15 측정) |
42
+ | 색인 표시 | Create(experimental) | `HwpxOxmlParagraph.add_index_mark(first, second=)`(P4). 팀장 실한컴(HWP 13.0.0.3901) GUI 프로브 gold 2건(`tests/fixtures/gui_probes/index_mark_first_only.hwpx`=1단계 키만, `index_mark_two_keys.hwpx`=2단계 키까지) 역설계. 배치는 `add_title_mark`와 **다르다** — titleMark는 `hp:t` 안에 들어가지만 indexmark는 같은 run 안의 `hp:ctrl` 형제로 텍스트 **앞**에 선다(`<hp:run><hp:ctrl><hp:indexmark><hp:firstKey>apple</hp:firstKey></hp:indexmark></hp:ctrl><hp:t>apple item paragraph.</hp:t></hp:run>`). 타겟팅 계약은 같다 — 호출자가 대상 문단을 직접 지정하는 형태가 실 편집기의 "캐럿이 있는 문단"과 대응한다. **스키마 편차**: `ParaList XML schema.xml:209-216`은 `firstKey`/`secondKey`를 둘 다 필수 시퀀스로 선언하는데(속성 0개, 두 키 모두 무타입) 실물은 1단계 색인에서 `secondKey`를 아예 생략한다 — `add_new_num`의 `autoNumFormat` 생략과 같은 부류로 실측을 따랐다(빈 문자열 키는 생략과 방출 XML이 달라져 typed 거부). 코퍼스 실사용 0건 — 계약 근거는 GUI 프로브 gold 전부. 실한컴 렌더 검증: `docs/openrate/report-v22.json` authored-index-mark 스트라텀(1키/2키/두 문단 독립 마크/**한 문단 2마크(관측 밖 경계)**/커스텀 charPr 표제 합성, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-18 측정) — 관측 밖 조합 포함 전량 수용 |
42
43
  | 암호화 HWPX | Unsupported-and-rejected | 복호화 API 없음. 암호화된 content part는 파싱 단계에서 예외(`XMLSyntaxError`)로 거부 — 무음으로 잘못된 문서를 만들지 않음(fail-closed) |
43
44
  | HWP 5.x 바이너리 | Unsupported-and-rejected | HWP v5는 ZIP이 아니므로 열기 시 `BadZipFile` 예외. OLE2/CFBF 시그니처를 확인하면 예외 메시지가 HWPX 변환을 안내한다(예외 타입은 그대로) |
44
45
  | 누름틀(form field) 생성 | Parse·Edit·Create(experimental)·Render-verified | `list_form_fields`·`fill_form_field`로 조회·서식 보존 채움에 더해, `doc.fields.add`(구 `add_form_field`, 5.1.0+)가 실한컴 CLICKHERE 계약 그대로 신규 누름틀을 생성한다(표 셀 배치 포함). 만든 필드는 기존 list/fill과 실제 한컴이 특수분기 없이 소비. 실한컴 렌더 검증: `docs/openrate/report-v15.json` authored-formfield 스트라텀(prompt/memo/editable 로테이션 + 표 셀 배치 1건, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정) — 이 영역의 첫 독립 실한컴 판정. **원장 라우팅은 없음** — `hp:fieldBegin`은 CLICKHERE/TOC/CROSSREF/하이퍼링크/메모가 공유하는 저수준 요소라 여전히 미등록(무근거 승격 회피 원칙) — 등급 갱신은 이 산문과 report-v15.json 직접 인용으로만 |
@@ -49,6 +50,7 @@
49
50
  | 바탕쪽 | Parse·Create(experimental)·Render-verified | `doc.parts.master_pages`(읽기, 6.4 트레인⑮)·`.add_master_page`(쓰기, 6.13 트레인㊻) + `doc.page.set_master_page`/`.master_page_refs`(절 참조 배선, `section_format.py`). 편집기 메뉴 표면 역매핑(트레인㊷)이 [부분 대응](읽기만, 쓰기 없음)으로 지목한 갭 — `HwpxOxmlMasterPage` 자신의 독스트링이 "읽기 전용... 쓰기 경로는 열지 않는다"고 명시했던 6.4 트레인⑮의 의도적 스코프 축소를 이번에 닫는다. 유일한 실 예시(`error__20250808__2015년_12월_재난안전종합상황_분석_및_전망.hwpx`)로 역설계: 파트 파일명·매니페스트 `opf:item id`·`masterPage` 루트 자신의 `id`·절의 `hp:masterPage/@idRef` 넷 다 **같은 문자열**("masterpage0")을 쓴다. 매니페스트에 이 파트는 있으나 spine `opf:itemref`는 없다(바탕쪽은 읽기 순서 파트가 아니다). `hp:secPr`의 자식 시퀀스에서 `hp:masterPage`는 맨 끝(pageBorderFill들 뒤)에 오고 `masterPageCnt`가 그 개수와 일치 — `SectionProperties.add_master_page_reference`가 그대로 append(멱등, 중복 idRef 무시)·카운트 동기화. `masterPage`의 `hp:subList`는 `textWidth`/`textHeight`를 실 예시가 페이지 여백에서 계산한 값으로 쓰지만 그 공식을 역산할 근거가 1건으로는 없어 다른 subList 빌더의 정직한 기본값("0")을 그대로 둔다(curve와 같은 원칙). 신규 바탕쪽이 여러 절 사이에서 공유/독립되는 관행 자체는 실증 근거가 없어 "파트 하나 생성 + 호출자가 지정한 절마다 명시적 배선"이라는 최소 계약만 제공한다. 실한컴 렌더 검증: `docs/openrate/report-v17.json` authored-master-page 스트라텀(page_type/page_number/page_duplicate/page_front 로테이션 + 두 번째 바탕쪽 생성으로 파트 인덱스 자동증가 확인, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-11 측정) — **패키지에 새 파트를 싣는 쓰기 경로가 실한컴을 통과한 첫 확인**(이전에 자체 발견·수리한 매니페스트 dirty 결함의 반증이기도 하다) |
50
51
  | 날짜/시간·교정 부호·파일 이름 필드 | Create(experimental)·Render-verified | `HwpxOxmlParagraph.add_date_field`/`.add_proofreading_mark`/`.add_path_field`(`oxml/field_marks.py`, 6.13 트레인㊻+6.14 트레인㊽b). 팀장 실한컴 macOS GUI 프로브①③(2026-08-11) gold(`tests/fixtures/gui_probes/date_and_proofreading_mark.hwpx`) 역설계 — `add_hyperlink`의 3-run 격리와 달리 세 필드 다 단일 `hp:run` 안에 `ctrl(begin)`/`t`/`ctrl(end)`을 나란히 담는다(선택군 반복 허용은 DEV-017이 이미 확인). **날짜/시간**(`type="DATE"`): `id`/`fieldid` 독립 난수·`dirty="0"`(TOC와 달리 재계산 트리거 아님)·`hp:parameters`(`Prop=8`·`Command`=포맷 미리보기 문자열·`DateNation`="KOR"·`DateFormat`="YYYY년 M월 D일") — 관측 포맷 1건뿐이라 이 값만 지원(typed 거부, curve·connectLine과 같은 원칙), 캐시 텍스트는 호출자가 계산해 넘긴다. **교정 부호**(`type="PROOFREADING_MARKS_SIGN"`): 스키마 선언값(`PROOFREADING_MARKS`)과 실측이 달라 **DEV-043**으로 등재 — `hp:parameters`(`Prop=0`·`Command="$RevisionSign;N;"`, 캐시 텍스트 없음), 인덱스가 확인된 건 "띄움표"(N=1) 하나뿐이라 그것만 지원(대화상자 21종 중 나머지 20종은 인덱스-기호 대응표 근거 없음). **파일 이름**(`type="PATH"`, 6.14 트레인㊽b): GUI 프로브 불필요 — 트레인㊺가 이미 확보한 실 코퍼스 계약(`tests/fixtures/markdown_export/99_all_in_one_stress.hwpx`, 페이지 머리말 안) 그대로. `hp:parameters`(`Prop=8`=DATE와 공유값·`Command`=`Format`=`"$F"` 리터럴 고정), 캐시 텍스트는 관측 예시에서 파일명 그대로(호출자가 계산해 넘김, 전체 경로인지 파일명뿐인지조차 표본 1건으로는 확정 못 함). 이 세 필드를 정밀 대조하다 `hp:fieldEnd`가 `beginIDRef` 말고도 `fieldid`(fieldBegin 자신의 `fieldid`와 같은 값)를 반복해서 갖는다는 사실도 새로 확인 — 날짜/교정 부호의 기존 구현엔 이게 빠져 있었다(이미 v18 실한컴이 수용은 했으나 gold와 완전히 일치시키려 세 필드 전부 소급 수정). 실한컴 렌더 검증: `docs/openrate/report-v18.json` authored-date-field·authored-proofreading-marks 두 스트라텀(cached_text/배치/반복/char_pr_id_ref/표제 합성 로테이션 각 5건, 표 합성 포함, 10건), macOS Hancom GUI 오라클 10/10 render_checked·0 render_failed(2026-08-11 측정) — **팀장 자신의 GUI 리버스로 확보한 계약이 같은 편집기로 되돌려 검증된** 첫 사례(계약 출처와 검증 오라클이 동일 애플리케이션). 파일 이름 필드는 `docs/openrate/report-v19.json` authored-path-field 스트라텀(cached_text/배치/반복/char_pr_id_ref/표제 합성 로테이션, 5건 — fieldEnd/@fieldid 소급 수정을 실제로 담은 첫 배치), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-11 측정)로 마찬가지로 검증 완료 |
51
52
  | 문서 옵션·호환성 | Parse·Preserve·Edit·Render-verified | `doc.parts.set_compatible_document_target_program`·`set_layout_compatibility_flags`·`set_doc_option_link_info`·`set_paragraph_auto_spacing`(6.6+). 읽기는 5.x부터(`hh:compatibleDocument`/`docOption`, cycle 6.1 `settings.py`/`header.py`) — 6.6이 쓰기 쪽. 실코퍼스 47/47 리버스로 계약 확정: `compatibleDocument@targetProgram`은 `"HWP201X"`만 관측·`layoutCompatibility`는 플래그 0개(스키마 48종 선언 대비)·`docOption/linkinfo`는 `path` 항상 빈 문자열·`footnoteInherit` 항상 `"0"`·`pageInherit`만 실제로 갈림(8/47 `"1"`). `hh:paraPr/hh:autoSpacing`은 `_apply_paragraph_margins`/`_apply_paragraph_line_spacing`이 이미 쓰는 자손-순회 관용구를 재사용하지만, 그 둘과 달리 실 autoSpacing은 `hp:switch`로 안 감싸인다(1832/1832 직속 자식, DEV-018과 대조 확인). 불리언은 전부 `"0"`/`"1"`(DEV-006과 같은 관례, `"true"`/`"false"` 0건). `HwpxOxmlHeader`의 owner 파일(`header_part.py`)이 1600줄 캡에 헤드룸이 없어(1599/1600) 새 모듈(`oxml/header_compat.py`)의 자유함수로 산다. 실한컴 렌더 검증: `docs/openrate/report-v10.json` authored-compat 스트라텀, macOS Hancom GUI 오라클 15/15 render_checked·0 render_failed(2026-08-07 측정) — **관측 밖 조합(targetProgram=HWP2018·실제 layoutCompatibility 플래그·footnoteInherit=1·비어있지 않은 linkinfo path) 전부 포함해서도 15/15 수용**, 즉 실한컴의 실제 수용 경계가 실코퍼스 관측 분포보다 넓다는 게 확인됐다(모든 프로브 축에서 거부 0건) |
53
+ | 라이선스 표시(CCL) | Parse·Create | `doc.parts.set_license_mark(mark_type=, flag=, lang=)`/`.remove_license_mark()`(`oxml/header_compat.py`, P4). 팀장 실한컴 GUI 프로브 gold(`tests/fixtures/gui_probes/license_mark_ccl.hwpx`, "입력 > CCL 넣기…") 역설계 — `hh:docOption` 안 `hh:linkinfo` 바로 뒤 형제로 `<hh:licensemark type="CCL" flag="0" lang="6"/>`. **트레인㊺의 판정("CCL 전용 요소는 없다, 그림+하이퍼링크 조합일 뿐")을 실측이 뒤집었다** — 코퍼스 67파일 0건이라 실사용 근거가 없었을 뿐, 이 메뉴는 실제로 전용 문서 수준 레코드를 쓴다(배지 그림이 별개라는 그 판정의 나머지 절반은 맞다 — 배지는 라이선스 증서 URL을 `href`로 단 평범한 `hp:pic`이고, 이 setter는 문서 수준 레코드만 건드린다). **스키마 편차**: `Header XML schema.xml`의 `DocOptionType`은 `type`을 `xs:unsignedInt use="required"`로 선언하는데 실물은 문자열 `"CCL"`이다 — 스키마만 보고 `int`로 선언돼 있던 읽기 모델(`LicenseMark.type`)은 실한컴 CCL 문서를 `to_model()`로 읽는 순간 `ValueError: Invalid integer value: 'CCL'`로 터졌고, 이 트레인이 실측을 따라 `str`로 고쳤다(DEV-043과 같은 실측 우선 부류). `flag`/`lang`은 관측 `0`/`6`이라 정수 유지 — 관측이 1건뿐이라 값 범위는 강제하지 않는다(`set_compatible_document_target_program`과 같은 원칙, `mark_type` 빈 문자열만 typed 거부). 실한컴 렌더 검증: `docs/openrate/report-v22.json` authored-license-mark 스트라텀(관측값 CCL/0/6·flag/lang 변주·**미관측 type 문자열 "KOGL"(경계)**·재설정 멱등, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-18 측정) — 미관측 type 문자열도 수용(수용 경계가 관측 분포보다 넓다) |
52
54
  | 페이지 레이아웃(용지·여백·머리말/꼬리말·쪽번호·단·줄번호·격자·요소 숨김) | Edit·Create·Render-verified | `doc.page`(6.8 트레인㉚ 등재, 코드 자체는 이전 사이클부터 존재). 최초 5개(`set_visibility`/`set_line_numbers`/`set_grid`/`restart_page_number`/`hide_page_elements` → `hp:visibility`/`hp:lineNumberShape`/`hp:grid`/`hp:newNum`/`hp:pageHiding`, `docs/openrate/report-v4.json` authored-page-structure·`report-v6.json` authored-pagecontrol, 각 15/15 render_checked)에 이어 **6.9 트레인㉜**이 나머지 실질 표면을 마저 검증: `set_size`/`set_margins`/`set_header`/`set_footer`/`set_page_number`/`set_columns` → `hp:pagePr`/`hp:margin`/`hp:header`/`hp:footer`/`hp:colPr`/`hp:pageNum`, `docs/openrate/report-v12.json` authored-pagelayout 15/15 render_checked·0 render_failed(2026-08-07 측정, header 제거 경로도 이 배치에서 함께 확인). 11개 요소 전부 `by-capability-area+openrate-corpus`. **남은 정직한 여백**: `hp:autoNum`은 각주/목록류 자동번호와 공유하는 요소라 의도적으로 미등록(실제 표시 요소인 `hp:pageNum`은 검증됨, DEV류 무근거 승격 회피 원칙과 동일), `setup()`은 위 이미 검증된 setter들을 그대로 호출하는 합성 편의 메서드라 별도 프로브가 없다, `remove_footer`는 `remove_header`와 대칭이나 v12가 이름으로 지목한 건 후자뿐이다. **6.12 트레인㊸ 갭③**이 `text_direction`/`set_text_direction`(`hp:secPr` 자신의 `textDirection`/`textVerticalWidthHead` 속성, 편집기 메뉴 표면 역매핑이 찾은 신규 갭)을 추가했다 — 실코퍼스 67파일 전수(모든 fixture 하위 디렉터리) 측정: `hp:secPr` 74건 전부 `textDirection="HORIZONTAL"`·`textVerticalWidthHead="0"`, `VERTICAL`/`VERTICALALL` 실사용 0건. 그래도 이 값은 새 구조를 추측하는 게 아니라 스키마가 명확히 선언한 열거값(`HORIZONTAL`/`VERTICAL`/`VERTICALALL`)을 이미 정상 파싱·직렬화되는 기존 요소(`hp:secPr`)에 그대로 쓰는 것뿐이라 구조적 추측 위험이 없다. 실한컴 렌더 검증: `docs/openrate/report-v16.json` authored-text-direction 스트라텀(`VERTICAL`×2·`VERTICALALL`×2·명시적 `HORIZONTAL`×1, `vertical_header_footer` True/False 조합), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-10 측정) — **이 실코퍼스(67파일 전수)에 실사용 0건이던 `VERTICAL`/`VERTICALALL`이 이 프로젝트에서 실한컴을 통과한 첫 문서**다. **원장 라우팅은 없음**: `textDirection`은 요소가 아니라 속성이고, 그 부모 `hp:secPr` 자체는 모든 문서에 항상 존재하는 컨테이너(위 5+6개 자식 요소를 이미 개별 등록)라 `hp:secPr`를 통째로 등록하면 이 값을 한 번도 안 건드린 문서까지 전부 위양성 처리된다 |
53
55
  | 문자 서식(굵게·기울임·밑줄·글꼴·크기·색 등) | Edit·Create·Render-verified | `doc.styles.ensure_run`(6.8 트레인㉚ 등재, 코드는 이전 사이클부터 존재 — `hh:charPr`는 이미 "문단·표 저작/편집" 영역이 요소째 소유하고 있어 이 영역은 캐파빌리티 키워드 배선 없이 매트릭스 산문으로만 근거를 남긴다). **17/17 키워드 인자 전부 실한컴 증거 확보 완결(6.12 트레인㊶)**: `bold`/`color`/`size`(`docs/openrate/report-v4.json` authored-named-style, 10/10 render_checked), `outline`/`emboss`/`engrave`/`script`(sup/sub, `docs/openrate/report-v7.json` authored-charformat, 15/15 render_checked), `underline`/`underline_shape`/`underline_color`/`strike`/`strike_shape`/`ratio`/`letter_spacing`/`shadow`(`docs/openrate/report-v14.json` authored-charformat2, 5/5 render_checked, 2026-08-08), 그리고 마지막 잔존이던 `italic`(v14의 authored-charformat2 뱅크가 빠뜨렸던 항목 — `docs/openrate/report-v15.json` authored-charformat-italic 스트라텀, italic 단독+bold/underline/color 조합, macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed, 2026-08-08 측정)까지 전부 확보됐다. `base_char_pr_id`는 XML 요소가 아니라 파이썬 레벨 조합 방식(기존 charPr 복제 후 오버라이드)이라 이 17개 카운트에서 애초에 제외(등록 대상 없음), `highlight`는 별도 매트릭스 행("형광펜(하이라이트)")이 이미 독립적으로 Render-verified라 이 행의 카운트에 안 넣는다 — 두 제외 사유 모두 이 행 초판부터 일관됨 |
54
56
  | 목록 서식(글머리표·번호매기기) | Create·Edit·Render-verified | `doc.styles.apply_list_format`/`.ensure_numbering`(6.8 트레인㉚ 등재, 코드는 이전 사이클부터 존재). 실한컴 렌더 검증: `docs/openrate/report-v14.json` authored-listformat 스트라텀(bullet/number kind × level 1·2 로테이션, bullet_char/number_format/start 오버라이드), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정) — 이 영역의 첫 독립 실한컴 판정. `hh:bullet`/`bullets`/`numbering`/`numberings`/`paraHead`가 원장에 새로 등록됐다(6.11 트레인㊳, 이전엔 대응 capabilityArea 자체가 없었다) — `hh:heading`(paraPr 안에서 numbering/bullet을 가리키는 polymorphic idRef)은 document_merge 등 다른 경로도 공유하는 저수준 요소라 의도적으로 미등록. **6.13 트레인㊻**: `ensure_numbering`/`apply_list_format`이 세 번째 `kind="outline"`을 받는다 — `hh:heading type="OUTLINE"`이 이 목록 서식과 **같은** `hh:numbering`/`hh:paraHead` id-space를 쓴다는 사실(`bind_outline_level`, `_document/headings.py`)을 그대로 재사용해, `add_heading`/`apply_paragraph_format(outline_level=)`이 항상 고정 `idRef="0"`(스켈레톤 개요 스타일)만 참조하던 것과 달리 **커스텀 개요 번호 모양**(numFormat/start)을 만들 길을 연다. 실코퍼스 67파일 전수: `hh:heading type="OUTLINE"` 451건 전부 `idRef="0"`뿐이라 커스텀 idRef 조합은 실코퍼스 밖이지만 — 재사용하는 `hh:numbering`/`hh:paraHead` 구조 자체는 v14에서 이미 render-verified라 구조적 추측 위험은 없었고, 이번에 실측으로도 확인됐다: `docs/openrate/report-v17.json` authored-outline-numbering 스트라텀(level/numFormat/start 로테이션 + 기존 `apply_paragraph_format(outline_level=)` 수준 증가와 합성한 1건, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-11 측정) — 합성 레코드가 "`apply_paragraph_format(outline_level=)`이 paraPr을 새로 만들 때마다 `idRef="0"`을 하드코딩해 커스텀 번호 모양을 무음으로 덮어쓴다"는 기존 동작(버그 아님, 충실 재현)도 같이 실증했다. `apply_paragraph_format(outline_level=)`을 통한 개요 적용/해제·수준 증감은 코드가 이미 완비돼 있었다(신규 코드 없음, 이번 트레인이 확인만 함) |
@@ -57,6 +59,7 @@
57
59
  | 찾아바꾸기 | Edit·Render-verified | `doc.text.replace`/`.find_runs`(6.8 트레인㉚ 등재, 코드는 이전 사이클부터 존재). 실한컴 렌더 검증: `docs/openrate/report-v14.json` authored-findreplace 스트라텀(무제한 치환 + `char_pr_id_ref`로 좁힌 스타일 한정 치환 교대), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정). **원장 라우팅은 없음** — 기존 `hp:t` 텍스트만 치환할 뿐 새 요소를 안 만든다(document-merge·mail-merge·table-navigation-fill과 같은 이유) — 등급 갱신은 이 산문과 report-v14.json 직접 인용으로만 |
58
60
  | 하이퍼링크·책갈피 | Create·Render-verified | `doc.refs.add_hyperlink`/`.add_bookmark`(6.8 트레인㉚에서 "네이티브 목차(TOC)/상호참조" 영역에서 분리 — 그 영역의 Render-verified 근거는 TOC 구조·페이지 정합뿐이라 하이퍼링크·책갈피를 한 번도 독립적으로 커버한 적이 없었다, 편집기 표면 인벤토리 트레인㉙ 발견). **6.9 트레인㉜**: `docs/openrate/report-v12.json` authored-hyperlink-bookmark 스트라텀(북마크 생성 후 외부 URL/문서 내 `#앵커` 하이퍼링크 교대), macOS Hancom GUI 오라클 15/15 render_checked·0 render_failed(2026-08-07 측정) — 이 영역의 첫 독립 실한컴 판정. `hp:bookmark`는 요소 직접 등록(`by-openrate-corpus`), `hp:fieldBegin`(하이퍼링크 자신의 컨트롤)은 CLICKHERE/TOC/CROSSREF와 공유하는 저수준 요소라 여전히 요소 등록 없음 — 영역 등급만 실측으로 갱신, 원장 오염 경로는 그대로 차단 |
59
61
  | 메일머지(placeholder 템플릿 배치 생성) | Edit·Render-verified | `hwpx.tools.mail_merge.merge_template_rows`(6.8 트레인㉛ 등재, 코드는 이전 사이클부터 존재 — 편집기 표면 인벤토리 트레인㉙·계층 판정 트레인㉚이 찾을 때까지 캐파빌리티·매트릭스 어디에도 등재가 없었다). `{{field}}`/`${field}`/`<<field>>` 세 플레이스홀더 문법을 지원하는 템플릿 1개 + 행 데이터(csv/json/시퀀스)로 문서 N개를 배치 생성 — 기존 `hp:t` 텍스트를 치환하는 것이라 새 요소를 만들지 않는다(Create가 아니라 Edit). `strict`/`fit_policy`/`value_sanitizer` 등 운영 옵션 다수. 실한컴 렌더 검증: `docs/openrate/report-v13.json` authored-mailmerge 스트라텀(세 플레이스홀더 문법 각각 겨냥하는 전용 템플릿으로 회전), macOS Hancom GUI 오라클 10/10 render_checked·0 render_failed(2026-08-08 측정) — 이 영역의 첫 독립 실한컴 판정 |
62
+ | 메일머지 표시 필드 | Create(experimental) | `HwpxOxmlParagraph.add_mail_merge_field(name, cached_text=)`(`oxml/field_marks.py`, P4). 팀장 실한컴 GUI 프로브 gold(`tests/fixtures/gui_probes/mailmerge_display_fields.hwpx`, "도구 > 메일 머지 > 메일 머지 표시 달기"로 필드 2개 삽입) 역설계 — 위 「메일머지(placeholder 템플릿 배치 생성)」가 **기존 `hp:t` 텍스트를 치환**하는 Edit인 것과 달리 이쪽은 `hp:fieldBegin type="MAILMERGE"` 자체를 새로 만드는 Create다(그래서 별도 영역). DATE/PATH와 같은 단일 run 안 `ctrl(begin)`/`t`/`ctrl(end)` 배치에 더해 **끝에 빈 `hp:t`가 하나 더** 붙고(gold 2/2), `hp:fieldBegin`에 `metaTag` 속성이 **없다**(DATE/교정 부호/PATH gold는 셋 다 `metaTag=""`) — 필드 종류마다 실측이 갈리는 유일한 속성. `hp:parameters`(`cnt="5"`): `Fiexde=1`(**오타 철자가 실물·한컴 공식 문서 공통의 정본이라 정정 금지** — `trackchageConfig`·memo의 `NOMAL`과 같은 부류) · `Prop=8`(DATE/PATH와 같은 캐시 필드류 공유값) · `Command`/`FieldValue`가 둘 다 필드 이름 그대로 · `FieldType="USER_DEFINE"`. 캐시 텍스트 기본값 `{{이름}}`은 `hwpx.tools.mail_merge`가 인식하는 플레이스홀더 문법이라 이 필드로 저작한 템플릿이 그 배치 생성기에 그대로 물린다. gold의 두 필드는 `id`가 다른데 `fieldid`는 같은 값을 공유하나, 표본이 문서 하나뿐이라 채번 규칙을 역산할 근거가 없어 DATE/PATH와 같은 독립 난수를 쓴다. 실한컴 렌더 검증: `docs/openrate/report-v22.json` authored-mailmerge-field 스트라텀(단일/2필드/커스텀 캐시/**한글 필드명(경계)**/표 셀 안 필드, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-18 측정) |
60
63
  | 문서 끼워 넣기(문서 병합) | Create·Render-verified | `hwpx.tools.document_merge.append_document`/`.insert_document`(6.9 트레인㉝ v1, 6.10 트레인㊱ v2 — 편집기 표면 인벤토리 트레인㉙의 macOS 메뉴 전수 스캔이 찾은 신규 갭, "입력→문서 끼워 넣기…"). 다른 HWPX 문서의 본문을 연 문서에 끼워 넣는다 — 핵심은 헤더가 소유한 8개 공유 자원(charPr/paraPr/style/borderFill/tabPr/numbering·bullet/memoPr/fontfaces)을 원본에서 새 id로 복제해 재매핑하는 것이지 단순 복사가 아니다(그대로 복사하면 대상 문서의 기존 id를 조용히 가로챈다). 한컴 편집기 실측(팀장, 2026-08-07): "입력→문서 끼워 넣기…" 패널의 4개 고유 옵션(글자 모양 유지·스타일 유지·쪽 모양 유지·문단 모양 유지, 기본값 전부 해제=흡수)이 정책 축의 실측 근거. v2는 이 4축을 한컴과 같은 이름의 매개변수(`keep_character_shape`/`keep_style`/`keep_paragraph_shape`/`keep_page_shape`)로 노출한다 — 기본값은 v1의 기출하·실검증 동작과 동일(글자·스타일·문단 모양=유지, 쪽 모양=흡수), 반대 방향은 매핑 실측이 없어 typed 오류로 정직 거부(과설계 안 함). v2는 MEMO 병합도 지원한다(DEV-042): `hp:memogroup`은 절 레벨 형제라 원본의 모든 절에서 참조된 `hp:memo`를 찾아 복제, 새 메모 id 채번 후 body 문단 복제본과 같은 리스트로 묶어 기존 축1 재매핑(charPr/paraPr/style/memoShapeIDRef)에 그대로 태운다. 실측으로 찾은 결함 8건을 수정(v1 6건 + v2 2건): 스타일 자신의 기본 charPr/paraPr 누락, `HwpxOxmlDocument.char_properties` 캐시 미무효화, `linkListIDRef`/`linkListNextIDRef` 과잉거부(팀장 독립 검증 — 벤더드 코퍼스 5,891/5,891건이 전부 "0" 관용값인데 존재만으로 거부해 표가 든 모든 문서, 우리 자신의 `add_table` 산출물 포함, 병합이 불가능했다), 저장 경로 dirty-tracking 미설정(가장 심각 — `target_header.mark_dirty()`를 호출하지 않아 헤더에 새로 추가한 요소가 저장 시 조용히 누락되는데, 그림이 있는 병합만 우연히 `add_image`의 부수효과로 가려져 있었다), `hp:secPr` 제거가 통째 run을 지워 표·텍스트까지 함께 삭제(실코퍼스 표 문서에서 secPr·표·텍스트가 한 run에 공존, 수정 후 secPr만 개별 제거), 새 id 채번 순서가 프로세스마다 비결정적(`set` 순회를 `sorted()`로 교체), `check_id_integrity` 자신의 `memoShapeIDRef="0"` 오탐(fresh 문서의 secPr 스켈레톤 기본값을 memo_shapes 테이블이 채워지는 순간부터 댕글링으로 오판 — 병합과 무관한 사전 존재 결함, `id_integrity.py`의 `_ALLOWED_SENTINELS`로 수정), `hp:fieldEnd`의 `fieldid` 미재생성(원본 문서의 raw uuid4().hex 값이 복사된 내용에 그대로 남음 — v14 생성기 자체 결정성 검사로 발견, `begin_fieldid_map`으로 수정). `hp:connectLine`(스마트 연결선)·`chartIDRef`·실체인 `linkListIDRef`가 있으면 fail-closed 거부. 참조 무결성은 재발명하지 않고 기존 `hwpx.tools.id_integrity.check_id_integrity`로 검증(설계 문서: `docs/2026-08-08-document-merge-contract.md`). 실한컴 렌더 검증: `docs/openrate/report-v13.json` authored-docmerge 스트라텀(v1, 10건 중 4건이 표 포함 — 위 결함 셋이 전부 표 병합에서 터졌던 형태를 정면으로 겨냥), macOS Hancom GUI 오라클 10/10 render_checked·0 render_failed(2026-08-08 측정). v2(4축 매개변수+MEMO 병합)는 `docs/openrate/report-v14.json` authored-docmerge2 스트라텀(단일 메모/센티널 기본값/대상 기존 memogroup 합류/표 동반 4갈래, 4축 기본값 명시통과 호출), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정)로 검증 완료. 6.15: 클론 경로 감사(속성-클론 중첩 참조 앨리어싱 수리, `b81713e`)의 후속으로 v20(`cross-real-merge`, 우리 자신이 생성한 문서가 아니라 실한컴이 저장한 문서끼리 병합 — v13의 성긴 헤더로는 이 앨리어싱 결함이 애초에 안 보였을 것이라는 지적)을 실행: 실 문서 3종 쌍방향 병합·중간 삽입 3건·필드 보유 소스 2종·3단 연쇄 병합 12건, macOS Hancom GUI 오라클 12/12 render_checked·0 render_failed(2026-08-12 측정) — 앨리어싱 수리 후 재현이 사라졌음을 실 소스로 재확인. 세 버전 모두 같은 스트라텀 그룹(document_merge)이라 원장 라우팅은 여전히 없음(위 산문 근거만) |
61
64
  | 덧말·글자 겹치기 | Parse·Create·Render-verified | `doc.shapes.add_dutmal`/`.add_composed_character`(6.9 트레인㉞). `hp:compose`(글자 겹치기/원문자·합자)는 읽기 모델이 이전 사이클(commit `6f88e2e`)부터 있었으나 저작 API·캐파빌리티 등재가 없었다 — 이번에 `add_composed_character`로 채우고 같은 `body.ComposedCharacter`/`_composed_character_to_xml` 경로를 저작에도 재사용(파싱·저작 한 코드 경로). `hp:dutmal`(덧말)은 이번에 처음 리버스엔지니어링 — 실코퍼스 표본 정확히 1건(`reader_writer__SimpleDutmal.hwpx`, hwpxlib 왕복 픽스처), macOS 편집기 메뉴 전수 스캔(트레인㉙)이 1급 메뉴 항목임은 확인했으나 그 이상의 빈도 근거는 없다 — 정직 고지. 그 1건이 스키마의 두 주장을 직접 반증한다(`option`은 스키마상 `fixed="4"`인데 실측 `"0"`, `szRatio`는 `xs:positiveInteger`인데 실측 `"0"` — DEV-041). 저작 API는 이 실측값을 기본값으로 채택하고 스키마 주장을 검증 규칙으로 강제하지 않는다. 둘 다 `hp:t`의 자식이 아니라 `hp:run` 직속(스키마 문서 순서와 실제 위치가 다름, 실코퍼스로 확인). 실한컴 렌더 검증: `docs/openrate/report-v13.json` authored-dutmal-compose 스트라텀(dutmal 기본값을 실측값 그대로 두고 검증 — 스키마 주장이 아니라 우리 채택을 오라클이 판정), macOS Hancom GUI 오라클 10/10 render_checked·0 render_failed(2026-08-08 측정) — `hp:compose`/`hp:dutmal`/`hp:mainText`/`hp:subText` 4개 요소 전부 원장에 `by-openrate-corpus` 반영(코드-쓰기 분류기가 `_qualified_tag(tag, "literal")`류 관용구를 못 잡던 갭도 이번에 수동 override로 같이 메움) |
62
65
 
@@ -86,6 +89,7 @@
86
89
  | 각주/미주 | `doc.notes` |
87
90
  | 네이티브 목차(TOC)/상호참조 | `doc.refs` |
88
91
  | 차례 숨기기·제목 차례 표시 | `doc.refs` |
92
+ | 색인 표시 | `doc.refs` |
89
93
  | 암호화 HWPX | 미지원 |
90
94
  | HWP 5.x 바이너리 | 미지원 |
91
95
  | 누름틀(form field) 생성 | `doc.fields` |
@@ -96,6 +100,7 @@
96
100
  | 바탕쪽 | `doc.parts`/`doc.page` |
97
101
  | 날짜/시간·교정 부호·파일 이름 필드 | `doc.fields` |
98
102
  | 문서 옵션·호환성 | `doc.parts` |
103
+ | 라이선스 표시(CCL) | `doc.parts` |
99
104
  | 페이지 레이아웃(용지·여백·머리말/꼬리말·쪽번호·단·줄번호·격자·요소 숨김) | `doc.page` |
100
105
  | 문자 서식(굵게·기울임·밑줄·글꼴·크기·색 등) | `doc.styles` |
101
106
  | 목록 서식(글머리표·번호매기기) | `doc.styles` |
@@ -104,6 +109,7 @@
104
109
  | 찾아바꾸기 | `doc.text` |
105
110
  | 하이퍼링크·책갈피 | `doc.refs` |
106
111
  | 메일머지(placeholder 템플릿 배치 생성) | 모듈 — `hwpx.tools.mail_merge` |
112
+ | 메일머지 표시 필드 | `doc.fields` |
107
113
  | 문서 끼워 넣기(문서 병합) | 모듈 — `hwpx.tools.document_merge` |
108
114
  | 덧말·글자 겹치기 | `doc.shapes` |
109
115
 
@@ -200,8 +200,12 @@ def _refresh_copied_paragraph_subtree_ids(paragraph: ET.Element) -> None:
200
200
  }:
201
201
  node.set("id", _object_id())
202
202
 
203
- if "instId" in node.attrib:
204
- node.set("instId", _object_id())
203
+ # 스키마·실한컴 산출물의 속성명은 소문자 "instid"다. 과거 이 코드가
204
+ # 방출한 "instId"(카멜케이스)를 실은 파일도 재발급 대상에 남긴다 —
205
+ # 속성명 자체는 보존(바이트 보수), 값만 재발급.
206
+ for inst_attr in ("instid", "instId"):
207
+ if inst_attr in node.attrib:
208
+ node.set(inst_attr, _object_id())
205
209
 
206
210
 
207
211
  def _clone_paragraph_element(paragraph: ET.Element) -> ET.Element:
@@ -32,7 +32,10 @@ INLINE_OBJECT_NAMES = {
32
32
  "ole",
33
33
  "chart",
34
34
  "video",
35
- "audio",
35
+ # "audio"는 제외 — 실한컴(Windows 한글 2024 실측)은 소리 삽입을
36
+ # hp:ole(EMBEDDED/ICON)로 방출하고 hp:audio를 생성하지 않으며, 보유
37
+ # 실코퍼스 표본도 0건이다. 실물 미검증 요소를 인라인 읽기 지원으로
38
+ # 분류하지 않는다(빈도 0 = 읽기측 불투명 보존만). python-hwpx#89.
36
39
  "textart",
37
40
  }
38
41
 
@@ -60,6 +60,35 @@ GUI 프로브 불필요했다. 구조는 DATE와 동형(단일 run, 캐시 텍
60
60
  **v1 스코프**: ``Command``/``Format`` 둘 다 이 관측값("$F") 하나만
61
61
  지원(typed 거부) -- 포맷 코드 문법을 역산할 근거가 없는 건 DATE의
62
62
  ``Command`` 미리보기 문자열과 같은 사정이다.
63
+
64
+ ## 메일머지 표시 필드(``type="MAILMERGE"``)
65
+
66
+ 실측 gold(``tests/fixtures/gui_probes/mailmerge_display_fields.hwpx``,
67
+ "도구 > 메일 머지 > 메일 머지 표시 달기"로 필드 2개를 삽입)가 위 세
68
+ 필드와 다른 점 셋:
69
+
70
+ * ``hp:fieldBegin``에 ``metaTag`` 속성이 **없다**(DATE/교정 부호/PATH
71
+ gold는 셋 다 ``metaTag=""``를 달고 있다) -- ``_field_begin``의
72
+ *meta_tag* 인자가 그 차이만 담당한다.
73
+ * ``hp:parameters``(``cnt="5"``)의 첫 파라미터가 ``hp:booleanParam``이고
74
+ 이름이 ``"Fiexde"``다 -- **오타 철자가 실물·한컴 공식 문서 공통의
75
+ 정본**이라 고쳐 쓰면 안 된다(``trackchageConfig``·memo의 ``NOMAL``과
76
+ 같은 부류). 나머지는 ``Prop=8``(DATE/PATH와 같은 "캐시 필드"류 공유
77
+ 값) · ``Command``/``FieldValue``가 둘 다 필드 이름 그대로 ·
78
+ ``FieldType="USER_DEFINE"``.
79
+ * ``ctrl(end)`` 뒤에 **빈 ``hp:t``가 하나 더** 붙는다(gold 2/2 필드 모두).
80
+ 용도를 역산할 근거는 없으나 실측이 일관되므로 그대로 재현한다 --
81
+ 캐시 텍스트를 넣는 ``hp:t``와 달리 내용이 없다.
82
+
83
+ 캐시 텍스트 기본값은 ``"{{이름}}"``(gold 실측 관행, ``hwpx.tools.
84
+ mail_merge``가 치환 대상으로 인식하는 세 문법 중 하나) -- DATE/PATH와
85
+ 달리 여기선 우리가 계산할 수 있는 값이라(필드 이름만으로 결정된다)
86
+ 호출자에게 떠넘기지 않는다.
87
+
88
+ ``fieldid``: gold의 두 필드는 ``id``가 서로 다른데 ``fieldid``는 같은
89
+ 값을 공유한다(``627928423``). 표본이 문서 하나뿐이라 이게 "필드 종류당
90
+ 1개"인지 "문서당 1개"인지 역산할 근거가 없어, DATE/PATH와 같은
91
+ 독립 난수 채번을 그대로 쓴다(실한컴도 ``id``≠``fieldid``인 건 같다).
63
92
  """
64
93
 
65
94
  from __future__ import annotations
@@ -80,6 +109,7 @@ __all__ = [
80
109
  "PROOFREADING_MARKS",
81
110
  "PATH_FORMATS",
82
111
  "create_date_field",
112
+ "create_mail_merge_field",
83
113
  "create_proofreading_mark_field",
84
114
  "create_path_field",
85
115
  ]
@@ -104,7 +134,9 @@ PATH_FORMATS: dict[str, str] = {
104
134
  }
105
135
 
106
136
 
107
- def _field_begin(field_type: str) -> tuple[ET.Element, ET.Element, str, str]:
137
+ def _field_begin(
138
+ field_type: str, *, meta_tag: bool = True
139
+ ) -> tuple[ET.Element, ET.Element, str, str]:
108
140
  """``hp:ctrl > hp:fieldBegin`` -- 관측된 고정 속성 그대로.
109
141
 
110
142
  Returns ``(ctrl, fieldBegin, field_id, field_ref_id)`` -- *field_id*는
@@ -112,11 +144,15 @@ def _field_begin(field_type: str) -> tuple[ET.Element, ET.Element, str, str]:
112
144
  ``fieldid``, ``id``와는 독립 난수)는 ``fieldEnd``의 ``fieldid``에
113
145
  그대로 쓴다(둘 다 실측 gold에 있는 속성 -- 값이 다른데도 fieldEnd가
114
146
  양쪽을 다 반복해서 갖는다).
147
+
148
+ *meta_tag*: ``metaTag=""``를 다는지. DATE/교정 부호/PATH gold는 셋 다
149
+ 달고 있고 MAILMERGE gold는 없다 -- 필드 종류마다 실측이 갈리는
150
+ 유일한 속성이라 여기서만 분기한다(모듈 독스트링 참조).
115
151
  """
116
152
  field_id = _object_id()
117
153
  field_ref_id = _object_id()
118
154
  ctrl = ET.Element(f"{_HP}ctrl")
119
- begin = _append_child(ctrl, f"{_HP}fieldBegin", {
155
+ attributes = {
120
156
  "id": field_id,
121
157
  "type": field_type,
122
158
  "name": "",
@@ -124,8 +160,10 @@ def _field_begin(field_type: str) -> tuple[ET.Element, ET.Element, str, str]:
124
160
  "dirty": "0",
125
161
  "zorder": "-1",
126
162
  "fieldid": field_ref_id,
127
- "metaTag": "",
128
- })
163
+ }
164
+ if meta_tag:
165
+ attributes["metaTag"] = ""
166
+ begin = _append_child(ctrl, f"{_HP}fieldBegin", attributes)
129
167
  return ctrl, begin, field_id, field_ref_id
130
168
 
131
169
 
@@ -242,6 +280,55 @@ def create_path_field(
242
280
  return [ctrl_begin, text_el, ctrl_end]
243
281
 
244
282
 
283
+ def create_mail_merge_field(
284
+ name: str,
285
+ *,
286
+ cached_text: str | None = None,
287
+ ) -> list[ET.Element]:
288
+ """메일머지 표시 필드(``type="MAILMERGE"``) 요소열을 만든다.
289
+
290
+ *name*은 데이터 열 이름("도구 > 메일 머지 > 메일 머지 표시 달기"
291
+ 대화상자의 필드 이름)이고, ``Command``/``FieldValue`` 두 파라미터에
292
+ 그대로 실린다. *cached_text*를 생략하면 ``"{{name}}"`` --
293
+ ``hwpx.tools.mail_merge.merge_template_rows``가 치환 대상으로 인식하는
294
+ 플레이스홀더 문법이라, 이 필드로 저작한 템플릿이 그 배치 생성기에
295
+ 그대로 물린다.
296
+
297
+ 반환값은 한 run에 순서대로 append할 요소 리스트
298
+ (``[ctrl(begin), t, ctrl(end), t(빈 것)]``) -- 마지막 빈 ``hp:t``까지
299
+ 실측 gold 그대로다(모듈 독스트링 참조).
300
+ """
301
+ if not name:
302
+ from ..errors import HwpxValueError
303
+
304
+ raise HwpxValueError(
305
+ "mail merge field name must be a non-empty string",
306
+ code="field-mail-merge-empty-name",
307
+ context={"name": name},
308
+ suggestion='데이터 열 이름을 주세요 -- 예: add_mail_merge_field("name").',
309
+ )
310
+
311
+ ctrl_begin, begin, field_id, field_ref_id = _field_begin("MAILMERGE", meta_tag=False)
312
+ params = _append_child(begin, f"{_HP}parameters", {"cnt": "5", "name": ""})
313
+ # "Fiexde"는 실물·한컴 공식 문서 공통의 오타 철자다 -- 고치지 않는다.
314
+ fixed = _append_child(params, f"{_HP}booleanParam", {"name": "Fiexde"})
315
+ fixed.text = "1"
316
+ prop = _append_child(params, f"{_HP}integerParam", {"name": "Prop"})
317
+ prop.text = "8"
318
+ command = _append_child(params, f"{_HP}stringParam", {"name": "Command"})
319
+ command.text = _sanitize_text(name)
320
+ field_type = _append_child(params, f"{_HP}stringParam", {"name": "FieldType"})
321
+ field_type.text = "USER_DEFINE"
322
+ field_value = _append_child(params, f"{_HP}stringParam", {"name": "FieldValue"})
323
+ field_value.text = _sanitize_text(name)
324
+
325
+ text_el = ET.Element(f"{_HP}t")
326
+ text_el.text = _sanitize_text(cached_text if cached_text is not None else f"{{{{{name}}}}}")
327
+
328
+ ctrl_end = _field_end(field_id, field_ref_id)
329
+ return [ctrl_begin, text_el, ctrl_end, ET.Element(f"{_HP}t")]
330
+
331
+
245
332
  def _append_field_elements(
246
333
  paragraph: "HwpxOxmlParagraph",
247
334
  elements: list[ET.Element],
@@ -308,3 +395,15 @@ def _paragraph_add_path_field(
308
395
  """파일 이름 필드를 이 문단에 삽입한다."""
309
396
  elements = create_path_field(cached_text, path_format=path_format)
310
397
  return _append_field_elements(self, elements, char_pr_id_ref=char_pr_id_ref)
398
+
399
+
400
+ def _paragraph_add_mail_merge_field(
401
+ self: "HwpxOxmlParagraph",
402
+ name: str,
403
+ *,
404
+ cached_text: str | None = None,
405
+ char_pr_id_ref: str | int | None = None,
406
+ ) -> "HwpxOxmlInlineObject":
407
+ """메일머지 표시 필드를 이 문단에 삽입한다."""
408
+ elements = create_mail_merge_field(name, cached_text=cached_text)
409
+ return _append_field_elements(self, elements, char_pr_id_ref=char_pr_id_ref)
@@ -34,7 +34,20 @@ class LinkInfo:
34
34
 
35
35
  @dataclass(slots=True)
36
36
  class LicenseMark:
37
- type: int
37
+ """``hh:docOption/hh:licensemark`` — 문서 수준 라이선스 레코드.
38
+
39
+ ``type``이 ``str``인 건 실측이다: 스키마(``Header XML schema.xml``의
40
+ ``DocOptionType``)는 ``xs:unsignedInt use="required"``라고 선언하지만
41
+ 실한컴(HWP 13.0.0.3901)이 실제로 쓰는 값은 ``"CCL"``이라는 문자열이다
42
+ (gold ``tests/fixtures/gui_probes/license_mark_ccl.hwpx``). 이전
43
+ ``int`` 선언은 스키마만 보고 정한 것이라, 실한컴이 만든 CCL 문서를
44
+ ``to_model()``로 읽으면 ``ValueError: Invalid integer value: 'CCL'``로
45
+ 터졌다 — 저작 쪽(``header_compat.set_license_mark``)을 열면서 실측으로
46
+ 드러났다. 실측 우선 원칙(DEV-043과 같은 부류)에 따라 값은 그대로
47
+ 보존한다. ``flag``/``lang``은 관측값이 ``"0"``/``"6"``이라 정수 그대로.
48
+ """
49
+
50
+ type: str
38
51
  flag: int
39
52
  lang: Optional[int]
40
53
 
@@ -989,8 +1002,12 @@ def parse_link_info(node: etree._Element) -> LinkInfo:
989
1002
 
990
1003
 
991
1004
  def parse_license_mark(node: etree._Element) -> LicenseMark:
1005
+ # 빠진 ``type``은 같은 ``DocOptionType`` 안에서 ``parse_link_info``가
1006
+ # 스키마-required인 ``path``를 다루는 방식 그대로 빈 문자열로 둔다
1007
+ # (문자열 속성은 이 모듈에서 raise 대상이 아니다 -- 정수 변환이
1008
+ # 필요한 ``flag``/``lang``만 ``parse_int``가 검사한다).
992
1009
  return LicenseMark(
993
- type=parse_int(node.get("type"), allow_none=False),
1010
+ type=node.get("type", ""),
994
1011
  flag=parse_int(node.get("flag"), allow_none=False),
995
1012
  lang=parse_int(node.get("lang")),
996
1013
  )
@@ -66,9 +66,11 @@ if TYPE_CHECKING:
66
66
 
67
67
  __all__ = [
68
68
  "apply_paragraph_auto_spacing",
69
+ "remove_license_mark",
69
70
  "set_compatible_document_target_program",
70
71
  "set_doc_option_link_info",
71
72
  "set_layout_compatibility_flags",
73
+ "set_license_mark",
72
74
  ]
73
75
 
74
76
 
@@ -146,6 +148,32 @@ def set_layout_compatibility_flags(header: HwpxOxmlHeader, flags: Iterable[str])
146
148
  header.mark_dirty()
147
149
 
148
150
 
151
+ def _doc_option_element(header: HwpxOxmlHeader) -> ET.Element:
152
+ """``hh:docOption``(+스키마 필수인 ``hh:linkinfo`` 첫 자식)을 보장하고
153
+ 돌려준다. ``linkinfo``는 ``minOccurs`` 기본값이라 필수인데, 코퍼스
154
+ 47/47이 ``path=""``/``pageInherit="0"``/``footnoteInherit="0"``이므로
155
+ 새로 만들 때 그 값으로 채운다."""
156
+
157
+ root = header.element
158
+ doc_option = HwpxOxmlHeader._direct_child_by_local(root, "docOption")
159
+ if doc_option is None:
160
+ doc_option = root.makeelement(f"{_HH}docOption", {})
161
+ HwpxOxmlHeader._insert_child_after(
162
+ root,
163
+ doc_option,
164
+ {"beginNum", "refList", "forbiddenWordList", "compatibleDocument"},
165
+ )
166
+ if HwpxOxmlHeader._direct_child_by_local(doc_option, "linkinfo") is None:
167
+ doc_option.insert(
168
+ 0,
169
+ doc_option.makeelement(
170
+ f"{_HH}linkinfo",
171
+ {"path": "", "pageInherit": "0", "footnoteInherit": "0"},
172
+ ),
173
+ )
174
+ return doc_option
175
+
176
+
149
177
  def set_doc_option_link_info(
150
178
  header: HwpxOxmlHeader,
151
179
  *,
@@ -157,22 +185,9 @@ def set_doc_option_link_info(
157
185
  ``footnoteInherit`` attributes. ``None`` leaves an attribute unchanged
158
186
  (or defaults it to the corpus-majority value on first creation)."""
159
187
 
160
- root = header.element
161
- doc_option = HwpxOxmlHeader._direct_child_by_local(root, "docOption")
162
- if doc_option is None:
163
- doc_option = root.makeelement(f"{_HH}docOption", {})
164
- HwpxOxmlHeader._insert_child_after(
165
- root,
166
- doc_option,
167
- {"beginNum", "refList", "forbiddenWordList", "compatibleDocument"},
168
- )
188
+ doc_option = _doc_option_element(header)
169
189
  link_info = HwpxOxmlHeader._direct_child_by_local(doc_option, "linkinfo")
170
- if link_info is None:
171
- link_info = doc_option.makeelement(
172
- f"{_HH}linkinfo",
173
- {"path": "", "pageInherit": "0", "footnoteInherit": "0"},
174
- )
175
- doc_option.append(link_info)
190
+ assert link_info is not None # _doc_option_element guarantees it
176
191
  if path is not None:
177
192
  link_info.set("path", path)
178
193
  if page_inherit is not None:
@@ -182,6 +197,80 @@ def set_doc_option_link_info(
182
197
  header.mark_dirty()
183
198
 
184
199
 
200
+ def set_license_mark(
201
+ header: HwpxOxmlHeader,
202
+ *,
203
+ mark_type: str,
204
+ flag: int,
205
+ lang: int | None = None,
206
+ ) -> None:
207
+ """Set ``hh:docOption/hh:licensemark`` -- the document-level licence
208
+ record "입력 > CCL 넣기…" writes.
209
+
210
+ Real-Hancom contract (gold ``tests/fixtures/gui_probes/
211
+ license_mark_ccl.hwpx``, HWP 13.0.0.3901): the element is a
212
+ ``hh:linkinfo`` sibling, second child of ``hh:docOption``, and reads
213
+ ``<hh:licensemark type="CCL" flag="0" lang="6"/>``.
214
+
215
+ **``type`` is a string, not a number.** The schema (``Header XML
216
+ schema.xml``'s ``DocOptionType``) declares ``type`` as
217
+ ``xs:unsignedInt use="required"`` -- real Hancom emits ``"CCL"``. Same
218
+ measurement-over-schema precedent as ``field_marks``'s
219
+ ``PROOFREADING_MARKS_SIGN`` (DEV-043), so *mark_type* is typed ``str``
220
+ and passed through verbatim.
221
+
222
+ *flag*/*lang* are ints (``xs:byte`` in the schema, observed ``0``/``6``
223
+ -- 6 is the language code for this document's UI locale). No value
224
+ range is enforced beyond that: one observation is not an enumeration,
225
+ and ``set_compatible_document_target_program`` already established that
226
+ this module does not invent restrictions it has no evidence for.
227
+ Omitting *lang* omits the attribute (the schema marks it optional; the
228
+ other two are required and always written).
229
+
230
+ This setter writes the **document-level record only**. The visible CC
231
+ badge in the gold is an ordinary picture with a licence-deed URL
232
+ (``hp:pic`` + ``href``) -- author it with ``add_picture`` plus that
233
+ ``href``, exactly as for any other linked image.
234
+ """
235
+
236
+ if not mark_type:
237
+ raise HwpxValueError(
238
+ "mark_type must be a non-empty string",
239
+ code="header-compat-empty-license-mark-type",
240
+ context={"mark_type": mark_type},
241
+ suggestion='Pass the observed real-Hancom value, e.g. "CCL".',
242
+ )
243
+
244
+ doc_option = _doc_option_element(header)
245
+ license_mark = HwpxOxmlHeader._direct_child_by_local(doc_option, "licensemark")
246
+ if license_mark is None:
247
+ license_mark = doc_option.makeelement(f"{_HH}licensemark", {})
248
+ doc_option.append(license_mark)
249
+ license_mark.set("type", mark_type)
250
+ license_mark.set("flag", str(int(flag)))
251
+ if lang is None:
252
+ license_mark.attrib.pop("lang", None)
253
+ else:
254
+ license_mark.set("lang", str(int(lang)))
255
+ header.mark_dirty()
256
+
257
+
258
+ def remove_license_mark(header: HwpxOxmlHeader) -> bool:
259
+ """Drop ``hh:docOption/hh:licensemark`` if present; returns whether one
260
+ was removed. ``hh:docOption`` itself stays (its ``hh:linkinfo`` child is
261
+ schema-required and unrelated to the licence record)."""
262
+
263
+ doc_option = HwpxOxmlHeader._direct_child_by_local(header.element, "docOption")
264
+ if doc_option is None:
265
+ return False
266
+ license_mark = HwpxOxmlHeader._direct_child_by_local(doc_option, "licensemark")
267
+ if license_mark is None:
268
+ return False
269
+ doc_option.remove(license_mark)
270
+ header.mark_dirty()
271
+ return True
272
+
273
+
185
274
  def apply_paragraph_auto_spacing(
186
275
  header: HwpxOxmlHeader,
187
276
  para_pr: ET.Element,
@@ -277,7 +277,9 @@ class HwpxOxmlNote:
277
277
 
278
278
  @property
279
279
  def inst_id(self) -> str | None:
280
- return self.element.get("instId")
280
+ # 정본 속성명은 "instid"(스키마·실한컴). "instId"는 과거 우리 저작이
281
+ # 방출한 카멜케이스 산출물 호환용 폴백.
282
+ return self.element.get("instid") or self.element.get("instId")
281
283
 
282
284
  @property
283
285
  def text(self) -> str:
@@ -128,7 +128,7 @@ def _paragraph_add_note(
128
128
  {
129
129
  "number": str(number),
130
130
  "suffixChar": str(ord(suffix[0])) if suffix else "41",
131
- "instId": _object_id(),
131
+ "instid": _object_id(),
132
132
  },
133
133
  )
134
134
  sublist_attrs = _default_sublist_attributes()