python-hwpx 5.6.0__tar.gz → 5.8.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.
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/NOTICE +8 -0
- {python_hwpx-5.6.0/src/python_hwpx.egg-info → python_hwpx-5.8.0}/PKG-INFO +9 -4
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/README.md +9 -4
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/pyproject.toml +1 -1
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/fields.py +115 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/capabilities.py +151 -2
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/support-matrix.md +3 -2
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/document.py +62 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/layout/report.py +11 -2
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/paragraph.py +85 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/patch.py +22 -1
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/report.py +34 -2
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/save_pipeline.py +21 -4
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/table_patch.py +1 -1
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/idempotence.py +7 -5
- {python_hwpx-5.6.0 → python_hwpx-5.8.0/src/python_hwpx.egg-info}/PKG-INFO +9 -4
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_capabilities_surface.py +55 -0
- python_hwpx-5.8.0/tests/test_check_box_authoring.py +154 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/LICENSE +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/setup.cfg +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/authoring.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/package.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/plan/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/plan/_execute.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/plan/_model.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/plan/_schema.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/plan/_validate.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_add_control.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_authoring_defaults.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_chart_authoring.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_contract_docs_sync.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_edit_plan.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_equation_authoring.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_ingest.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_llms_txt.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_new_features.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_note_authoring_contract.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.6.0 → python_hwpx-5.8.0}/tests/test_version_metadata.py +0 -0
|
@@ -58,6 +58,14 @@ neolord0/hwpxlib (https://github.com/neolord0/hwpxlib), licensed Apache-2.0.
|
|
|
58
58
|
Only sample DATA files are vendored; no hwpxlib source code or structure is
|
|
59
59
|
copied (clean-room). Pinned ref is recorded in manifest.json.
|
|
60
60
|
|
|
61
|
+
## Hancom file-format specification attribution
|
|
62
|
+
|
|
63
|
+
Parts of this project were developed with reference to the publicly
|
|
64
|
+
released Hancom file-format documentation (HWP 5.0 / HWPML) and the
|
|
65
|
+
public OWPML schema. As required by that documentation:
|
|
66
|
+
|
|
67
|
+
“본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다.”
|
|
68
|
+
|
|
61
69
|
────────────────────────────────────────
|
|
62
70
|
DISCLAIMER — HWPX Format
|
|
63
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -158,10 +158,15 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
158
158
|
|
|
159
159
|
## 실측으로 말합니다
|
|
160
160
|
|
|
161
|
-
만든 파일이 실제 한컴오피스에서
|
|
162
|
-
|
|
161
|
+
만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
|
|
162
|
+
스택과 날짜를 병기합니다:
|
|
163
163
|
|
|
164
|
-
- **한컴 열림
|
|
164
|
+
- **한컴 열림 120/120 · 렌더 검증 120/120** — 현행 스택(5.7.0 · 실한컴
|
|
165
|
+
12.0.0.3288 · 2026-08-03)의 축소 측정: 기준 스트라텀 + 신규 저작 표면
|
|
166
|
+
6종(각주·누름틀·수식·차트·체크박스·편집 계획). 영수증은 행마다 bucket이
|
|
167
|
+
있어 `jq` 한 줄로 재현됩니다
|
|
168
|
+
- **한컴 열림 476/476** — 전수 동결 코퍼스(N=497) 측정
|
|
169
|
+
(3.4.1 · 실한컴 12.0.0.3288 · 2026-07-19)
|
|
165
170
|
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
166
171
|
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
167
172
|
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
@@ -107,10 +107,15 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
107
107
|
|
|
108
108
|
## 실측으로 말합니다
|
|
109
109
|
|
|
110
|
-
만든 파일이 실제 한컴오피스에서
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- **한컴 열림
|
|
110
|
+
만든 파일이 실제 한컴오피스에서 열리는지 재서 그대로 공개합니다. 측정마다
|
|
111
|
+
스택과 날짜를 병기합니다:
|
|
112
|
+
|
|
113
|
+
- **한컴 열림 120/120 · 렌더 검증 120/120** — 현행 스택(5.7.0 · 실한컴
|
|
114
|
+
12.0.0.3288 · 2026-08-03)의 축소 측정: 기준 스트라텀 + 신규 저작 표면
|
|
115
|
+
6종(각주·누름틀·수식·차트·체크박스·편집 계획). 영수증은 행마다 bucket이
|
|
116
|
+
있어 `jq` 한 줄로 재현됩니다
|
|
117
|
+
- **한컴 열림 476/476** — 전수 동결 코퍼스(N=497) 측정
|
|
118
|
+
(3.4.1 · 실한컴 12.0.0.3288 · 2026-07-19)
|
|
114
119
|
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
115
120
|
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
116
121
|
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.8.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -386,6 +386,121 @@ def add_form_field(
|
|
|
386
386
|
)
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
_CHECK_BOX_TAG = f"{_HP}checkBtn"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _iter_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
|
|
393
|
+
"""Every ``<hp:checkBtn>`` in document order, with its owning element."""
|
|
394
|
+
|
|
395
|
+
found: list[dict[str, Any]] = []
|
|
396
|
+
index = 0
|
|
397
|
+
for section_index, section in enumerate(doc.sections):
|
|
398
|
+
for element in section.element.iter(_CHECK_BOX_TAG):
|
|
399
|
+
found.append(
|
|
400
|
+
{
|
|
401
|
+
"index": index,
|
|
402
|
+
"sectionIndex": section_index,
|
|
403
|
+
"name": element.get("name", ""),
|
|
404
|
+
"caption": element.get("caption", ""),
|
|
405
|
+
"checked": element.get("value", "").upper() == "CHECKED",
|
|
406
|
+
"value": element.get("value", ""),
|
|
407
|
+
"_element": element,
|
|
408
|
+
"_section": section,
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
index += 1
|
|
412
|
+
return found
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def list_check_boxes(doc: "HwpxDocument") -> list[dict[str, Any]]:
|
|
416
|
+
"""Return check-box form objects in document order."""
|
|
417
|
+
|
|
418
|
+
return [
|
|
419
|
+
{key: value for key, value in match.items() if not key.startswith("_")}
|
|
420
|
+
for match in _iter_check_boxes(doc)
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def add_check_box(
|
|
425
|
+
doc: "HwpxDocument",
|
|
426
|
+
caption: str,
|
|
427
|
+
*,
|
|
428
|
+
checked: bool = False,
|
|
429
|
+
name: str | None = None,
|
|
430
|
+
paragraph: Any | None = None,
|
|
431
|
+
section: Any | None = None,
|
|
432
|
+
section_index: int | None = None,
|
|
433
|
+
) -> dict[str, Any]:
|
|
434
|
+
"""Create a check-box form object and return its payload.
|
|
435
|
+
|
|
436
|
+
The emitted XML follows the check-box contract measured against real
|
|
437
|
+
Hancom (specs/060): ``value`` selects ☑/□ and the ``<hp:formCharPr>`` child
|
|
438
|
+
is mandatory — without it Hancom refuses the document. The created object is
|
|
439
|
+
re-read through the standard :func:`list_check_boxes` reader, so creation
|
|
440
|
+
fails loudly if the ordinary consumer would not see it.
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
if not str(caption).strip():
|
|
444
|
+
raise ValueError("check box caption must be a non-empty string")
|
|
445
|
+
if paragraph is None:
|
|
446
|
+
paragraph = doc.add_paragraph(
|
|
447
|
+
"", section=section, section_index=section_index, include_run=False,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
control = paragraph.add_check_box(caption, checked=checked, name=name)
|
|
451
|
+
_clear_form_field_layout_cache(paragraph.element)
|
|
452
|
+
|
|
453
|
+
created_name = control.element.get("name", "")
|
|
454
|
+
for match in _iter_check_boxes(doc):
|
|
455
|
+
if match["_element"] is control.element:
|
|
456
|
+
return {
|
|
457
|
+
key: value for key, value in match.items() if not key.startswith("_")
|
|
458
|
+
}
|
|
459
|
+
raise RuntimeError(
|
|
460
|
+
f"created check box {created_name!r} was not recognized by the standard reader"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def set_check_box(
|
|
465
|
+
doc: "HwpxDocument",
|
|
466
|
+
checked: bool,
|
|
467
|
+
*,
|
|
468
|
+
index: int | None = None,
|
|
469
|
+
name: str | None = None,
|
|
470
|
+
) -> dict[str, Any]:
|
|
471
|
+
"""Set a check box's state, selecting it by ``index`` or ``name``.
|
|
472
|
+
|
|
473
|
+
Exactly one selector is required — an ambiguous or missing selector is a
|
|
474
|
+
typed refusal, never a guess.
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
if (index is None) == (name is None):
|
|
478
|
+
raise ValueError("provide exactly one of index or name")
|
|
479
|
+
matches = _iter_check_boxes(doc)
|
|
480
|
+
if index is not None:
|
|
481
|
+
chosen = [m for m in matches if m["index"] == index]
|
|
482
|
+
if not chosen:
|
|
483
|
+
raise ValueError(f"check box index not found: {index}")
|
|
484
|
+
else:
|
|
485
|
+
wanted = str(name).strip()
|
|
486
|
+
chosen = [m for m in matches if m["name"] == wanted]
|
|
487
|
+
if not chosen:
|
|
488
|
+
raise ValueError(f"check box name not found: {wanted}")
|
|
489
|
+
if len(chosen) > 1:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"check box name is ambiguous ({len(chosen)} matches): {wanted}"
|
|
492
|
+
)
|
|
493
|
+
match = chosen[0]
|
|
494
|
+
match["_element"].set("value", "CHECKED" if checked else "UNCHECKED")
|
|
495
|
+
match["_section"].mark_dirty()
|
|
496
|
+
return {
|
|
497
|
+
key: value
|
|
498
|
+
for key, value in {**match, "checked": bool(checked),
|
|
499
|
+
"value": "CHECKED" if checked else "UNCHECKED"}.items()
|
|
500
|
+
if not key.startswith("_")
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
389
504
|
def _select_form_field(
|
|
390
505
|
doc: "HwpxDocument",
|
|
391
506
|
matches: Sequence[dict[str, Any]],
|
|
@@ -8,8 +8,17 @@
|
|
|
8
8
|
1. 여기 등재된 진입점은 전부 실제로 import 가능해야 한다.
|
|
9
9
|
2. ``editPlanOps``는 :data:`hwpx.plan.PLAN_OPS`·실행 디스패치·edit-plan JSON
|
|
10
10
|
Schema enum과 일치해야 한다.
|
|
11
|
-
3. :data:`_CAPABILITY_AREAS`의
|
|
12
|
-
|
|
11
|
+
3. :data:`_CAPABILITY_AREAS`의 ``authoring_methods``는 라이브
|
|
12
|
+
``HwpxDocument``의 ``add_*`` 전수와 정확히 일치해야 한다 — **레지스트리를
|
|
13
|
+
코드에 대조하는 방향**이다.
|
|
14
|
+
4. 매트릭스 행 제목은 ``docs/support-matrix.md`` 표의 행 제목과 집합 일치해야
|
|
15
|
+
한다.
|
|
16
|
+
|
|
17
|
+
3번이 있는 이유: 4번만으로는 부족하다. 문서 둘이 서로 맞는다는 사실은 코드에
|
|
18
|
+
대해 아무것도 말해주지 않으므로, **양쪽에 다 없는** 능력은 통과한다. 실제로
|
|
19
|
+
``add_check_box``가 5.7.0에서 실한컴 수용 게이트까지 통과해 출하됐는데
|
|
20
|
+
레지스트리에도 매트릭스에도 없었다 — 이 부류의 드리프트를 "구조적으로
|
|
21
|
+
막는다"고 선언한 바로 다음 릴리스에서다. 코드에 대조해야 잡힌다.
|
|
13
22
|
|
|
14
23
|
**core는 환경 변수를 읽지 않는다** — 렌더 오라클 가용성은 여기 없다(자기서술은
|
|
15
24
|
``renderOracle.bundled="none"``으로 그 사실 자체를 말한다). 오라클 탐지·실행은
|
|
@@ -39,21 +48,39 @@ _EXTRA_MODULES: dict[str, tuple[str, ...]] = {
|
|
|
39
48
|
#: 능력 영역 레지스트리. ``matrix_row``는 ``docs/support-matrix.md`` 표 1열
|
|
40
49
|
#: 제목과 **정확히** 일치해야 한다(가드 테스트가 집합 대조). ``entry_points``는
|
|
41
50
|
#: ``"모듈:이름"`` — 전부 import 가능해야 한다(가드 테스트가 해석).
|
|
51
|
+
#: Every ``HwpxDocument.add_*`` method must be claimed by exactly one area.
|
|
52
|
+
#:
|
|
53
|
+
#: ``entry_points`` names modules, which is too coarse to notice a new
|
|
54
|
+
#: authoring method: ten areas all point at ``hwpx.document:HwpxDocument``. So
|
|
55
|
+
#: ``authoring_methods`` names the methods, and the guard in
|
|
56
|
+
#: ``tests/test_capabilities_surface.py`` compares this registry against the
|
|
57
|
+
#: live class.
|
|
58
|
+
#:
|
|
59
|
+
#: Why the guard runs in that direction: the previous guard compared the
|
|
60
|
+
#: registry's row titles against the row titles in ``docs/support-matrix.md``.
|
|
61
|
+
#: Two documents agreeing with each other says nothing about the code, so a
|
|
62
|
+
#: capability missing from *both* passed. ``add_check_box`` shipped in 5.7.0
|
|
63
|
+
#: with a real-Hancom acceptance gate and appeared in neither, one release
|
|
64
|
+
#: after the changelog announced that this class of drift was now structurally
|
|
65
|
+
#: blocked. Comparing against the class is the direction that can catch it.
|
|
42
66
|
_CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
43
67
|
{
|
|
44
68
|
"area": "paragraph-table-authoring",
|
|
45
69
|
"matrix_row": "문단·표 저작/편집",
|
|
46
70
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
71
|
+
"authoring_methods": ("add_paragraph", "add_section"),
|
|
47
72
|
},
|
|
48
73
|
{
|
|
49
74
|
"area": "table-structure",
|
|
50
75
|
"matrix_row": "표 구조 변경(행·열·표 삭제/삽입, 열 오토핏)",
|
|
51
76
|
"entry_points": ("hwpx.table_patch:apply_table_ops",),
|
|
77
|
+
"authoring_methods": (),
|
|
52
78
|
},
|
|
53
79
|
{
|
|
54
80
|
"area": "table-create",
|
|
55
81
|
"matrix_row": "표 생성(병합·중첩 포함)",
|
|
56
82
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
83
|
+
"authoring_methods": ("add_table",),
|
|
57
84
|
},
|
|
58
85
|
{
|
|
59
86
|
"area": "form-fill",
|
|
@@ -63,6 +90,7 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
63
90
|
"hwpx.table_patch:fill_cells",
|
|
64
91
|
"hwpx.body_patch:apply_body_ops",
|
|
65
92
|
),
|
|
93
|
+
"authoring_methods": (),
|
|
66
94
|
},
|
|
67
95
|
{
|
|
68
96
|
"area": "edit-plan",
|
|
@@ -71,71 +99,91 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
71
99
|
"hwpx.plan:apply_edit_plan",
|
|
72
100
|
"hwpx.plan:validate_edit_plan",
|
|
73
101
|
),
|
|
102
|
+
"authoring_methods": (),
|
|
74
103
|
},
|
|
75
104
|
{
|
|
76
105
|
"area": "shape-authoring",
|
|
77
106
|
"matrix_row": "도형 저작(선·사각형·타원)",
|
|
78
107
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
108
|
+
"authoring_methods": ("add_line", "add_rectangle", "add_ellipse"),
|
|
79
109
|
},
|
|
80
110
|
{
|
|
81
111
|
"area": "shape-escape-hatch",
|
|
82
112
|
"matrix_row": "저수준 도형·컨트롤 탈출구",
|
|
83
113
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
114
|
+
"authoring_methods": ("add_shape", "add_control"),
|
|
84
115
|
},
|
|
85
116
|
{
|
|
86
117
|
"area": "curve-objects",
|
|
87
118
|
"matrix_row": "arc·polygon·curve·connectLine",
|
|
88
119
|
"entry_points": (),
|
|
120
|
+
"authoring_methods": (),
|
|
89
121
|
},
|
|
90
122
|
{
|
|
91
123
|
"area": "picture",
|
|
92
124
|
"matrix_row": "그림 삽입/치환",
|
|
93
125
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
126
|
+
"authoring_methods": ("add_picture", "add_image"),
|
|
94
127
|
},
|
|
95
128
|
{
|
|
96
129
|
"area": "chart",
|
|
97
130
|
"matrix_row": "차트",
|
|
98
131
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
132
|
+
"authoring_methods": ("add_chart",),
|
|
99
133
|
},
|
|
100
134
|
{
|
|
101
135
|
"area": "equation",
|
|
102
136
|
"matrix_row": "수식",
|
|
103
137
|
"entry_points": ("hwpx.equation.authoring:latex_to_eqedit",),
|
|
138
|
+
"authoring_methods": ("add_equation",),
|
|
104
139
|
},
|
|
105
140
|
{
|
|
106
141
|
"area": "redline",
|
|
107
142
|
"matrix_row": "변경추적(redline)",
|
|
108
143
|
"entry_points": ("hwpx.tools.redline:verify_redline",),
|
|
144
|
+
"authoring_methods": ("add_track_change", "add_tracked_insert", "add_tracked_delete", "add_tracked_replace"),
|
|
109
145
|
},
|
|
110
146
|
{
|
|
111
147
|
"area": "memo",
|
|
112
148
|
"matrix_row": "메모(코멘트)",
|
|
113
149
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
150
|
+
"authoring_methods": ("add_memo", "add_memo_with_anchor"),
|
|
114
151
|
},
|
|
115
152
|
{
|
|
116
153
|
"area": "footnote-endnote",
|
|
117
154
|
"matrix_row": "각주/미주",
|
|
118
155
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
156
|
+
"authoring_methods": ("add_footnote", "add_endnote"),
|
|
119
157
|
},
|
|
120
158
|
{
|
|
121
159
|
"area": "toc-crossref",
|
|
122
160
|
"matrix_row": "네이티브 목차(TOC)/상호참조",
|
|
123
161
|
"entry_points": ("hwpx.tools.toc_author:add_native_toc",),
|
|
162
|
+
"authoring_methods": ("add_bookmark", "add_hyperlink"),
|
|
124
163
|
},
|
|
125
164
|
{
|
|
126
165
|
"area": "encrypted-hwpx",
|
|
127
166
|
"matrix_row": "암호화 HWPX",
|
|
128
167
|
"entry_points": (),
|
|
168
|
+
"authoring_methods": (),
|
|
129
169
|
},
|
|
130
170
|
{
|
|
131
171
|
"area": "hwp5-binary",
|
|
132
172
|
"matrix_row": "HWP 5.x 바이너리",
|
|
133
173
|
"entry_points": (),
|
|
174
|
+
"authoring_methods": (),
|
|
134
175
|
},
|
|
135
176
|
{
|
|
136
177
|
"area": "form-field-create",
|
|
137
178
|
"matrix_row": "누름틀(form field) 생성",
|
|
138
179
|
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
180
|
+
"authoring_methods": ("add_form_field",),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"area": "check-box",
|
|
184
|
+
"matrix_row": "체크박스 양식개체",
|
|
185
|
+
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
186
|
+
"authoring_methods": ("add_check_box",),
|
|
139
187
|
},
|
|
140
188
|
)
|
|
141
189
|
|
|
@@ -458,9 +506,110 @@ def contract_json_schema(name: str) -> dict[str, Any]:
|
|
|
458
506
|
return builder()
|
|
459
507
|
|
|
460
508
|
|
|
509
|
+
def verify_self_description() -> list[str]:
|
|
510
|
+
"""설치된 휠에서 자기서술이 실표면과 맞는지 직접 검사한다.
|
|
511
|
+
|
|
512
|
+
왜 라이브러리 안에 있나: 5.6.0의 CHANGELOG는 이 부류의 드리프트를 "이제
|
|
513
|
+
구조적으로 막습니다"라고 **선언**했고, 바로 다음 릴리스에서 같은 드리프트를
|
|
514
|
+
냈다. 두 번째 선언은 신뢰받을 이유가 없다. 그래서 선언 대신 **누구나 자기
|
|
515
|
+
설치본에서 돌려 볼 수 있는 검사**를 동봉한다 —
|
|
516
|
+
``python -m hwpx.capabilities --verify``.
|
|
517
|
+
|
|
518
|
+
저장소의 테스트가 아니라 설치된 패키지에서 도는 것이 요점이다. 사용자가
|
|
519
|
+
받은 휠이 실제로 무엇을 할 수 있는지 그 휠 자신에게 물어볼 수 있다.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
위반 목록. 빈 리스트면 자기서술이 실표면과 일치한다.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
from .document import HwpxDocument
|
|
526
|
+
|
|
527
|
+
problems: list[str] = []
|
|
528
|
+
|
|
529
|
+
live = {name for name in dir(HwpxDocument) if name.startswith("add_")}
|
|
530
|
+
registered: dict[str, str] = {}
|
|
531
|
+
for row in _CAPABILITY_AREAS:
|
|
532
|
+
for method in row.get("authoring_methods", ()):
|
|
533
|
+
if method in registered:
|
|
534
|
+
problems.append(
|
|
535
|
+
f"authoring method {method!r} is claimed by both "
|
|
536
|
+
f"{registered[method]!r} and {row['area']!r}"
|
|
537
|
+
)
|
|
538
|
+
registered[method] = row["area"]
|
|
539
|
+
|
|
540
|
+
for method in sorted(live - set(registered)):
|
|
541
|
+
problems.append(
|
|
542
|
+
f"HwpxDocument.{method} exists but no capability area claims it; "
|
|
543
|
+
"the self-description understates what this install can do"
|
|
544
|
+
)
|
|
545
|
+
for method in sorted(set(registered) - live):
|
|
546
|
+
problems.append(
|
|
547
|
+
f"capability registry claims HwpxDocument.{method}, which this "
|
|
548
|
+
"install does not have; the self-description overstates it"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
matrix_rows = {row["matrix_row"] for row in _CAPABILITY_AREAS}
|
|
552
|
+
try:
|
|
553
|
+
text = contract_document("support-matrix")
|
|
554
|
+
except HwpxError as exc: # pragma: no cover - only when the doc is absent
|
|
555
|
+
problems.append(f"bundled support matrix is unreadable: {exc}")
|
|
556
|
+
else:
|
|
557
|
+
documented = {
|
|
558
|
+
line.split("|")[1].strip()
|
|
559
|
+
for line in text.splitlines()
|
|
560
|
+
if line.startswith("|")
|
|
561
|
+
}
|
|
562
|
+
for row in sorted(matrix_rows - documented):
|
|
563
|
+
problems.append(f"capability area {row!r} has no support-matrix row")
|
|
564
|
+
|
|
565
|
+
return problems
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _verify_main(argv: list[str] | None = None) -> int:
|
|
569
|
+
import argparse
|
|
570
|
+
import json as _json
|
|
571
|
+
import sys
|
|
572
|
+
|
|
573
|
+
parser = argparse.ArgumentParser(
|
|
574
|
+
prog="python -m hwpx.capabilities",
|
|
575
|
+
description="Report or verify this install's machine-readable self-description.",
|
|
576
|
+
)
|
|
577
|
+
parser.add_argument(
|
|
578
|
+
"--verify",
|
|
579
|
+
action="store_true",
|
|
580
|
+
help="check the self-description against the installed surface",
|
|
581
|
+
)
|
|
582
|
+
args = parser.parse_args(argv)
|
|
583
|
+
|
|
584
|
+
if not args.verify:
|
|
585
|
+
print(_json.dumps(describe_capabilities(), ensure_ascii=False, indent=2))
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
problems = verify_self_description()
|
|
589
|
+
for problem in problems:
|
|
590
|
+
print(f"self-description error: {problem}", file=sys.stderr)
|
|
591
|
+
if problems:
|
|
592
|
+
print(
|
|
593
|
+
f"{len(problems)} self-description problem(s) found in "
|
|
594
|
+
f"python-hwpx {_package_version()}",
|
|
595
|
+
file=sys.stderr,
|
|
596
|
+
)
|
|
597
|
+
return 1
|
|
598
|
+
print(
|
|
599
|
+
f"python-hwpx {_package_version()}: self-description matches the "
|
|
600
|
+
f"installed surface ({len(_CAPABILITY_AREAS)} capability areas)"
|
|
601
|
+
)
|
|
602
|
+
return 0
|
|
603
|
+
|
|
604
|
+
|
|
461
605
|
__all__ = [
|
|
462
606
|
"CAPABILITIES_SCHEMA",
|
|
463
607
|
"describe_capabilities",
|
|
464
608
|
"contract_document",
|
|
465
609
|
"contract_json_schema",
|
|
610
|
+
"verify_self_description",
|
|
466
611
|
]
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
if __name__ == "__main__": # pragma: no cover
|
|
615
|
+
raise SystemExit(_verify_main())
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | corpus-metrics「오픈 수용률」476/476, 「저작 품질 게이트」실저작 58/58, 「렌더 검증」416건 |
|
|
25
25
|
| 표 구조 변경(행·열·표 삭제/삽입, 열 오토핏) | Preserve·Edit | `hwpx.table_patch`; corpus-metrics「바이트 보존」497/497(patch 경로) |
|
|
26
|
-
| 표 생성(병합·중첩 포함) | Create·Render-verified | `add_table`·`merge_cells`(피복 셀 제거+cellSpan — 실한컴 병합 의미론과 동형, 3종 병합 렌더 픽셀 확인)·셀 문단 `add_table`(중첩).
|
|
26
|
+
| 표 생성(병합·중첩 포함) | Create·Render-verified | `add_table`·`merge_cells`(피복 셀 제거+cellSpan — 실한컴 병합 의미론과 동형, 3종 병합 렌더 픽셀 확인)·셀 문단 `add_table`(중첩). 기본값은 5.4.0에서 실한컴에 맞췄다: 셀 안여백 510/510/141/141, 기본 표 폭은 본문 폭, 중첩 표는 부모 셀 사용 폭. **남은 갭**: 선 종류는 raw 전용 |
|
|
27
27
|
| 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch`; corpus-metrics「바이트 보존」497/497. wild 공개 양식의 서식 충실은 2차 실측 후 **산출분 pass 82.1%(23/28)·산출+fail 7.6%(5/66)** — 불가능 타깃은 typed 거부 35건(전수 감별 과잉거부 0). 잔존은 페이지 리플 5건(4건 typed 경고 동반·완전 무음 1건, 「구조결함 2차 실측」절), 표 shape 부류는 측정 인공물로 판명·판정기 교정 |
|
|
28
28
|
| 편집 계획 실행(edit plan) | Preserve·Edit | `hwpx.plan.apply_edit_plan`(experimental, 5.6.0+): 바이트-스플라이스 op 7종을 계획 1파일로 합성 — 정적 선검증→전 체인 인메모리 실행→최종 open-safety 검증→단 1회 원자 쓰기. 중간 실패 시 output·source 바이트 불변(테스트가 바이트 비교로 증명), step별+원본→최종 `hwpx.mutation-report/v1` 실측 사영 |
|
|
29
29
|
| 도형 저작(선·사각형·타원) | Parse·Preserve·Edit·Create·Render-verified | 전용 `add_line`·`add_rectangle`·`add_ellipse`. 5.0.0에서 기하 네임스페이스(`hc:`)·선 bounding box·`resize()`의 기하 반영을 수정한 뒤 실한컴 12.30.0(build 6446)에서 **개봉 7/7**(선 수평·대각·수직, 사각형, 타원, resize 후 혼합, 그림). 렌더로 사각형 144×72pt+`#CCE5FF`, 타원 100×60pt+`#FFD9CC`, 대각선이 요청 박스를 실제 span, `resize(14400,7200)` 후 새 크기·DASH 반영까지 확인 |
|
|
30
30
|
| 저수준 도형·컨트롤 탈출구 | Edit | `add_shape`·`add_control`은 건네받은 요소와 속성만 쓰고 OWPML 필수 하위 요소(`offset`·`orgSz`·`curSz`·`sz`·`pos`·유형별 기하)는 만들지 않는다. 그대로 저장한 문서는 실한컴 12.30.0이 **거부**한다(음성 대조로 확인). 신호는 생성 시점의 `UserWarning` **하나뿐**이다 — 저장을 막지 않고 `validate_package`·`validate_editor_open_safety`도 통과시킨다(실측 `ok=True`). 도형은 위의 전용 헬퍼를 쓸 것(전용 헬퍼는 경고하지 않는다) |
|
|
31
31
|
| arc·polygon·curve·connectLine | Parse·Unsupported-but-preserved | 열거·읽기는 되지만 저작 API 없음. 기존 개체는 patch 저장에서 바이트 동일 왕복 |
|
|
32
32
|
| 그림 삽입/치환 | Edit·Create | `add_picture`·`add_image`·`replace_picture`. 단순 그림 개체 자동 생성은 지원하며 실한컴 개봉을 확인했다(실한컴 코퍼스와 자식 요소 단위 대조). 그룹·효과 등 복잡 개체 생성은 미지원 |
|
|
33
|
-
| 차트 |
|
|
33
|
+
| 차트 | Create(experimental)·Preserve | `add_chart`(5.3.0+): 데이터·계열·축 레이블에서 차트 개체를 생성한다. 기존 차트 part는 patch 저장 시 바이트 보존(497/497). 생성 어휘 밖의 차트 종류·서식은 미지원이며 기존 개체 보존으로만 다룬다 |
|
|
34
34
|
| 수식 | Parse·Create(experimental)·Render-verified | `add_equation`(EqEdit script 삽입, 5.2.0+)과 `hwpx.equation.latex_to_eqedit`(렌더 검증 토큰셋만 변환, 밖은 `UnsupportedLatexError`로 typed 거부). 저작 어휘 전 토큰을 실한컴 렌더 오라클 픽셀 실측(60수식 배터리)으로 확정했고, 기존 수식 개체는 파싱·patch 보존됨(미리보기 MathML 렌더는 뷰어/플러그인 계층) |
|
|
35
35
|
| 변경추적(redline) | Edit·Create | `add_tracked_insert`·`add_tracked_delete`·`add_tracked_replace`; 실 Windows 한컴 COM `IsTrackChange=1`·검토 리본 수락/거부 스파이크. **렌더 주의**: 한컴이 변경추적 문서의 PDF export 자체를 거부 → corpus-metrics「렌더 검증」에서 `render_unavailable`로 정직 집계(결함 아님, 한컴 제약) |
|
|
36
36
|
| 메모(코멘트) | Edit·Create·Render-verified | `add_memo`·`add_memo_with_anchor`; subList 코멘트 텍스트 + `MemoShapeIDRef` 버그 수정을 실 Windows 한컴에서 검증(CHANGELOG) |
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
| 암호화 HWPX | Unsupported-and-rejected | 복호화 API 없음. 암호화된 content part는 파싱 단계에서 예외(`XMLSyntaxError`)로 거부 — 무음으로 잘못된 문서를 만들지 않음(fail-closed) |
|
|
40
40
|
| HWP 5.x 바이너리 | Unsupported-and-rejected | HWP v5는 ZIP이 아니므로 열기 시 `BadZipFile` 예외. OLE2/CFBF 시그니처를 확인하면 예외 메시지가 HWPX 변환을 안내한다(예외 타입은 그대로) |
|
|
41
41
|
| 누름틀(form field) 생성 | Parse·Edit·Create(experimental) | `list_form_fields`·`fill_form_field`로 조회·서식 보존 채움에 더해, `add_form_field`(5.1.0+)가 실한컴 CLICKHERE 계약 그대로 신규 누름틀을 생성한다(표 셀 배치 포함). 만든 필드는 기존 list/fill과 실제 한컴이 특수분기 없이 소비 |
|
|
42
|
+
| 체크박스 양식개체 | Create·Render-verified | `add_check_box`·`list_check_boxes`·`set_check_box`(5.7.0+). 실한컴 실측 계약: `value` CHECKED=☑ / UNCHECKED=□, `<hp:formCharPr>`는 **필수 자식**(없으면 한컴이 문서를 거부하는데 우리 open-safety·ID 무결성은 통과한다 — 실한컴이 유일한 판정자다). 라디오(`hp:radioBtn`)·명령단추(`hp:btn`)는 읽기·보존만 하고 저작 API 없음 |
|
|
42
43
|
|
|
43
44
|
## 상태 판정 근거 요약
|
|
44
45
|
|
|
@@ -1462,6 +1462,68 @@ class HwpxDocument:
|
|
|
1462
1462
|
section_index=section_index,
|
|
1463
1463
|
)
|
|
1464
1464
|
|
|
1465
|
+
def list_check_boxes(self) -> list[dict[str, Any]]:
|
|
1466
|
+
"""Return check-box form objects (체크박스) in document order.
|
|
1467
|
+
|
|
1468
|
+
Each entry carries ``index``/``name``/``caption``/``checked``.
|
|
1469
|
+
"""
|
|
1470
|
+
|
|
1471
|
+
return _fields.list_check_boxes(self)
|
|
1472
|
+
|
|
1473
|
+
def add_check_box(
|
|
1474
|
+
self,
|
|
1475
|
+
caption: str,
|
|
1476
|
+
*,
|
|
1477
|
+
checked: bool = False,
|
|
1478
|
+
name: str | None = None,
|
|
1479
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
1480
|
+
section: HwpxOxmlSection | None = None,
|
|
1481
|
+
section_index: int | None = None,
|
|
1482
|
+
) -> dict[str, Any]:
|
|
1483
|
+
"""Create a check-box form object (체크박스). **Experimental contract.**
|
|
1484
|
+
|
|
1485
|
+
Real Hancom draws ☑ when *checked* and □ otherwise, with *caption* beside
|
|
1486
|
+
the box and present in the rendered text layer. The created object is
|
|
1487
|
+
read back through :meth:`list_check_boxes` with no special-casing.
|
|
1488
|
+
|
|
1489
|
+
Note:
|
|
1490
|
+
Korean government forms specify a text ``[ ]`` + √ convention rather
|
|
1491
|
+
than this form object (시행규칙 별표 4 제10호), so this primitive is
|
|
1492
|
+
for forms that genuinely use Hancom check boxes — not for 공문서.
|
|
1493
|
+
|
|
1494
|
+
Args:
|
|
1495
|
+
caption: Label drawn beside the box (non-empty).
|
|
1496
|
+
checked: Initial state.
|
|
1497
|
+
name: Object name; generated when omitted.
|
|
1498
|
+
paragraph: Target paragraph (e.g. a table cell). A new paragraph is
|
|
1499
|
+
appended to *section* when omitted.
|
|
1500
|
+
"""
|
|
1501
|
+
|
|
1502
|
+
return _fields.add_check_box(
|
|
1503
|
+
self,
|
|
1504
|
+
caption,
|
|
1505
|
+
checked=checked,
|
|
1506
|
+
name=name,
|
|
1507
|
+
paragraph=paragraph,
|
|
1508
|
+
section=section,
|
|
1509
|
+
section_index=section_index,
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
def set_check_box(
|
|
1513
|
+
self,
|
|
1514
|
+
checked: bool,
|
|
1515
|
+
*,
|
|
1516
|
+
index: int | None = None,
|
|
1517
|
+
name: str | None = None,
|
|
1518
|
+
) -> dict[str, Any]:
|
|
1519
|
+
"""Set a check box's state, selecting it by ``index`` or ``name``.
|
|
1520
|
+
|
|
1521
|
+
Exactly one selector is required; an ambiguous name is refused rather
|
|
1522
|
+
than guessed.
|
|
1523
|
+
"""
|
|
1524
|
+
|
|
1525
|
+
return _fields.set_check_box(self, checked, index=index, name=name)
|
|
1526
|
+
|
|
1465
1527
|
|
|
1466
1528
|
def add_chart(
|
|
1467
1529
|
self,
|
|
@@ -104,10 +104,19 @@ class LayoutLintReport:
|
|
|
104
104
|
|
|
105
105
|
from hwpx.quality.report import LayoutReport
|
|
106
106
|
|
|
107
|
+
warnings = [str(f) for f in self.warnings]
|
|
108
|
+
errors = [str(f) for f in self.errors]
|
|
109
|
+
if errors:
|
|
110
|
+
status = "failed"
|
|
111
|
+
elif warnings:
|
|
112
|
+
status = "warned"
|
|
113
|
+
else:
|
|
114
|
+
status = "passed"
|
|
107
115
|
return LayoutReport(
|
|
108
116
|
ok=self.ok,
|
|
109
|
-
warnings=
|
|
110
|
-
errors=
|
|
117
|
+
warnings=warnings,
|
|
118
|
+
errors=errors,
|
|
119
|
+
status=status,
|
|
111
120
|
)
|
|
112
121
|
|
|
113
122
|
def to_dict(self) -> dict[str, Any]:
|