python-hwpx 5.0.2__tar.gz → 5.1.1__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.0.2/src/python_hwpx.egg-info → python_hwpx-5.1.1}/PKG-INFO +1 -1
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/pyproject.toml +1 -1
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/fields.py +81 -1
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/document.py +42 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/paragraph.py +100 -1
- {python_hwpx-5.0.2 → python_hwpx-5.1.1/src/python_hwpx.egg-info}/PKG-INFO +1 -1
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- python_hwpx-5.1.1/tests/test_form_field_authoring.py +222 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/LICENSE +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/NOTICE +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/README.md +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/setup.cfg +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/package.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/patch.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_add_control.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_ingest.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_llms_txt.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_new_features.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_opc_package.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.0.2 → python_hwpx-5.1.1}/tests/test_version_metadata.py +0 -0
|
@@ -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.1.1"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -211,10 +211,16 @@ def _form_field_payload(
|
|
|
211
211
|
name = _field_parameter_value(parameters, "fieldName", "fieldname", "field_name", "name", "title")
|
|
212
212
|
prompt = _first_attr(field_begin, _FORM_FIELD_PROMPT_ATTRS)
|
|
213
213
|
if not prompt:
|
|
214
|
-
|
|
214
|
+
# "Direction" is the real-Hancom 안내문 parameter (P0 gold contract).
|
|
215
|
+
prompt = _field_parameter_value(parameters, "Direction", *_FORM_FIELD_PARAM_NAMES)
|
|
215
216
|
instruction = _field_parameter_value(parameters, "instruction", "guide", "help", "description", "desc")
|
|
216
217
|
if not instruction:
|
|
217
218
|
instruction = prompt
|
|
219
|
+
memo = _field_parameter_value(parameters, "HelpState", "memo")
|
|
220
|
+
dirty = (field_begin.get("dirty") or "").strip()
|
|
221
|
+
# Contract: while dirty != "1" the content between begin/end is the prompt
|
|
222
|
+
# placeholder (screen-only, not printed), not a user value.
|
|
223
|
+
is_placeholder = dirty != "1" and bool(prompt) and current_value == prompt
|
|
218
224
|
return {
|
|
219
225
|
"index": index,
|
|
220
226
|
"field_id": _field_identifier(field_begin),
|
|
@@ -223,9 +229,13 @@ def _form_field_payload(
|
|
|
223
229
|
"name": name,
|
|
224
230
|
"prompt": prompt,
|
|
225
231
|
"instruction": instruction,
|
|
232
|
+
"memo": memo,
|
|
233
|
+
"dirty": dirty,
|
|
234
|
+
"is_placeholder": is_placeholder,
|
|
226
235
|
"current_value": current_value,
|
|
227
236
|
"field_type": field_begin.get("type", ""),
|
|
228
237
|
"control_type": ctrl.get("type", ""),
|
|
238
|
+
"_field_begin": field_begin,
|
|
229
239
|
"section_index": section_index,
|
|
230
240
|
"paragraph_index": paragraph_index,
|
|
231
241
|
"paragraph_index_in_section": paragraph_index_in_section,
|
|
@@ -320,6 +330,62 @@ def list_form_fields(doc: "HwpxDocument") -> list[dict[str, Any]]:
|
|
|
320
330
|
]
|
|
321
331
|
|
|
322
332
|
|
|
333
|
+
_PROMPT_TEXT_COLOR = "#FF0000"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def add_form_field(
|
|
337
|
+
doc: "HwpxDocument",
|
|
338
|
+
name: str,
|
|
339
|
+
*,
|
|
340
|
+
prompt: str = "",
|
|
341
|
+
memo: str = "",
|
|
342
|
+
editable: bool = True,
|
|
343
|
+
paragraph: Any | None = None,
|
|
344
|
+
section: Any | None = None,
|
|
345
|
+
section_index: int | None = None,
|
|
346
|
+
) -> dict[str, Any]:
|
|
347
|
+
"""Create a click-here (누름틀) form field and return its field payload.
|
|
348
|
+
|
|
349
|
+
The emitted XML follows the real-Hancom CLICKHERE contract
|
|
350
|
+
(reverse-engineered from Hancom Office 12.0.0.3288 gold documents). The
|
|
351
|
+
prompt (안내문) is materialized as a screen-only red-italic run, exactly as
|
|
352
|
+
Hancom authors it. The created field is immediately re-read through the
|
|
353
|
+
standard ``list_form_fields`` matcher — creation fails loudly if the
|
|
354
|
+
standard consumer would not recognize it (no special-casing by design).
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
if not str(name).strip():
|
|
358
|
+
raise ValueError("form field name must be a non-empty string")
|
|
359
|
+
if paragraph is None:
|
|
360
|
+
paragraph = doc.add_paragraph(
|
|
361
|
+
"", section=section, section_index=section_index, include_run=False,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
prompt_char_pr: str | None = None
|
|
365
|
+
if _sanitize_field_text(prompt):
|
|
366
|
+
prompt_char_pr = doc.ensure_run_style(italic=True, color=_PROMPT_TEXT_COLOR)
|
|
367
|
+
|
|
368
|
+
control = paragraph.add_form_field(
|
|
369
|
+
name,
|
|
370
|
+
prompt=prompt,
|
|
371
|
+
memo=memo,
|
|
372
|
+
editable=editable,
|
|
373
|
+
prompt_char_pr_id_ref=prompt_char_pr,
|
|
374
|
+
)
|
|
375
|
+
_clear_form_field_layout_cache(paragraph.element)
|
|
376
|
+
|
|
377
|
+
field_begin = control.element.find(f"{_HP}fieldBegin")
|
|
378
|
+
created_id = field_begin.get("id", "") if field_begin is not None else ""
|
|
379
|
+
for match in _iter_form_field_matches(doc):
|
|
380
|
+
if match.get("id") == created_id:
|
|
381
|
+
return {
|
|
382
|
+
key: value for key, value in match.items() if not key.startswith("_")
|
|
383
|
+
}
|
|
384
|
+
raise RuntimeError(
|
|
385
|
+
"created form field was not recognized by the standard form-field matcher"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
323
389
|
def _select_form_field(
|
|
324
390
|
doc: "HwpxDocument",
|
|
325
391
|
matches: Sequence[dict[str, Any]],
|
|
@@ -447,9 +513,23 @@ def fill_form_field(
|
|
|
447
513
|
node.text = ""
|
|
448
514
|
for child in list(node):
|
|
449
515
|
child.tail = ""
|
|
516
|
+
if match.get("is_placeholder"):
|
|
517
|
+
# Contract (P0 gold): Hancom swaps the screen-only prompt style for
|
|
518
|
+
# the surrounding style when a value replaces the placeholder.
|
|
519
|
+
begin_run = runs[int(match["_begin_run_index"])]
|
|
520
|
+
begin_ref = begin_run.get("charPrIDRef")
|
|
521
|
+
primary_run = primary.getparent()
|
|
522
|
+
if begin_ref is not None and primary_run is not None:
|
|
523
|
+
primary_run.set("charPrIDRef", begin_ref)
|
|
450
524
|
else:
|
|
451
525
|
_insert_form_field_text_run(doc, match, sanitized)
|
|
452
526
|
|
|
527
|
+
# Contract (P0 gold): a field that went through fill machinery carries
|
|
528
|
+
# dirty="1"; while dirty != "1" readers treat the content as the prompt.
|
|
529
|
+
field_begin = match.get("_field_begin")
|
|
530
|
+
if field_begin is not None:
|
|
531
|
+
field_begin.set("dirty", "1")
|
|
532
|
+
|
|
453
533
|
if fit_result is not None:
|
|
454
534
|
_apply_form_field_fit_style(doc, match, fit_result)
|
|
455
535
|
|
|
@@ -1388,6 +1388,48 @@ class HwpxDocument:
|
|
|
1388
1388
|
section_index=section_index,
|
|
1389
1389
|
)
|
|
1390
1390
|
|
|
1391
|
+
def add_form_field(
|
|
1392
|
+
self,
|
|
1393
|
+
name: str,
|
|
1394
|
+
*,
|
|
1395
|
+
prompt: str = "",
|
|
1396
|
+
memo: str = "",
|
|
1397
|
+
editable: bool = True,
|
|
1398
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
1399
|
+
section: HwpxOxmlSection | None = None,
|
|
1400
|
+
section_index: int | None = None,
|
|
1401
|
+
) -> dict[str, Any]:
|
|
1402
|
+
"""Create a click-here (누름틀) form field. **Experimental contract.**
|
|
1403
|
+
|
|
1404
|
+
Emits the real-Hancom CLICKHERE shape (안내문 placeholder run included)
|
|
1405
|
+
so the created field is indistinguishable from a Hancom-authored one:
|
|
1406
|
+
``list_form_fields``/``fill_form_field`` recognize it with no
|
|
1407
|
+
special-casing, and real Hancom Office enumerates and fills it.
|
|
1408
|
+
|
|
1409
|
+
Args:
|
|
1410
|
+
name: Field name (non-empty).
|
|
1411
|
+
prompt: 안내문 shown while the field is empty. Screen-only —
|
|
1412
|
+
Hancom does not print it.
|
|
1413
|
+
memo: Help text (``HelpState``).
|
|
1414
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
1415
|
+
omitted a new paragraph is appended to *section*.
|
|
1416
|
+
|
|
1417
|
+
Returns:
|
|
1418
|
+
The created field's payload, same shape as a ``list_form_fields``
|
|
1419
|
+
entry.
|
|
1420
|
+
"""
|
|
1421
|
+
|
|
1422
|
+
return _fields.add_form_field(
|
|
1423
|
+
self,
|
|
1424
|
+
name,
|
|
1425
|
+
prompt=prompt,
|
|
1426
|
+
memo=memo,
|
|
1427
|
+
editable=editable,
|
|
1428
|
+
paragraph=paragraph,
|
|
1429
|
+
section=section,
|
|
1430
|
+
section_index=section_index,
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1391
1433
|
|
|
1392
1434
|
def set_page_size(
|
|
1393
1435
|
self,
|
|
@@ -27,7 +27,7 @@ from ._document_primitives import (
|
|
|
27
27
|
_sanitize_text,
|
|
28
28
|
)
|
|
29
29
|
from .memo import HwpxOxmlNote
|
|
30
|
-
from .namespaces import tag_local_name
|
|
30
|
+
from .namespaces import XML_NS, tag_local_name
|
|
31
31
|
from .objects import (
|
|
32
32
|
HwpxOxmlInlineObject,
|
|
33
33
|
HwpxOxmlShape,
|
|
@@ -834,6 +834,105 @@ class HwpxOxmlParagraph:
|
|
|
834
834
|
self.section.mark_dirty()
|
|
835
835
|
return HwpxOxmlInlineObject(ctrl1, self)
|
|
836
836
|
|
|
837
|
+
def add_form_field(
|
|
838
|
+
self,
|
|
839
|
+
name: str,
|
|
840
|
+
*,
|
|
841
|
+
prompt: str = "",
|
|
842
|
+
memo: str = "",
|
|
843
|
+
editable: bool = True,
|
|
844
|
+
prompt_char_pr_id_ref: str | int | None = None,
|
|
845
|
+
char_pr_id_ref: str | int | None = None,
|
|
846
|
+
) -> HwpxOxmlInlineObject:
|
|
847
|
+
"""Insert a click-here (누름틀) form field at the end of this paragraph.
|
|
848
|
+
|
|
849
|
+
Emits the real-Hancom CLICKHERE shape (reverse-engineered from Hancom
|
|
850
|
+
Office 12.0.0.3288 gold documents):
|
|
851
|
+
a ``fieldBegin`` ctrl run carrying the ``Prop``/``Command``/``Direction``/
|
|
852
|
+
``HelpState`` parameters, an optional prompt run showing *prompt* (the
|
|
853
|
+
안내문, screen-only — Hancom does not print it), and a ``fieldEnd`` ctrl
|
|
854
|
+
run. ``Command`` lengths count UTF-16 characters, so values may contain
|
|
855
|
+
spaces. ``id``/``fieldid`` values are semantically free — Hancom reissues
|
|
856
|
+
its own on save.
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
name: Field name used by ``list_form_fields``/``fill_form_field``.
|
|
860
|
+
prompt: 안내문 text shown while the field is empty (``Direction``).
|
|
861
|
+
memo: Help text (``HelpState``).
|
|
862
|
+
prompt_char_pr_id_ref: charPr for the prompt run (callers normally
|
|
863
|
+
pass a red-italic style; Hancom uses one).
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
The ``<hp:ctrl>`` element wrapping the ``<hp:fieldBegin>``.
|
|
867
|
+
"""
|
|
868
|
+
field_id = _object_id()
|
|
869
|
+
field_instance_id = _object_id()
|
|
870
|
+
direction = _sanitize_text(prompt)
|
|
871
|
+
help_state = _sanitize_text(memo)
|
|
872
|
+
|
|
873
|
+
# Run 1: fieldBegin with the CLICKHERE parameter block
|
|
874
|
+
run1 = self._create_run_for_object(char_pr_id_ref=char_pr_id_ref)
|
|
875
|
+
ctrl1 = _append_child(run1, f"{_HP}ctrl", {})
|
|
876
|
+
field_begin = _append_child(ctrl1, f"{_HP}fieldBegin", {
|
|
877
|
+
"id": field_id,
|
|
878
|
+
"type": "CLICK_HERE",
|
|
879
|
+
"name": _sanitize_text(name),
|
|
880
|
+
"editable": "1" if editable else "0",
|
|
881
|
+
"dirty": "0",
|
|
882
|
+
"zorder": "-1",
|
|
883
|
+
"fieldid": field_instance_id,
|
|
884
|
+
"metaTag": "",
|
|
885
|
+
})
|
|
886
|
+
payload = (
|
|
887
|
+
f"Direction:wstring:{len(direction)}:{direction} "
|
|
888
|
+
f"HelpState:wstring:{len(help_state)}:{help_state} "
|
|
889
|
+
)
|
|
890
|
+
param_count = 2 + (1 if direction else 0) + (1 if help_state else 0)
|
|
891
|
+
parameters = _append_child(
|
|
892
|
+
field_begin, f"{_HP}parameters", {"cnt": str(param_count), "name": ""}
|
|
893
|
+
)
|
|
894
|
+
prop = _append_child(parameters, f"{_HP}integerParam", {"name": "Prop"})
|
|
895
|
+
prop.text = "9"
|
|
896
|
+
command = _append_child(parameters, f"{_HP}stringParam", {
|
|
897
|
+
"name": "Command",
|
|
898
|
+
f"{{{XML_NS}}}space": "preserve",
|
|
899
|
+
})
|
|
900
|
+
command.text = f"Clickhere:set:{len(payload)}:{payload} "
|
|
901
|
+
if direction:
|
|
902
|
+
direction_param = _append_child(
|
|
903
|
+
parameters, f"{_HP}stringParam", {"name": "Direction"}
|
|
904
|
+
)
|
|
905
|
+
direction_param.text = direction
|
|
906
|
+
if help_state:
|
|
907
|
+
help_param = _append_child(
|
|
908
|
+
parameters, f"{_HP}stringParam", {"name": "HelpState"}
|
|
909
|
+
)
|
|
910
|
+
help_param.text = help_state
|
|
911
|
+
|
|
912
|
+
# Run 2: the prompt placeholder (only while a prompt exists)
|
|
913
|
+
if direction:
|
|
914
|
+
run2 = self._create_run_for_object(
|
|
915
|
+
char_pr_id_ref=(
|
|
916
|
+
prompt_char_pr_id_ref
|
|
917
|
+
if prompt_char_pr_id_ref is not None
|
|
918
|
+
else char_pr_id_ref
|
|
919
|
+
),
|
|
920
|
+
)
|
|
921
|
+
prompt_text = _append_child(run2, f"{_HP}t", {})
|
|
922
|
+
prompt_text.text = direction
|
|
923
|
+
|
|
924
|
+
# Run 3: fieldEnd + trailing empty text node (gold shape)
|
|
925
|
+
run3 = self._create_run_for_object(char_pr_id_ref=char_pr_id_ref)
|
|
926
|
+
ctrl3 = _append_child(run3, f"{_HP}ctrl", {})
|
|
927
|
+
_append_child(ctrl3, f"{_HP}fieldEnd", {
|
|
928
|
+
"beginIDRef": field_id,
|
|
929
|
+
"fieldid": field_instance_id,
|
|
930
|
+
})
|
|
931
|
+
_append_child(run3, f"{_HP}t", {})
|
|
932
|
+
|
|
933
|
+
self.section.mark_dirty()
|
|
934
|
+
return HwpxOxmlInlineObject(ctrl1, self)
|
|
935
|
+
|
|
837
936
|
@property
|
|
838
937
|
def bookmarks(self) -> list[str]:
|
|
839
938
|
"""Return the names of all bookmarks in this paragraph."""
|
|
@@ -137,6 +137,7 @@ tests/test_equation_render.py
|
|
|
137
137
|
tests/test_error_messages.py
|
|
138
138
|
tests/test_evalplan_core_primitives.py
|
|
139
139
|
tests/test_existing_document_format_editing.py
|
|
140
|
+
tests/test_form_field_authoring.py
|
|
140
141
|
tests/test_form_fields.py
|
|
141
142
|
tests/test_form_fill_core_primitives.py
|
|
142
143
|
tests/test_form_fit.py
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hwpx import HwpxDocument
|
|
7
|
+
from hwpx.tools.package_validator import validate_editor_open_safety
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
|
|
12
|
+
HH = "{http://www.hancom.co.kr/hwpml/2011/head}"
|
|
13
|
+
DATA = Path(__file__).parent / "data"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _roundtrip(doc: HwpxDocument) -> HwpxDocument:
|
|
17
|
+
buffer = io.BytesIO()
|
|
18
|
+
doc.save_to_stream(buffer)
|
|
19
|
+
buffer.seek(0)
|
|
20
|
+
return HwpxDocument.open(buffer)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _field_begin(doc: HwpxDocument, name: str):
|
|
24
|
+
for section in doc.sections:
|
|
25
|
+
for fb in section.element.iter(f"{HP}fieldBegin"):
|
|
26
|
+
if fb.get("name") == name:
|
|
27
|
+
return fb
|
|
28
|
+
raise AssertionError(f"fieldBegin not found: {name}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestGoldContractShape:
|
|
32
|
+
"""The emitted XML must match the real-Hancom CLICKHERE gold pair
|
|
33
|
+
(specs/053-formfield-authoring/evidence/p0/clickhere-contract.md)."""
|
|
34
|
+
|
|
35
|
+
def test_field_begin_attributes(self) -> None:
|
|
36
|
+
doc = HwpxDocument.new()
|
|
37
|
+
doc.add_form_field("성명", prompt="이름 입력", memo="도움말")
|
|
38
|
+
fb = _field_begin(doc, "성명")
|
|
39
|
+
assert fb.get("type") == "CLICK_HERE"
|
|
40
|
+
assert fb.get("editable") == "1"
|
|
41
|
+
assert fb.get("dirty") == "0"
|
|
42
|
+
assert fb.get("zorder") == "-1"
|
|
43
|
+
assert fb.get("metaTag") == ""
|
|
44
|
+
assert fb.get("id") and fb.get("fieldid")
|
|
45
|
+
|
|
46
|
+
def test_command_serialization_counts_characters(self) -> None:
|
|
47
|
+
# Korean + spaces inside values: lengths are UTF-16 character counts.
|
|
48
|
+
doc = HwpxDocument.new()
|
|
49
|
+
doc.add_form_field("f", prompt="안내 문", memo="m 1")
|
|
50
|
+
fb = _field_begin(doc, "f")
|
|
51
|
+
params = {
|
|
52
|
+
p.get("name"): (p.text or "")
|
|
53
|
+
for p in fb.find(f"{HP}parameters")
|
|
54
|
+
}
|
|
55
|
+
payload = "Direction:wstring:4:안내 문 HelpState:wstring:3:m 1 "
|
|
56
|
+
assert params["Command"] == f"Clickhere:set:{len(payload)}:{payload} "
|
|
57
|
+
assert params["Direction"] == "안내 문"
|
|
58
|
+
assert params["HelpState"] == "m 1"
|
|
59
|
+
assert params["Prop"] == "9"
|
|
60
|
+
command_param = fb.find(f"{HP}parameters/{HP}stringParam[@name='Command']")
|
|
61
|
+
assert command_param.get(
|
|
62
|
+
"{http://www.w3.org/XML/1998/namespace}space"
|
|
63
|
+
) == "preserve"
|
|
64
|
+
|
|
65
|
+
def test_param_count_omits_empty_entries(self) -> None:
|
|
66
|
+
doc = HwpxDocument.new()
|
|
67
|
+
doc.add_form_field("only_name")
|
|
68
|
+
fb = _field_begin(doc, "only_name")
|
|
69
|
+
parameters = fb.find(f"{HP}parameters")
|
|
70
|
+
assert parameters.get("cnt") == "2"
|
|
71
|
+
names = [p.get("name") for p in parameters]
|
|
72
|
+
assert names == ["Prop", "Command"]
|
|
73
|
+
|
|
74
|
+
def test_prompt_run_is_red_italic_and_field_end_pairs(self) -> None:
|
|
75
|
+
doc = HwpxDocument.new()
|
|
76
|
+
doc.add_form_field("f", prompt="누르세요")
|
|
77
|
+
fb = _field_begin(doc, "f")
|
|
78
|
+
paragraph = fb.getparent().getparent().getparent()
|
|
79
|
+
runs = [child for child in paragraph if child.tag == f"{HP}run"]
|
|
80
|
+
begin_index = next(
|
|
81
|
+
i for i, run in enumerate(runs)
|
|
82
|
+
if run.find(f"{HP}ctrl/{HP}fieldBegin") is not None
|
|
83
|
+
)
|
|
84
|
+
prompt_run = runs[begin_index + 1]
|
|
85
|
+
assert prompt_run.find(f"{HP}t").text == "누르세요"
|
|
86
|
+
char_pr = doc.headers[0].element.find(
|
|
87
|
+
f".//{HH}charPr[@id='{prompt_run.get('charPrIDRef')}']"
|
|
88
|
+
)
|
|
89
|
+
assert char_pr.get("textColor", "").upper() == "#FF0000"
|
|
90
|
+
assert char_pr.find(f"{HH}italic") is not None
|
|
91
|
+
end_run = runs[begin_index + 2]
|
|
92
|
+
fe = end_run.find(f"{HP}ctrl/{HP}fieldEnd")
|
|
93
|
+
assert fe.get("beginIDRef") == fb.get("id")
|
|
94
|
+
assert fe.get("fieldid") == fb.get("fieldid")
|
|
95
|
+
assert end_run.find(f"{HP}t") is not None
|
|
96
|
+
|
|
97
|
+
def test_no_prompt_run_when_prompt_empty(self) -> None:
|
|
98
|
+
doc = HwpxDocument.new()
|
|
99
|
+
doc.add_form_field("bare")
|
|
100
|
+
fb = _field_begin(doc, "bare")
|
|
101
|
+
paragraph = fb.getparent().getparent().getparent()
|
|
102
|
+
runs = [child for child in paragraph if child.tag == f"{HP}run"]
|
|
103
|
+
begin_index = next(
|
|
104
|
+
i for i, run in enumerate(runs)
|
|
105
|
+
if run.find(f"{HP}ctrl/{HP}fieldBegin") is not None
|
|
106
|
+
)
|
|
107
|
+
assert runs[begin_index + 1].find(f"{HP}ctrl/{HP}fieldEnd") is not None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestRoundTrip:
|
|
111
|
+
"""Created fields must be consumed by the standard list/fill APIs with no
|
|
112
|
+
special-casing, across a save/reopen boundary."""
|
|
113
|
+
|
|
114
|
+
def test_create_save_reopen_list(self) -> None:
|
|
115
|
+
doc = HwpxDocument.new()
|
|
116
|
+
created = doc.add_form_field("부서", prompt="부서 입력", memo="조직명")
|
|
117
|
+
assert created["name"] == "부서"
|
|
118
|
+
assert created["prompt"] == "부서 입력"
|
|
119
|
+
assert created["memo"] == "조직명"
|
|
120
|
+
assert created["is_placeholder"] is True
|
|
121
|
+
assert created["has_end"] is True
|
|
122
|
+
|
|
123
|
+
reopened = _roundtrip(doc)
|
|
124
|
+
fields = reopened.list_form_fields()
|
|
125
|
+
assert [f["name"] for f in fields] == ["부서"]
|
|
126
|
+
field = fields[0]
|
|
127
|
+
assert field["dirty"] == "0"
|
|
128
|
+
assert field["is_placeholder"] is True
|
|
129
|
+
assert field["current_value"] == "부서 입력"
|
|
130
|
+
|
|
131
|
+
def test_fill_swaps_placeholder_style_and_sets_dirty(self) -> None:
|
|
132
|
+
doc = HwpxDocument.new()
|
|
133
|
+
doc.add_form_field("성명", prompt="이름 입력")
|
|
134
|
+
reopened = _roundtrip(doc)
|
|
135
|
+
response = reopened.fill_form_field("홍길동", name="성명")
|
|
136
|
+
assert response["ok"] is True
|
|
137
|
+
field = response["field"]
|
|
138
|
+
assert field["current_value"] == "홍길동"
|
|
139
|
+
assert field["dirty"] == "1"
|
|
140
|
+
assert field["is_placeholder"] is False
|
|
141
|
+
|
|
142
|
+
fb = _field_begin(reopened, "성명")
|
|
143
|
+
paragraph = fb.getparent().getparent().getparent()
|
|
144
|
+
runs = [child for child in paragraph if child.tag == f"{HP}run"]
|
|
145
|
+
begin_index = next(
|
|
146
|
+
i for i, run in enumerate(runs)
|
|
147
|
+
if run.find(f"{HP}ctrl/{HP}fieldBegin") is not None
|
|
148
|
+
)
|
|
149
|
+
# value run must carry the surrounding style, not the red prompt style
|
|
150
|
+
assert (
|
|
151
|
+
runs[begin_index + 1].get("charPrIDRef")
|
|
152
|
+
== runs[begin_index].get("charPrIDRef")
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
final = _roundtrip(reopened)
|
|
156
|
+
assert final.list_form_fields()[0]["current_value"] == "홍길동"
|
|
157
|
+
|
|
158
|
+
def test_field_in_table_cell(self) -> None:
|
|
159
|
+
doc = HwpxDocument.new()
|
|
160
|
+
table = doc.add_table(2, 2)
|
|
161
|
+
cell_paragraph = table.cell(0, 0).paragraphs[0]
|
|
162
|
+
doc.add_form_field("셀필드", prompt="셀 입력", paragraph=cell_paragraph)
|
|
163
|
+
reopened = _roundtrip(doc)
|
|
164
|
+
fields = reopened.list_form_fields()
|
|
165
|
+
assert [f["name"] for f in fields] == ["셀필드"]
|
|
166
|
+
reopened.fill_form_field("값123", name="셀필드")
|
|
167
|
+
assert reopened.list_form_fields()[0]["current_value"] == "값123"
|
|
168
|
+
|
|
169
|
+
def test_multiple_fields_pair_independently(self) -> None:
|
|
170
|
+
doc = HwpxDocument.new()
|
|
171
|
+
doc.add_form_field("a", prompt="가")
|
|
172
|
+
doc.add_form_field("b")
|
|
173
|
+
doc.add_form_field("c", prompt="다", memo="m")
|
|
174
|
+
reopened = _roundtrip(doc)
|
|
175
|
+
fields = reopened.list_form_fields()
|
|
176
|
+
assert [f["name"] for f in fields] == ["a", "b", "c"]
|
|
177
|
+
assert all(f["has_end"] for f in fields)
|
|
178
|
+
begin_ids = {f["id"] for f in fields}
|
|
179
|
+
assert len(begin_ids) == 3
|
|
180
|
+
|
|
181
|
+
def test_open_safety(self) -> None:
|
|
182
|
+
doc = HwpxDocument.new()
|
|
183
|
+
doc.add_form_field("f", prompt="입력")
|
|
184
|
+
buffer = io.BytesIO()
|
|
185
|
+
doc.save_to_stream(buffer)
|
|
186
|
+
report = validate_editor_open_safety(buffer.getvalue())
|
|
187
|
+
assert report.ok, report
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class TestValidation:
|
|
191
|
+
def test_blank_name_rejected(self) -> None:
|
|
192
|
+
doc = HwpxDocument.new()
|
|
193
|
+
with pytest.raises(ValueError):
|
|
194
|
+
doc.add_form_field(" ")
|
|
195
|
+
|
|
196
|
+
def test_illegal_control_chars_sanitized(self) -> None:
|
|
197
|
+
doc = HwpxDocument.new()
|
|
198
|
+
created = doc.add_form_field("ok", prompt="안내\x00문")
|
|
199
|
+
assert created["prompt"] == "안내문"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestHancomGoldFixtures:
|
|
203
|
+
"""Reader regression against real Hancom-authored CLICKHERE documents
|
|
204
|
+
(P0 gold pair, Hancom 12.0.0.3288)."""
|
|
205
|
+
|
|
206
|
+
def test_gold_empty_reads_prompt_memo_placeholder(self) -> None:
|
|
207
|
+
doc = HwpxDocument.open(DATA / "clickhere_gold_empty.hwpx")
|
|
208
|
+
(field,) = doc.list_form_fields()
|
|
209
|
+
assert field["name"] == "NAMETEXT_THREE"
|
|
210
|
+
assert field["field_type"] == "CLICK_HERE"
|
|
211
|
+
assert field["prompt"] == "DIRTEXT_ONE"
|
|
212
|
+
assert field["memo"] == "MEMOTEXT_TWO"
|
|
213
|
+
assert field["dirty"] == "0"
|
|
214
|
+
assert field["is_placeholder"] is True
|
|
215
|
+
assert field["current_value"] == "DIRTEXT_ONE"
|
|
216
|
+
|
|
217
|
+
def test_gold_filled_reads_value_not_placeholder(self) -> None:
|
|
218
|
+
doc = HwpxDocument.open(DATA / "clickhere_gold_filled.hwpx")
|
|
219
|
+
(field,) = doc.list_form_fields()
|
|
220
|
+
assert field["current_value"] == "FILLED_VALUE_X"
|
|
221
|
+
assert field["dirty"] == "1"
|
|
222
|
+
assert field["is_placeholder"] is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|