python-hwpx-automation 7.3.3__tar.gz → 7.4.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 (235) hide show
  1. {python_hwpx_automation-7.3.3/src/python_hwpx_automation.egg-info → python_hwpx_automation-7.4.0}/PKG-INFO +3 -3
  2. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/README.md +2 -2
  3. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/pyproject.toml +1 -1
  4. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/search.py +13 -10
  5. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/identity.json +2 -2
  6. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/commands.py +36 -1
  7. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/composer.py +51 -0
  8. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/harvest.py +44 -2
  9. python_hwpx_automation-7.4.0/src/hwpx_automation/office/authoring/design/marked.py +273 -0
  10. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/plan.py +3 -0
  11. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/wordbox.py +29 -4
  12. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/_render_hwpx_mac.applescript +89 -4
  13. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/cli.py +10 -0
  14. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/mac_session.py +20 -0
  15. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/oracle.py +119 -4
  16. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/public-modules.json +5 -4
  17. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0/src/python_hwpx_automation.egg-info}/PKG-INFO +3 -3
  18. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/python_hwpx_automation.egg-info/SOURCES.txt +1 -0
  19. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/LICENSE +0 -0
  20. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/MANIFEST.in +0 -0
  21. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/NOTICE +0 -0
  22. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/setup.cfg +0 -0
  23. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/__init__.py +0 -0
  24. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/__init__.pyi +0 -0
  25. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/__main__.py +0 -0
  26. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/agent_document.py +0 -0
  27. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/api.py +0 -0
  28. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/blind_eval.py +0 -0
  29. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/capabilities.py +0 -0
  30. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/compat.py +0 -0
  31. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/configuration.py +0 -0
  32. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/__init__.py +0 -0
  33. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/content.py +0 -0
  34. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/context.py +0 -0
  35. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/diff.py +0 -0
  36. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/document.py +0 -0
  37. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/formatting.py +0 -0
  38. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/handles.py +0 -0
  39. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/locations.py +0 -0
  40. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/locator.py +0 -0
  41. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/plan.py +0 -0
  42. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/resources.py +0 -0
  43. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/transactions.py +0 -0
  44. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/core/txn.py +0 -0
  45. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/document_state.py +0 -0
  46. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/errors.py +0 -0
  47. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/execution_lock.py +0 -0
  48. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/fastmcp_adapter.py +0 -0
  49. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/form_fill.py +0 -0
  50. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/form_output_models.py +0 -0
  51. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/__init__.py +0 -0
  52. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/_shared.py +0 -0
  53. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/agent_document.py +0 -0
  54. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/authoring.py +0 -0
  55. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/content_edit.py +0 -0
  56. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/form_fill.py +0 -0
  57. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/layout_style.py +0 -0
  58. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/quality_render.py +0 -0
  59. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/read_export.py +0 -0
  60. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/specialized.py +0 -0
  61. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/tracked_changes.py +0 -0
  62. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/handlers/workflow.py +0 -0
  63. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/hwp_converter.py +0 -0
  64. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/hwp_support.py +0 -0
  65. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/hwpx_ops.py +0 -0
  66. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/identity.py +0 -0
  67. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ingest_adapters.py +0 -0
  68. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/markdown_plan.py +0 -0
  69. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/mcp_cli.py +0 -0
  70. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/metadata/tools_meta.py +0 -0
  71. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/mixed_form.py +0 -0
  72. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/mutation_models.py +0 -0
  73. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/network_policy.py +0 -0
  74. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/__init__.py +0 -0
  75. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/__init__.py +0 -0
  76. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/_batch_publication.py +0 -0
  77. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/_batch_verification.py +0 -0
  78. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/_text_scope.py +0 -0
  79. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/__init__.py +0 -0
  80. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/bundle.py +0 -0
  81. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/catalog.py +0 -0
  82. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/dump.py +0 -0
  83. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/mapping.py +0 -0
  84. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/model.py +0 -0
  85. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/native.py +0 -0
  86. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/blueprint/replay.py +0 -0
  87. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/catalog.py +0 -0
  88. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/cli.py +0 -0
  89. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/document.py +0 -0
  90. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/form_plan.py +0 -0
  91. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/model.py +0 -0
  92. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/path.py +0 -0
  93. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/query.py +0 -0
  94. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/agent/story.py +0 -0
  95. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/__init__.py +0 -0
  96. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/advanced_generators.py +0 -0
  97. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/builder/__init__.py +0 -0
  98. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/builder/core.py +0 -0
  99. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/builder/report.py +0 -0
  100. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/__init__.py +0 -0
  101. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/_support.py +0 -0
  102. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profile.py +0 -0
  103. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +0 -0
  104. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +0 -0
  105. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +0 -0
  106. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +0 -0
  107. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/profile.json +0 -0
  108. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  109. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +0 -0
  110. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +0 -0
  111. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +0 -0
  112. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +0 -0
  113. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  114. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +0 -0
  115. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +0 -0
  116. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +0 -0
  117. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +0 -0
  118. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +0 -0
  119. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  120. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +0 -0
  121. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +0 -0
  122. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +0 -0
  123. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +0 -0
  124. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/profile.json +0 -0
  125. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  126. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/design/validator.py +0 -0
  127. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/presets/__init__.py +0 -0
  128. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/presets/proposal.py +0 -0
  129. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/report_parser.py +0 -0
  130. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/style_profile.py +0 -0
  131. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/authoring/template_analyzer.py +0 -0
  132. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/charting/__init__.py +0 -0
  133. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/compliance/__init__.py +0 -0
  134. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/compliance/official_lint.py +0 -0
  135. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/compliance/pii.py +0 -0
  136. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/document_ops/__init__.py +0 -0
  137. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/document_ops/comparison.py +0 -0
  138. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/document_ops/mail_merge.py +0 -0
  139. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/document_ops/redline.py +0 -0
  140. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/evalplan/__init__.py +0 -0
  141. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/evalplan/runtime.py +0 -0
  142. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/__init__.py +0 -0
  143. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/compose.py +0 -0
  144. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/ir.py +0 -0
  145. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/measure.py +0 -0
  146. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/parser.py +0 -0
  147. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/exam/profile.py +0 -0
  148. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/__init__.py +0 -0
  149. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/classification.py +0 -0
  150. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fill_residue.py +0 -0
  151. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/__init__.py +0 -0
  152. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/apply.py +0 -0
  153. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/engine.py +0 -0
  154. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/measure.py +0 -0
  155. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/policy.py +0 -0
  156. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/report.py +0 -0
  157. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/fit/seal.py +0 -0
  158. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/guidance.py +0 -0
  159. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/quality.py +0 -0
  160. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/split_run.py +0 -0
  161. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/form_fill/template_formfit.py +0 -0
  162. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/__init__.py +0 -0
  163. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/composition.py +0 -0
  164. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/data/bank.json +0 -0
  165. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/data/genres.json +0 -0
  166. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/gianmun.py +0 -0
  167. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/house_style/org_chart.py +0 -0
  168. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/quality/__init__.py +0 -0
  169. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/quality/page_guard.py +0 -0
  170. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/__init__.py +0 -0
  171. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/__main__.py +0 -0
  172. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/_hancom_open_rate.ps1 +0 -0
  173. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +0 -0
  174. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/_render_hwpx.ps1 +0 -0
  175. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/block_splits.py +0 -0
  176. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/detectors.py +0 -0
  177. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/diff.py +0 -0
  178. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/fixture_corpus.py +0 -0
  179. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/page_qa.py +0 -0
  180. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/qa_contracts.py +0 -0
  181. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/qa_metrics.py +0 -0
  182. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/rendering/worker.py +0 -0
  183. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/utilities/__init__.py +0 -0
  184. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/office/utilities/table_compute.py +0 -0
  185. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/__init__.py +0 -0
  186. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/_border_fill.py +0 -0
  187. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/composition.py +0 -0
  188. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/content_layout.py +0 -0
  189. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/context.py +0 -0
  190. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/form_fields.py +0 -0
  191. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/media.py +0 -0
  192. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/memo_style.py +0 -0
  193. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/package_validation.py +0 -0
  194. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/planning.py +0 -0
  195. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/preview_export.py +0 -0
  196. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/read_query.py +0 -0
  197. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/save_policy.py +0 -0
  198. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/tables.py +0 -0
  199. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/ops_services/transactions.py +0 -0
  200. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/preview_output_models.py +0 -0
  201. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/py.typed +0 -0
  202. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/quality.py +0 -0
  203. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/quality_generation.py +0 -0
  204. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/resources.py +0 -0
  205. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/runtime.py +0 -0
  206. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/runtime_services.py +0 -0
  207. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/server.py +0 -0
  208. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/storage.py +0 -0
  209. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/tool_bindings.py +0 -0
  210. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/tool_contract.py +0 -0
  211. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/upstream.py +0 -0
  212. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/utils/__init__.py +0 -0
  213. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/utils/helpers.py +0 -0
  214. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/utils/read_budget.py +0 -0
  215. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/visual_qa.py +0 -0
  216. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/__init__.py +0 -0
  217. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/adapters.py +0 -0
  218. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/dispatcher.py +0 -0
  219. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/evidence_lineage.py +0 -0
  220. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/models.py +0 -0
  221. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/policy.py +0 -0
  222. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/render_contracts.py +0 -0
  223. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/render_metrics.py +0 -0
  224. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/render_queue.py +0 -0
  225. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/render_security.py +0 -0
  226. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/render_transport.py +0 -0
  227. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/rendering.py +0 -0
  228. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/service.py +0 -0
  229. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/state_machine.py +0 -0
  230. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workflow/store.py +0 -0
  231. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/hwpx_automation/workspace.py +0 -0
  232. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/python_hwpx_automation.egg-info/dependency_links.txt +0 -0
  233. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/python_hwpx_automation.egg-info/entry_points.txt +0 -0
  234. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/python_hwpx_automation.egg-info/requires.txt +0 -0
  235. {python_hwpx_automation-7.3.3 → python_hwpx_automation-7.4.0}/src/python_hwpx_automation.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx-automation
3
- Version: 7.3.3
3
+ Version: 7.4.0
4
4
  Summary: Task-oriented HWPX document automation for Python, with an optional MCP adapter.
5
5
  Author: Kohkyuhyun
6
6
  License-Expression: Apache-2.0
@@ -74,7 +74,7 @@ Dynamic: license-file
74
74
 
75
75
  <!-- release-state: release-approved -->
76
76
  > [!NOTE]
77
- > 발행이 승인된 7.3.3 후보 (release-approved; 원격 발행 대기): `python-hwpx 6.6.0 → python-hwpx-automation 7.3.3 → hwpx-plugin 2.4.0` (`5e5c23651f92785a`).
77
+ > 발행이 승인된 7.4.0 후보 (release-approved; 원격 발행 대기): `python-hwpx 6.6.0 → python-hwpx-automation 7.4.0 → hwpx-plugin 2.4.0` (`5e5c23651f92785a`).
78
78
  > 공개 트레인: `python-hwpx 6.6.0 → python-hwpx-automation 7.3.1 → hwpx-plugin 2.4.0`.
79
79
  > 공개 좌표는 core·automation PyPI와 plugin GitHub
80
80
  > Release·marketplace·실제 marketplace 설치 관찰 후에만 승격합니다.
@@ -162,7 +162,7 @@ PDF와 쪽 PNG로 렌더합니다. 별도 환경에서 실행하므로 호출하
162
162
  고정 버전을 건드리지 않습니다.
163
163
 
164
164
  ```bash
165
- uvx --from "python-hwpx-automation[oracle]==7.3.3" \
165
+ uvx --from "python-hwpx-automation[oracle]==7.4.0" \
166
166
  hwpx render-pdf in.hwpx out.pdf --png out/page --json
167
167
  ```
168
168
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  <!-- release-state: release-approved -->
16
16
  > [!NOTE]
17
- > 발행이 승인된 7.3.3 후보 (release-approved; 원격 발행 대기): `python-hwpx 6.6.0 → python-hwpx-automation 7.3.3 → hwpx-plugin 2.4.0` (`5e5c23651f92785a`).
17
+ > 발행이 승인된 7.4.0 후보 (release-approved; 원격 발행 대기): `python-hwpx 6.6.0 → python-hwpx-automation 7.4.0 → hwpx-plugin 2.4.0` (`5e5c23651f92785a`).
18
18
  > 공개 트레인: `python-hwpx 6.6.0 → python-hwpx-automation 7.3.1 → hwpx-plugin 2.4.0`.
19
19
  > 공개 좌표는 core·automation PyPI와 plugin GitHub
20
20
  > Release·marketplace·실제 marketplace 설치 관찰 후에만 승격합니다.
@@ -102,7 +102,7 @@ PDF와 쪽 PNG로 렌더합니다. 별도 환경에서 실행하므로 호출하
102
102
  고정 버전을 건드리지 않습니다.
103
103
 
104
104
  ```bash
105
- uvx --from "python-hwpx-automation[oracle]==7.3.3" \
105
+ uvx --from "python-hwpx-automation[oracle]==7.4.0" \
106
106
  hwpx render-pdf in.hwpx out.pdf --png out/page --json
107
107
  ```
108
108
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx-automation"
7
- version = "7.3.3"
7
+ version = "7.4.0"
8
8
  description = "Task-oriented HWPX document automation for Python, with an optional MCP adapter."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Kohkyuhyun" }]
@@ -93,11 +93,16 @@ def _replace_in_runs(runs: list[Any], find_text: str, replace_text: str) -> int:
93
93
  if not runs:
94
94
  return 0
95
95
 
96
- replaced = _replace_within_runs(runs, find_text, replace_text)
97
- merged_after_simple = "".join(run.text or "" for run in runs)
98
- if find_text in merged_after_simple:
99
- replaced += _replace_across_runs(runs, find_text, replace_text)
100
- return replaced
96
+ # Decide from the text before any replacement: re-scanning afterwards would find
97
+ # find_text again inside replace_text when the one contains the other.
98
+ if not _has_match_across_runs([run.text or "" for run in runs], find_text):
99
+ return _replace_within_runs(runs, find_text, replace_text)
100
+ return _replace_across_runs(runs, find_text, replace_text)
101
+
102
+
103
+ def _has_match_across_runs(texts: list[str], find_text: str) -> bool:
104
+ """Whether some occurrence of *find_text* in the joined text spans two runs."""
105
+ return "".join(texts).count(find_text) != sum(text.count(find_text) for text in texts)
101
106
 
102
107
 
103
108
  def _replace_within_xml_runs(run_elements: list[Any], find_text: str, replace_text: str) -> int:
@@ -134,11 +139,9 @@ def _replace_in_xml_runs(run_elements: list[Any], find_text: str, replace_text:
134
139
  if not run_elements:
135
140
  return 0
136
141
 
137
- replaced = _replace_within_xml_runs(run_elements, find_text, replace_text)
138
- merged_after_simple = "".join(_xml_run_text(run_element) for run_element in run_elements)
139
- if find_text in merged_after_simple:
140
- replaced += _replace_across_xml_runs(run_elements, find_text, replace_text)
141
- return replaced
142
+ if not _has_match_across_runs([_xml_run_text(run_element) for run_element in run_elements], find_text):
143
+ return _replace_within_xml_runs(run_elements, find_text, replace_text)
144
+ return _replace_across_xml_runs(run_elements, find_text, replace_text)
142
145
 
143
146
 
144
147
  def find_in_doc(doc: Any, text_to_find: str, match_case: bool = True, max_results: int = 50) -> dict:
@@ -7,9 +7,9 @@
7
7
  "candidate": {
8
8
  "pythonHwpx": "6.6.0",
9
9
  "canonicalDistribution": "python-hwpx-automation",
10
- "canonicalAutomation": "7.3.3",
10
+ "canonicalAutomation": "7.4.0",
11
11
  "compatibilityDistribution": "hwpx-mcp-server",
12
- "compatibility": "7.3.3",
12
+ "compatibility": "7.4.0",
13
13
  "plugin": "2.4.0",
14
14
  "contractHash": "5e5c23651f92785a"
15
15
  },
@@ -438,6 +438,32 @@ def _insert_direct_child(
438
438
  container.insert(list(container).index(siblings[index]), element)
439
439
 
440
440
 
441
+ def _drop_layout_cache(paragraph_element: Any | None) -> None:
442
+ """Remove a paragraph's line layout cache (``hp:linesegarray``) after its content changed.
443
+
444
+ The cache records where each line starts in the paragraph's content. Once an object or a run has left or
445
+ joined the paragraph, the cache no longer matches: Hancom reports a document whose cached lines point past
446
+ the remaining content as damaged, while without the cache it lays the paragraph out again. Only the
447
+ paragraph that changed is touched; every other paragraph keeps the cache Hancom wrote. ``None`` (an
448
+ element no paragraph holds) changes nothing.
449
+ """
450
+
451
+ if paragraph_element is None:
452
+ return
453
+ for child in list(paragraph_element):
454
+ if _local_name(child).lower() == "linesegarray":
455
+ paragraph_element.remove(child)
456
+
457
+
458
+ def _paragraph_element_of(element: Any) -> Any | None:
459
+ """The nearest ``hp:p`` holding *element*, or ``None`` when it is detached."""
460
+
461
+ current = element
462
+ while current is not None and _local_name(current) != "p":
463
+ current = current.getparent() if hasattr(current, "getparent") else None
464
+ return current
465
+
466
+
441
467
  def _insert_inline(
442
468
  view: HwpxAgentDocument,
443
469
  parent: NodeRecord,
@@ -468,6 +494,7 @@ def _insert_inline(
468
494
  if _local_name(anchor_run) == "ctrl":
469
495
  anchor_run = anchor_run.getparent()
470
496
  paragraph.element.insert(list(paragraph.element).index(anchor_run), run)
497
+ _drop_layout_cache(paragraph.element)
471
498
  paragraph.section.mark_dirty()
472
499
  return run
473
500
 
@@ -482,6 +509,7 @@ def _remove_inline_element(element: Any, paragraph: Any) -> None:
482
509
  run.remove(control)
483
510
  if not list(run) and run in list(paragraph.element):
484
511
  paragraph.element.remove(run)
512
+ _drop_layout_cache(paragraph.element)
485
513
  paragraph.section.mark_dirty()
486
514
 
487
515
 
@@ -834,7 +862,8 @@ def _add(
834
862
  if position["mode"] != "append":
835
863
  parent.native.element.remove(created.element)
836
864
  _insert_direct_child(view, parent, kind, created.element, position)
837
- parent.native.section.mark_dirty()
865
+ _drop_layout_cache(parent.native.element)
866
+ parent.native.section.mark_dirty()
838
867
  return created
839
868
  if kind == "table":
840
869
  created = parent.native.add_table(
@@ -896,7 +925,9 @@ def _remove(document: HwpxDocument, record: NodeRecord) -> None:
896
925
  if record.kind == "paragraph":
897
926
  native.remove()
898
927
  elif record.kind == "run":
928
+ paragraph_element = native.paragraph.element
899
929
  native.remove()
930
+ _drop_layout_cache(paragraph_element)
900
931
  elif record.kind == "table":
901
932
  _remove_inline_element(native.element, native.paragraph)
902
933
  elif record.kind == "row":
@@ -914,6 +945,7 @@ def _remove(document: HwpxDocument, record: NodeRecord) -> None:
914
945
  if parent is None:
915
946
  raise AgentContractError("not_found", "picture is detached", target=record.path)
916
947
  parent.remove(_element(record.native))
948
+ _drop_layout_cache(_paragraph_element_of(parent))
917
949
  _mark_containing_section(document, parent)
918
950
  elif record.kind == "shape":
919
951
  _remove_inline_element(native.element, native.paragraph)
@@ -934,6 +966,7 @@ def _detach(view: HwpxAgentDocument, record: NodeRecord) -> Any:
934
966
  record.native.section.mark_dirty()
935
967
  elif record.kind == "run":
936
968
  record.native.paragraph.element.remove(element)
969
+ _drop_layout_cache(record.native.paragraph.element)
937
970
  record.native.paragraph.section.mark_dirty()
938
971
  elif record.kind in _INLINE_KINDS:
939
972
  paragraph = record.native.paragraph if hasattr(record.native, "paragraph") else None
@@ -1002,6 +1035,7 @@ def _move(
1002
1035
  if source.kind == "paragraph":
1003
1036
  parent.native.mark_dirty()
1004
1037
  elif source.kind == "run":
1038
+ _drop_layout_cache(parent.native.element)
1005
1039
  parent.native.section.mark_dirty()
1006
1040
  else:
1007
1041
  _refresh_table_rows(source.native.table)
@@ -1157,6 +1191,7 @@ def _copy_node(
1157
1191
  if source.kind == "paragraph":
1158
1192
  parent.native.mark_dirty()
1159
1193
  elif source.kind == "run":
1194
+ _drop_layout_cache(parent.native.element)
1160
1195
  parent.native.section.mark_dirty()
1161
1196
  else:
1162
1197
  _refresh_table_rows(parent.native)
@@ -22,6 +22,7 @@ from hwpx.document import HwpxDocument
22
22
  from hwpx.quality import QualityPolicy
23
23
 
24
24
  import hwpx_automation.office.authoring.design._support as S
25
+ from .marked import OBJECT_TAGS, fill_placeholders, fragment_paragraphs, is_marked_profile
25
26
  from .plan import Block, DocumentPlan
26
27
  from .profile import Profile, load_profile
27
28
  from .validator import StyleCoverage, style_coverage
@@ -82,7 +83,11 @@ def compose(
82
83
  secpr = deepcopy(secpr)
83
84
 
84
85
  new_paragraphs: list[etree._Element] = []
86
+ marked = is_marked_profile(prof.manifest)
85
87
  for block in plan.iter_blocks():
88
+ if marked:
89
+ new_paragraphs.extend(_marked_fragment_for(prof, block, production, warnings))
90
+ continue
86
91
  frag = _fragment_for(prof, block, production, warnings)
87
92
  if frag is None:
88
93
  continue
@@ -96,6 +101,7 @@ def compose(
96
101
  S.move_secpr_into(new_paragraphs[0], secpr)
97
102
  S.replace_section_body(section.element, new_paragraphs)
98
103
  _reassign_paragraph_ids(new_paragraphs)
104
+ _reassign_object_ids(new_paragraphs)
99
105
  section.mark_dirty()
100
106
 
101
107
  coverage = style_coverage(doc, new_paragraphs)
@@ -188,6 +194,35 @@ def _fragment_for(
188
194
  return frag
189
195
 
190
196
 
197
+ def _marked_fragment_for(
198
+ prof: Profile, block: Block, production: bool, warnings: list[str]
199
+ ) -> list[etree._Element]:
200
+ """Clone the author-named role and fill its slots (marked-template profiles)."""
201
+
202
+ if not prof.has_role(block.role):
203
+ _forbid_fallback(
204
+ production, f"marked profile {prof.id!r} has no role {block.role!r}", warnings
205
+ )
206
+ return []
207
+ values = dict(block.values)
208
+ slots = prof.manifest.get("slots", {}).get(block.role, [])
209
+ # Plain ``text`` fills the unnamed slot, or the only slot a role has.
210
+ if block.text and "" in slots and "" not in values:
211
+ values[""] = block.text
212
+ elif block.text and len(slots) == 1 and slots[0] not in values:
213
+ values[slots[0]] = block.text
214
+ paragraphs = fragment_paragraphs(prof.fragment(block.role))
215
+ unfilled = fill_placeholders(paragraphs, block.role, values)
216
+ if not values:
217
+ # A block with no text and no values asks for the role as is (a page
218
+ # break line, a separator) -- blank slots are what it wants.
219
+ return paragraphs
220
+ for slot in unfilled:
221
+ name = f"{block.role}.{slot}" if slot else block.role
222
+ warnings.append(f"MARKED_SLOT_EMPTY: no value for {{{{{name}}}}}; left blank")
223
+ return paragraphs
224
+
225
+
191
226
  def _forbid_fallback(production: bool, message: str, warnings: list[str]) -> None:
192
227
  if production:
193
228
  raise ProfileRequiredError("PROFILE_REQUIRED: " + message)
@@ -279,4 +314,20 @@ def _reassign_paragraph_ids(paragraphs: list[etree._Element]) -> None:
279
314
  para.set("id", str(uuid4().int % (2**31)))
280
315
 
281
316
 
317
+ # Composed bodies are made only of clones, so object ids restart from a fixed
318
+ # base — deterministic output, and no clone shares the id of another.
319
+ _OBJECT_ID_BASE = 1_000_000_000
320
+
321
+
322
+ def _reassign_object_ids(paragraphs: list[etree._Element]) -> None:
323
+ """Give every table/drawing object a unique id (fragments may repeat)."""
324
+
325
+ next_id = _OBJECT_ID_BASE
326
+ for root in paragraphs:
327
+ for element in root.iter():
328
+ if S.ln(element.tag) in OBJECT_TAGS and element.get("id") is not None:
329
+ element.set("id", str(next_id))
330
+ next_id += 1
331
+
332
+
282
333
  __all__ = ["compose", "compose_bytes", "ComposeResult", "ProfileRequiredError"]
@@ -3,8 +3,8 @@
3
3
 
4
4
  Given a genuine ``.hwpx`` it writes, under ``profiles/<id>/``:
5
5
 
6
- * ``template.hwpx`` — a **body-stripped skeleton**: the real document with its
7
- section body reduced to a single ``secPr``-carrying paragraph. ``header.xml``
6
+ * ``template.hwpx`` — a **body-stripped skeleton**: the real document reduced to
7
+ its first section, whose body is a single ``secPr``-carrying paragraph. ``header.xml``
8
8
  (every style/font/borderFill) and the page setup survive untouched, so the
9
9
  committed artifact is small and carries no document content (no PII), yet is
10
10
  genuinely Hancom-derived — never imagined XML.
@@ -160,6 +160,9 @@ def _subList(tc: etree._Element):
160
160
 
161
161
  def _build_skeleton(source: Path, out_path: Path) -> dict:
162
162
  doc = HwpxDocument.open(source)
163
+ # Only the first section's page setup is reused; later sections would ride
164
+ # into every composed document with the source's text.
165
+ _drop_later_sections(doc)
163
166
  section = doc.sections[0]
164
167
  secpr = S.find_secpr(section.element)
165
168
  if secpr is None:
@@ -199,6 +202,26 @@ def _build_skeleton(source: Path, out_path: Path) -> dict:
199
202
  }
200
203
 
201
204
 
205
+ def _drop_later_sections(doc) -> None:
206
+ """Remove every section after the first, part and manifest item included.
207
+
208
+ Older cores remove the section from the document tree but keep its part,
209
+ which would still be saved with the source text in it.
210
+ """
211
+
212
+ dropped = [section.part_name for section in doc.sections[1:]]
213
+ for extra in reversed(doc.sections[1:]):
214
+ doc.remove_section(extra)
215
+ package = doc._package
216
+ for item in list(package._manifest_items()):
217
+ href = item.get("href", "") or ""
218
+ if any(name.endswith(href) for name in dropped if href) and item.get("id"):
219
+ package.remove_manifest_item(item.get("id"))
220
+ for name in dropped:
221
+ if package.has_part(name):
222
+ package.delete(name)
223
+
224
+
202
225
  def _strip_binaries(doc) -> list[str]:
203
226
  """Drop embedded images + the source preview (size + PII) from the skeleton.
204
227
 
@@ -209,6 +232,10 @@ def _strip_binaries(doc) -> list[str]:
209
232
 
210
233
  package = doc._package
211
234
  removed: list[str] = []
235
+ # Image fills defined in header.xml (borderFill imgBrush) are styles, not
236
+ # body content: dropping their binaries would leave dangling references.
237
+ kept_ids = _header_binary_refs(package)
238
+ kept_parts: set[str] = set()
212
239
 
213
240
  for item in list(package._manifest_items()):
214
241
  href = item.get("href", "") or ""
@@ -220,10 +247,15 @@ def _strip_binaries(doc) -> list[str]:
220
247
  or media.lower().startswith("image/")
221
248
  ):
222
249
  item_id = item.get("id")
250
+ if item_id in kept_ids:
251
+ kept_parts.add(href)
252
+ continue
223
253
  if item_id:
224
254
  package.remove_manifest_item(item_id)
225
255
 
226
256
  for name in list(package.part_names()):
257
+ if any(name.endswith(href) for href in kept_parts):
258
+ continue
227
259
  if name.startswith("BinData/") or name.startswith("Preview/PrvImage"):
228
260
  try:
229
261
  package.delete(name)
@@ -236,6 +268,16 @@ def _strip_binaries(doc) -> list[str]:
236
268
  return removed
237
269
 
238
270
 
271
+ def _header_binary_refs(package) -> set[str]:
272
+ refs: set[str] = set()
273
+ for path in package.header_paths():
274
+ for node in package.get_xml(path).iter():
275
+ for key, value in node.attrib.items():
276
+ if S.ln(key) == "binaryItemIDRef":
277
+ refs.add(value)
278
+ return refs
279
+
280
+
239
281
  # Manifest (content.hpf) metadata that carries source PII — author, the person who
240
282
  # last saved, the source title, and timestamps. Cleared so generated docs inherit
241
283
  # nothing identifying.
@@ -0,0 +1,273 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Marked-template profiles — fragments named by the template author.
3
+
4
+ :func:`~hwpx_automation.office.authoring.design.harvest.harvest_profile` guesses
5
+ roles from font size and keeps a single info table, so it cannot tell a coloured
6
+ chapter-band table from a data table, or one bullet level from another. Here the
7
+ author decides instead: in a Hancom-saved template they type placeholders where
8
+ content goes —
9
+
10
+ * ``□ {{1단계}}`` — a paragraph role ``1단계`` with one unnamed slot;
11
+ * ``{{띠.번호}}`` / ``{{띠.제목}}`` in the cells of a band table — a role ``띠``
12
+ with the slots ``번호`` and ``제목``.
13
+
14
+ Every top-level paragraph that carries placeholders becomes one role. Text-less
15
+ paragraphs directly above it (the spacer lines Hancom users put between items)
16
+ belong to the same role, so spacing is cloned with it. Fills, borders, fonts and
17
+ indents are never interpreted: the composer clones the saved XML and swaps only
18
+ the placeholder text.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import re
24
+ from copy import deepcopy
25
+ from pathlib import Path
26
+ from typing import Mapping
27
+
28
+ from lxml import etree # pyright: ignore[reportAttributeAccessIssue]
29
+
30
+ from hwpx.document import HwpxDocument
31
+
32
+ import hwpx_automation.office.authoring.design._support as S
33
+ from .harvest import PROFILE_SCHEMA, _build_skeleton, _references_binary
34
+
35
+ MARKED_PROFILE_KIND = "marked"
36
+ # Wrapper for a multi-paragraph fragment file; never written into a document.
37
+ FRAGMENT_WRAPPER_TAG = "fragment"
38
+
39
+ PLACEHOLDER_RE = re.compile(r"\{\{\s*([^{}.\s]+)(?:\.([^{}\s]+))?\s*\}\}")
40
+
41
+ # Objects whose ``id`` must stay unique when a fragment is cloned more than once.
42
+ OBJECT_TAGS = frozenset(
43
+ {
44
+ "tbl", "pic", "rect", "ellipse", "arc", "polygon", "curve", "line",
45
+ "connectLine", "container", "ole", "equation", "textart",
46
+ }
47
+ )
48
+
49
+
50
+ class MarkedTemplateError(ValueError):
51
+ """The template's placeholders cannot be turned into a profile."""
52
+
53
+
54
+ def is_marked_profile(manifest: Mapping[str, object]) -> bool:
55
+ return manifest.get("kind") == MARKED_PROFILE_KIND
56
+
57
+
58
+ def _text_pieces(element: etree._Element) -> list[tuple[etree._Element, str]]:
59
+ """(owner, attr) pairs for every text piece of every ``<hp:t>`` under *element*.
60
+
61
+ ``attr`` is ``"text"`` for ``t.text`` and ``"tail"`` for the tail of an inline
62
+ child (tab, line break) inside the ``<hp:t>``.
63
+ """
64
+
65
+ pieces: list[tuple[etree._Element, str]] = []
66
+ for t in S.iter_local(element, "t"):
67
+ pieces.append((t, "text"))
68
+ for child in t:
69
+ pieces.append((child, "tail"))
70
+ return pieces
71
+
72
+
73
+ def _get(piece: tuple[etree._Element, str]) -> str:
74
+ owner, attr = piece
75
+ return (owner.text if attr == "text" else owner.tail) or ""
76
+
77
+
78
+ def _set(piece: tuple[etree._Element, str], value: str) -> None:
79
+ owner, attr = piece
80
+ if attr == "text":
81
+ owner.text = value
82
+ else:
83
+ owner.tail = value
84
+
85
+
86
+ def placeholders_in(element: etree._Element) -> list[tuple[str, str]]:
87
+ """(role, slot) for every placeholder under *element*; slot ``""`` if unnamed."""
88
+
89
+ found: list[tuple[str, str]] = []
90
+ for para in S.iter_local(element, "p"):
91
+ joined = "".join(_get(p) for p in _direct_pieces(para))
92
+ found.extend((m.group(1), m.group(2) or "") for m in PLACEHOLDER_RE.finditer(joined))
93
+ return list(dict.fromkeys(found))
94
+
95
+
96
+ def _direct_pieces(paragraph: etree._Element) -> list[tuple[etree._Element, str]]:
97
+ """Text pieces of *paragraph* itself, excluding paragraphs nested in its tables."""
98
+
99
+ pieces: list[tuple[etree._Element, str]] = []
100
+ for run in S.children_local(paragraph, "run"):
101
+ for t in S.children_local(run, "t"):
102
+ pieces.append((t, "text"))
103
+ for child in t:
104
+ pieces.append((child, "tail"))
105
+ return pieces
106
+
107
+
108
+ def _is_blank(paragraph: etree._Element) -> bool:
109
+ if any(_get(p).strip() for p in _text_pieces(paragraph)):
110
+ return False
111
+ for node in paragraph.iter():
112
+ if S.ln(node.tag) in OBJECT_TAGS:
113
+ return False
114
+ return True
115
+
116
+
117
+ def _strip_section_setup(fragment: etree._Element) -> None:
118
+ """Drop the page setup a template's first paragraph carries (secPr + colPr ctrl)."""
119
+
120
+ for run in S.children_local(fragment, "run"):
121
+ for secpr in S.children_local(run, "secPr"):
122
+ run.remove(secpr)
123
+ for ctrl in S.children_local(run, "ctrl"):
124
+ if any(S.ln(c.tag) == "colPr" for c in ctrl):
125
+ run.remove(ctrl)
126
+
127
+
128
+ def fill_placeholders(
129
+ paragraphs: list[etree._Element], role: str, values: Mapping[str, str]
130
+ ) -> list[str]:
131
+ """Replace ``{{role}}``/``{{role.slot}}`` text in place; return unfilled slots.
132
+
133
+ A placeholder inside one text piece keeps that piece's run (and its style).
134
+ One that Hancom split across runs is rebuilt in the paragraph's first piece.
135
+ Missing values become empty text so a placeholder never ships.
136
+ """
137
+
138
+ unfilled: list[str] = []
139
+
140
+ def replace(match: re.Match[str]) -> str:
141
+ if match.group(1) != role:
142
+ return match.group(0)
143
+ slot = match.group(2) or ""
144
+ if slot not in values:
145
+ unfilled.append(slot)
146
+ return ""
147
+ return str(values[slot])
148
+
149
+ for root in paragraphs:
150
+ for para in S.iter_local(root, "p"):
151
+ pieces = _direct_pieces(para)
152
+ for piece in pieces:
153
+ text = _get(piece)
154
+ if "{{" in text:
155
+ _set(piece, PLACEHOLDER_RE.sub(replace, text))
156
+ joined = "".join(_get(p) for p in pieces)
157
+ if any(m.group(1) == role for m in PLACEHOLDER_RE.finditer(joined)):
158
+ _set(pieces[0], PLACEHOLDER_RE.sub(replace, joined))
159
+ for piece in pieces[1:]:
160
+ _set(piece, "")
161
+ S.strip_lineseg(root)
162
+ return list(dict.fromkeys(unfilled))
163
+
164
+
165
+ def _pick_marked_fragments(
166
+ section_element: etree._Element,
167
+ ) -> tuple[dict[str, list[etree._Element]], dict[str, list[str]], int]:
168
+ fragments: dict[str, list[etree._Element]] = {}
169
+ slots: dict[str, list[str]] = {}
170
+ pending: list[etree._Element] = []
171
+ ignored = 0
172
+ for para in S.children_local(section_element, "p"):
173
+ marks = placeholders_in(para)
174
+ if not marks:
175
+ if _is_blank(para):
176
+ pending.append(para)
177
+ else:
178
+ ignored += 1 # static content outside any role
179
+ pending = []
180
+ continue
181
+ roles = list(dict.fromkeys(role for role, _ in marks))
182
+ if len(roles) > 1:
183
+ raise MarkedTemplateError(
184
+ f"one paragraph carries placeholders of several roles {roles}; "
185
+ "put each role in its own paragraph or table"
186
+ )
187
+ role = roles[0]
188
+ if role in fragments:
189
+ raise MarkedTemplateError(f"role {role!r} appears more than once in the template")
190
+ group = [deepcopy(p) for p in (*pending, para)]
191
+ pending = []
192
+ for frag in group:
193
+ if _references_binary(frag):
194
+ raise MarkedTemplateError(
195
+ f"role {role!r} contains an image or embedded object; "
196
+ "fragments must be text, tables and shapes only"
197
+ )
198
+ _strip_section_setup(frag)
199
+ S.strip_lineseg(frag)
200
+ fragments[role] = group
201
+ slots[role] = [slot for _, slot in marks]
202
+ if not fragments:
203
+ raise MarkedTemplateError("template has no {{placeholder}} paragraphs")
204
+ return fragments, slots, ignored
205
+
206
+
207
+ def harvest_marked_profile(source: str | Path, profile_id: str, out_root: str | Path) -> dict:
208
+ """Harvest a placeholder-marked template into ``<out_root>/<profile_id>/``.
209
+
210
+ Only the first section is read. Returns the written manifest plus
211
+ ``skeleton_open_safe`` and ``stripped_parts`` diagnostics.
212
+ """
213
+
214
+ source = Path(source)
215
+ out_dir = Path(out_root) / profile_id
216
+ (out_dir / "fragments").mkdir(parents=True, exist_ok=True)
217
+
218
+ doc = HwpxDocument.open(source)
219
+ try:
220
+ fragments, slots, ignored = _pick_marked_fragments(doc.sections[0].element)
221
+ finally:
222
+ doc.close()
223
+
224
+ paths: dict[str, str] = {}
225
+ for index, (role, group) in enumerate(fragments.items()):
226
+ wrapper = etree.Element(FRAGMENT_WRAPPER_TAG)
227
+ wrapper.extend(group)
228
+ rel = f"fragments/{index:02d}.xml"
229
+ (out_dir / rel).write_bytes(etree.tostring(wrapper, encoding="UTF-8"))
230
+ paths[role] = rel
231
+
232
+ skel = _build_skeleton(source, out_dir / "template.hwpx")
233
+ manifest = {
234
+ "schemaVersion": PROFILE_SCHEMA,
235
+ "kind": MARKED_PROFILE_KIND,
236
+ "id": profile_id,
237
+ "template": "template.hwpx",
238
+ "fragments": paths,
239
+ "slots": slots,
240
+ "page": skel["page"],
241
+ "char_pr_count": skel["char_pr_count"],
242
+ "style_coverage_threshold": 0.98,
243
+ "source_basename": source.name,
244
+ "ignored_static_paragraphs": ignored,
245
+ }
246
+ (out_dir / "profile.json").write_text(
247
+ json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
248
+ )
249
+ return {
250
+ **manifest,
251
+ "skeleton_open_safe": skel["open_safe"],
252
+ "stripped_parts": skel.get("stripped_parts", []),
253
+ }
254
+
255
+
256
+ def fragment_paragraphs(fragment: etree._Element) -> list[etree._Element]:
257
+ """The ``<hp:p>`` list a (possibly wrapped) fragment element stands for."""
258
+
259
+ if fragment.tag == FRAGMENT_WRAPPER_TAG:
260
+ return list(fragment)
261
+ return [fragment]
262
+
263
+
264
+ __all__ = [
265
+ "MARKED_PROFILE_KIND",
266
+ "PLACEHOLDER_RE",
267
+ "MarkedTemplateError",
268
+ "harvest_marked_profile",
269
+ "fill_placeholders",
270
+ "placeholders_in",
271
+ "fragment_paragraphs",
272
+ "is_marked_profile",
273
+ ]
@@ -26,6 +26,8 @@ class Block:
26
26
  text: str = ""
27
27
  columns: list[str] = field(default_factory=list)
28
28
  rows: list[list[str]] = field(default_factory=list)
29
+ #: Slot values for a marked-template role (``{{role.slot}}`` → value).
30
+ values: dict[str, str] = field(default_factory=dict)
29
31
 
30
32
  @classmethod
31
33
  def from_dict(cls, data: dict[str, Any]) -> "Block":
@@ -35,6 +37,7 @@ class Block:
35
37
  text=str(data.get("text", "")),
36
38
  columns=[str(c) for c in data.get("columns", [])],
37
39
  rows=[[str(c) for c in row] for row in data.get("rows", [])],
40
+ values={str(k): str(v) for k, v in (data.get("values") or {}).items()},
38
41
  )
39
42
 
40
43