python-hwpx 5.0.1__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.1/src/python_hwpx.egg-info → python_hwpx-5.1.1}/PKG-INFO +69 -55
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/README.md +65 -54
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/pyproject.toml +4 -1
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/fields.py +81 -1
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/document.py +42 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/opc/package.py +17 -4
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/paragraph.py +100 -1
- {python_hwpx-5.0.1 → python_hwpx-5.1.1/src/python_hwpx.egg-info}/PKG-INFO +69 -55
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/SOURCES.txt +3 -0
- python_hwpx-5.1.1/tests/test_adoption_metrics_snapshot.py +90 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_documentation_code_fences.py +8 -6
- python_hwpx-5.1.1/tests/test_form_field_authoring.py +222 -0
- python_hwpx-5.1.1/tests/test_llms_txt.py +62 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_opc_package.py +39 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_release_workflow_safety.py +67 -4
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/LICENSE +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/NOTICE +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/setup.cfg +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/package.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/patch.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_add_control.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_ingest.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_new_features.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.0.1 → python_hwpx-5.1.1}/tests/test_version_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.1.1
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,8 +14,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
20
|
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
21
|
+
Classifier: Typing :: Typed
|
|
19
22
|
Requires-Python: >=3.10
|
|
20
23
|
Description-Content-Type: text/markdown
|
|
21
24
|
License-File: LICENSE
|
|
@@ -63,26 +66,33 @@ Dynamic: license-file
|
|
|
63
66
|
|
|
64
67
|
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
한컴오피스가 없어도 됩니다. HWPX는 ZIP+XML(OWPML) 포맷이라 순수 파이썬만으로
|
|
70
|
+
읽고, 고치고, 새로 만들 수 있습니다 — Windows·macOS·Linux·CI, 그리고
|
|
71
|
+
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존 문서는 손댄
|
|
72
|
+
곳만 바뀌고 나머지는 바이트 그대로 유지되며, 새 문서는 실제 한컴오피스가 여는
|
|
73
|
+
형태로 만들어집니다.
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| Judgment | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) | 에이전트 intent/genre 판단·ambiguity 처리·plugin/skill 가이드 |
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/chatgpt-formfill.png" width="760" alt="일반 ChatGPT 대화에 hwpx 양식을 올려 지도안 작성을 부탁하고, 양식이 유지된 채 채워진 문서를 돌려받는 화면">
|
|
77
|
+
</p>
|
|
78
|
+
<p align="center"><sub>일반 ChatGPT 대화 — 양식 <code>.hwpx</code>를 올리고 말로 부탁하면, 서식을 유지한 채 채워진 문서가 돌아옵니다.</sub></p>
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
방지, 의존성 허용 범위는
|
|
80
|
-
[제품 경계 문서](docs/architecture/product-boundary.md)에 고정돼 있습니다.
|
|
80
|
+
**ChatGPT에서 그대로 따라 하기** — 문서를 올리면서 이렇게 부탁하면 됩니다:
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
```text
|
|
83
|
+
이 .hwpx 파일을 python-hwpx 라이브러리로 열어서 작업해줘.
|
|
84
|
+
(pip install python-hwpx 로 설치하면 돼)
|
|
85
|
+
양식과 서식은 그대로 두고, ○○만 바꿔서 새 파일로 돌려줘.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
설치부터 결과 파일까지 대화 안에서 끝납니다 — 내 컴퓨터에 파이썬이 없어도 됩니다.
|
|
89
|
+
AI 도구가 정확한 API를 배우도록 [llms.txt](https://airmang.github.io/python-hwpx/llms.txt)도 제공합니다.
|
|
90
|
+
|
|
91
|
+
| | 저장소 | 역할 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| 📦 | [`python-hwpx`](https://github.com/airmang/python-hwpx) | HWPX 문서를 읽고·고치고·만드는 순수 파이썬 엔진 |
|
|
94
|
+
| 🔌 | [`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation) | 저작·양식 채움 워크플로, `hwpx` CLI, 선택형 MCP 서버 |
|
|
95
|
+
| 🎯 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) | 에이전트가 알맞은 도구를 고르도록 돕는 플러그인/스킬 번들 |
|
|
86
96
|
|
|
87
97
|
## 시작하기
|
|
88
98
|
|
|
@@ -90,11 +100,6 @@ Windows도 필요하지 않으므로 **ChatGPT 채팅 환경에서도 HWPX 문
|
|
|
90
100
|
pip install python-hwpx # Python 3.10+
|
|
91
101
|
```
|
|
92
102
|
|
|
93
|
-
> **5.x 호환 extra:** `pip install "python-hwpx[visual]"`은 기존 설치 명령을
|
|
94
|
-
> 깨뜨리지 않기 위해 계속 허용되지만, `visual` extra는 비어 있어 PDF·이미지
|
|
95
|
-
> 렌더링 의존성을 설치하지 않습니다. 렌더·PDF 이미징 실행은
|
|
96
|
-
> `python-hwpx-automation[oracle]`이 소유합니다.
|
|
97
|
-
|
|
98
103
|
```python
|
|
99
104
|
from hwpx import HwpxDocument
|
|
100
105
|
|
|
@@ -103,23 +108,24 @@ doc.add_paragraph("자동화로 추가한 문단입니다.")
|
|
|
103
108
|
doc.save_to_path("보고서-수정.hwpx")
|
|
104
109
|
```
|
|
105
110
|
|
|
106
|
-
백지에서
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
문서 저작·양식 채움·시험지 조판 같은 상위 워크플로가 필요하면
|
|
111
|
+
백지에서 시작할 때는 `HwpxDocument.new()`로 만들어 같은 API로 문단·표·머리말을
|
|
112
|
+
채우면 됩니다. 문서 저작·양식 채움·시험지 조판 같은 상위 워크플로가 필요하면
|
|
110
113
|
[`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)을
|
|
111
|
-
함께 설치하세요.
|
|
114
|
+
함께 설치하세요.
|
|
115
|
+
|
|
116
|
+
> 기존 설치 명령 호환을 위해 `pip install "python-hwpx[visual]"`도 계속
|
|
117
|
+
> 동작합니다. 다만 이 extra는 비어 있어 렌더·PDF 의존성을 설치하지 않습니다 —
|
|
118
|
+
> 그 역할은 `python-hwpx-automation[oracle]`이 맡습니다.
|
|
112
119
|
|
|
113
120
|
## 무엇을 하나
|
|
114
121
|
|
|
115
|
-
- **읽기·추출** — 텍스트/HTML/
|
|
116
|
-
- **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호
|
|
117
|
-
- **양식 채우기** —
|
|
118
|
-
- **생성·일괄 처리** —
|
|
119
|
-
-
|
|
120
|
-
- **검증·안전** — 패키지 구조 검증 CLI, 열림 안전 게이트, 모든 쓰기에 영수증(`MutationReport`) — 번들 XSD는 느슨한 구조 스키마이며 OWPML 전체 검증은 아닙니다
|
|
122
|
+
- **읽기·추출** — 텍스트/HTML/Markdown 내보내기 (서식·중첩 표·각주 보존)
|
|
123
|
+
- **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호 같은 서식
|
|
124
|
+
- **양식 채우기** — 라벨로 셀을 찾아 값만 채우기, 행·열 조정 같은 구조 편집도 바이트 보존으로
|
|
125
|
+
- **생성·일괄 처리** — 새 문서 저작, 목차·상호참조, mail merge, 텍스트 diff, 변경추적(redline)
|
|
126
|
+
- **검증·안전** — 패키지 구조 검증 CLI, 열림 안전 게이트, 모든 저장에 영수증(`MutationReport`)
|
|
121
127
|
|
|
122
|
-
자세한 내용: [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [
|
|
128
|
+
자세한 내용: [5분 퀵스타트](docs/quickstart.md) · [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [예제](docs/examples.md)
|
|
123
129
|
|
|
124
130
|
### 양식 채우기 — 서식은 그대로, 값만
|
|
125
131
|
|
|
@@ -144,22 +150,31 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
144
150
|
```
|
|
145
151
|
|
|
146
152
|
요청한 보존 등급을 지킬 수 없으면 아무것도 쓰지 않고 실패합니다(fail-closed).
|
|
147
|
-
전체
|
|
153
|
+
전체 규칙: [안전한 쓰기 계약](docs/safe-write-contract.md).
|
|
154
|
+
|
|
155
|
+
*예제 중 **독립 실행 예제** 표시가 없는 블록은 여러분의 기존 문서를 입력으로
|
|
156
|
+
쓰는 조각입니다. 예제별 Python 블록 판정은
|
|
157
|
+
[실행 ledger](docs/python-example-ledger.json)에 동결돼 있습니다.*
|
|
148
158
|
|
|
149
159
|
## 실측으로 말합니다
|
|
150
160
|
|
|
151
|
-
|
|
161
|
+
만든 파일이 실제 한컴오피스에서 열리는지, 동결 코퍼스 전수(N=497)를 재서
|
|
162
|
+
그대로 공개합니다:
|
|
152
163
|
|
|
153
|
-
- **한컴
|
|
154
|
-
- **미수정 영역 바이트 보존 497/497** · 개인정보 0
|
|
155
|
-
- **렌더 검증 416/476**
|
|
156
|
-
-
|
|
164
|
+
- **한컴 열림 476/476** — 우리가 만든 파일을 실한컴이 전부 엽니다
|
|
165
|
+
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
166
|
+
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
167
|
+
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
168
|
+
|
|
169
|
+
<p align="center">
|
|
170
|
+
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/hancom-open-generated.png" width="760" alt="ChatGPT 실행 환경에서 python-hwpx로 생성한 HWPX 문서를 실제 한컴오피스에서 연 모습">
|
|
171
|
+
</p>
|
|
172
|
+
<p align="center"><sub>ChatGPT 실행 환경에서 생성한 문서를 실제 한컴오피스로 연 모습.</sub></p>
|
|
157
173
|
|
|
158
|
-
|
|
159
|
-
명시되어 있습니다. 현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
|
|
174
|
+
현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
|
|
160
175
|
|
|
161
|
-
> 위 수치는
|
|
162
|
-
> 다른 축이므로 파서 프로젝트 수치와
|
|
176
|
+
> 위 수치는 "만든 파일을 실한컴이 받아주는가"라는 축입니다. 문서 파싱 성능과는
|
|
177
|
+
> 다른 축이므로 파서 프로젝트 수치와 직접 비교하지 마세요.
|
|
163
178
|
|
|
164
179
|
## 어디서 쓰나
|
|
165
180
|
|
|
@@ -172,11 +187,10 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
172
187
|
| Codex 마켓플레이스 플러그인 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) 설치 |
|
|
173
188
|
| Claude Code · Hermes · OpenClaw | 같은 MCP 서버를 각 클라이언트에 등록 ([`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)) |
|
|
174
189
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
같은 여정이 됩니다.
|
|
190
|
+
실제로 일반 ChatGPT 대화에 `.hwpx`를 올리고 설치를 부탁했을 때 PyPI 설치와
|
|
191
|
+
편집·되받기까지 동작하는 것을 확인했습니다. 파이썬 실행과 네트워크 허용 범위는
|
|
192
|
+
플랜·설정에 따라 다르며, 네트워크가 막힌 환경에서는 wheel 파일을 함께 올리면
|
|
193
|
+
오프라인 설치로 같은 여정이 됩니다.
|
|
180
194
|
|
|
181
195
|
## 비교
|
|
182
196
|
|
|
@@ -192,11 +206,10 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
192
206
|
|
|
193
207
|
## 알려진 제약
|
|
194
208
|
|
|
195
|
-
- `add_shape()` / `add_control()`은
|
|
196
|
-
|
|
197
|
-
경고 하나뿐입니다(저장도 패키지 검증도 막지 않습니다). 도형은 `add_line()` /
|
|
209
|
+
- `add_shape()` / `add_control()`은 저수준 탈출구라, 그대로 저장하면 한/글이
|
|
210
|
+
열지 못하는 파일이 됩니다(호출 시 경고만 나옵니다). 도형은 `add_line()` /
|
|
198
211
|
`add_rectangle()` / `add_ellipse()`를 쓰세요.
|
|
199
|
-
- 그림은 단순 개체 생성까지
|
|
212
|
+
- 그림은 단순 개체 생성까지 지원합니다 (그룹·효과 미지원).
|
|
200
213
|
- 암호화된 HWPX는 지원하지 않습니다.
|
|
201
214
|
|
|
202
215
|
## 기여하기
|
|
@@ -204,11 +217,12 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
204
217
|
[help wanted](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) ·
|
|
205
218
|
[로드맵](https://github.com/airmang/python-hwpx/milestones) ·
|
|
206
219
|
[Discussions](https://github.com/airmang/python-hwpx/discussions) ·
|
|
207
|
-
[내부 실전 가이드](docs/internals/) ·
|
|
208
220
|
[CONTRIBUTING](CONTRIBUTING.md)
|
|
209
221
|
|
|
210
222
|
HWPX 내부 구조가 처음이라면 [내부 실전 가이드](docs/internals/)부터 — 실제 한/글
|
|
211
223
|
동작에서 확인된 조판 캐시·목차 필드·OPC 재패킹 같은 실전 지식을 정리해 두었습니다.
|
|
224
|
+
공개 API의 안정 범위는 [안정 API 표면](docs/stable-api.md), 계층 간 소유권은
|
|
225
|
+
[제품 경계 문서](docs/architecture/product-boundary.md)에 있습니다.
|
|
212
226
|
|
|
213
227
|
## 감사의 말
|
|
214
228
|
|
|
@@ -15,26 +15,33 @@
|
|
|
15
15
|
|
|
16
16
|
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
한컴오피스가 없어도 됩니다. HWPX는 ZIP+XML(OWPML) 포맷이라 순수 파이썬만으로
|
|
19
|
+
읽고, 고치고, 새로 만들 수 있습니다 — Windows·macOS·Linux·CI, 그리고
|
|
20
|
+
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존 문서는 손댄
|
|
21
|
+
곳만 바뀌고 나머지는 바이트 그대로 유지되며, 새 문서는 실제 한컴오피스가 여는
|
|
22
|
+
형태로 만들어집니다.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
| Judgment | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) | 에이전트 intent/genre 판단·ambiguity 처리·plugin/skill 가이드 |
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/chatgpt-formfill.png" width="760" alt="일반 ChatGPT 대화에 hwpx 양식을 올려 지도안 작성을 부탁하고, 양식이 유지된 채 채워진 문서를 돌려받는 화면">
|
|
26
|
+
</p>
|
|
27
|
+
<p align="center"><sub>일반 ChatGPT 대화 — 양식 <code>.hwpx</code>를 올리고 말로 부탁하면, 서식을 유지한 채 채워진 문서가 돌아옵니다.</sub></p>
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
방지, 의존성 허용 범위는
|
|
32
|
-
[제품 경계 문서](docs/architecture/product-boundary.md)에 고정돼 있습니다.
|
|
29
|
+
**ChatGPT에서 그대로 따라 하기** — 문서를 올리면서 이렇게 부탁하면 됩니다:
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
```text
|
|
32
|
+
이 .hwpx 파일을 python-hwpx 라이브러리로 열어서 작업해줘.
|
|
33
|
+
(pip install python-hwpx 로 설치하면 돼)
|
|
34
|
+
양식과 서식은 그대로 두고, ○○만 바꿔서 새 파일로 돌려줘.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
설치부터 결과 파일까지 대화 안에서 끝납니다 — 내 컴퓨터에 파이썬이 없어도 됩니다.
|
|
38
|
+
AI 도구가 정확한 API를 배우도록 [llms.txt](https://airmang.github.io/python-hwpx/llms.txt)도 제공합니다.
|
|
39
|
+
|
|
40
|
+
| | 저장소 | 역할 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| 📦 | [`python-hwpx`](https://github.com/airmang/python-hwpx) | HWPX 문서를 읽고·고치고·만드는 순수 파이썬 엔진 |
|
|
43
|
+
| 🔌 | [`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation) | 저작·양식 채움 워크플로, `hwpx` CLI, 선택형 MCP 서버 |
|
|
44
|
+
| 🎯 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) | 에이전트가 알맞은 도구를 고르도록 돕는 플러그인/스킬 번들 |
|
|
38
45
|
|
|
39
46
|
## 시작하기
|
|
40
47
|
|
|
@@ -42,11 +49,6 @@ Windows도 필요하지 않으므로 **ChatGPT 채팅 환경에서도 HWPX 문
|
|
|
42
49
|
pip install python-hwpx # Python 3.10+
|
|
43
50
|
```
|
|
44
51
|
|
|
45
|
-
> **5.x 호환 extra:** `pip install "python-hwpx[visual]"`은 기존 설치 명령을
|
|
46
|
-
> 깨뜨리지 않기 위해 계속 허용되지만, `visual` extra는 비어 있어 PDF·이미지
|
|
47
|
-
> 렌더링 의존성을 설치하지 않습니다. 렌더·PDF 이미징 실행은
|
|
48
|
-
> `python-hwpx-automation[oracle]`이 소유합니다.
|
|
49
|
-
|
|
50
52
|
```python
|
|
51
53
|
from hwpx import HwpxDocument
|
|
52
54
|
|
|
@@ -55,23 +57,24 @@ doc.add_paragraph("자동화로 추가한 문단입니다.")
|
|
|
55
57
|
doc.save_to_path("보고서-수정.hwpx")
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
백지에서
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
문서 저작·양식 채움·시험지 조판 같은 상위 워크플로가 필요하면
|
|
60
|
+
백지에서 시작할 때는 `HwpxDocument.new()`로 만들어 같은 API로 문단·표·머리말을
|
|
61
|
+
채우면 됩니다. 문서 저작·양식 채움·시험지 조판 같은 상위 워크플로가 필요하면
|
|
62
62
|
[`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)을
|
|
63
|
-
함께 설치하세요.
|
|
63
|
+
함께 설치하세요.
|
|
64
|
+
|
|
65
|
+
> 기존 설치 명령 호환을 위해 `pip install "python-hwpx[visual]"`도 계속
|
|
66
|
+
> 동작합니다. 다만 이 extra는 비어 있어 렌더·PDF 의존성을 설치하지 않습니다 —
|
|
67
|
+
> 그 역할은 `python-hwpx-automation[oracle]`이 맡습니다.
|
|
64
68
|
|
|
65
69
|
## 무엇을 하나
|
|
66
70
|
|
|
67
|
-
- **읽기·추출** — 텍스트/HTML/
|
|
68
|
-
- **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호
|
|
69
|
-
- **양식 채우기** —
|
|
70
|
-
- **생성·일괄 처리** —
|
|
71
|
-
-
|
|
72
|
-
- **검증·안전** — 패키지 구조 검증 CLI, 열림 안전 게이트, 모든 쓰기에 영수증(`MutationReport`) — 번들 XSD는 느슨한 구조 스키마이며 OWPML 전체 검증은 아닙니다
|
|
71
|
+
- **읽기·추출** — 텍스트/HTML/Markdown 내보내기 (서식·중첩 표·각주 보존)
|
|
72
|
+
- **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호 같은 서식
|
|
73
|
+
- **양식 채우기** — 라벨로 셀을 찾아 값만 채우기, 행·열 조정 같은 구조 편집도 바이트 보존으로
|
|
74
|
+
- **생성·일괄 처리** — 새 문서 저작, 목차·상호참조, mail merge, 텍스트 diff, 변경추적(redline)
|
|
75
|
+
- **검증·안전** — 패키지 구조 검증 CLI, 열림 안전 게이트, 모든 저장에 영수증(`MutationReport`)
|
|
73
76
|
|
|
74
|
-
자세한 내용: [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [
|
|
77
|
+
자세한 내용: [5분 퀵스타트](docs/quickstart.md) · [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [예제](docs/examples.md)
|
|
75
78
|
|
|
76
79
|
### 양식 채우기 — 서식은 그대로, 값만
|
|
77
80
|
|
|
@@ -96,22 +99,31 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
요청한 보존 등급을 지킬 수 없으면 아무것도 쓰지 않고 실패합니다(fail-closed).
|
|
99
|
-
전체
|
|
102
|
+
전체 규칙: [안전한 쓰기 계약](docs/safe-write-contract.md).
|
|
103
|
+
|
|
104
|
+
*예제 중 **독립 실행 예제** 표시가 없는 블록은 여러분의 기존 문서를 입력으로
|
|
105
|
+
쓰는 조각입니다. 예제별 Python 블록 판정은
|
|
106
|
+
[실행 ledger](docs/python-example-ledger.json)에 동결돼 있습니다.*
|
|
100
107
|
|
|
101
108
|
## 실측으로 말합니다
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
만든 파일이 실제 한컴오피스에서 열리는지, 동결 코퍼스 전수(N=497)를 재서
|
|
111
|
+
그대로 공개합니다:
|
|
104
112
|
|
|
105
|
-
- **한컴
|
|
106
|
-
- **미수정 영역 바이트 보존 497/497** · 개인정보 0
|
|
107
|
-
- **렌더 검증 416/476**
|
|
108
|
-
-
|
|
113
|
+
- **한컴 열림 476/476** — 우리가 만든 파일을 실한컴이 전부 엽니다
|
|
114
|
+
- **미수정 영역 바이트 보존 497/497** · 개인정보 유출 0
|
|
115
|
+
- **렌더 검증 416/476** — 한컴 자체가 PDF 내보내기를 거부한 43건도 숨기지 않고 집계
|
|
116
|
+
- 전체 수치와 주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html) · 기능별 등급: [지원 매트릭스](docs/support-matrix.md)
|
|
117
|
+
|
|
118
|
+
<p align="center">
|
|
119
|
+
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/hancom-open-generated.png" width="760" alt="ChatGPT 실행 환경에서 python-hwpx로 생성한 HWPX 문서를 실제 한컴오피스에서 연 모습">
|
|
120
|
+
</p>
|
|
121
|
+
<p align="center"><sub>ChatGPT 실행 환경에서 생성한 문서를 실제 한컴오피스로 연 모습.</sub></p>
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
명시되어 있습니다. 현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
|
|
123
|
+
현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
|
|
112
124
|
|
|
113
|
-
> 위 수치는
|
|
114
|
-
> 다른 축이므로 파서 프로젝트 수치와
|
|
125
|
+
> 위 수치는 "만든 파일을 실한컴이 받아주는가"라는 축입니다. 문서 파싱 성능과는
|
|
126
|
+
> 다른 축이므로 파서 프로젝트 수치와 직접 비교하지 마세요.
|
|
115
127
|
|
|
116
128
|
## 어디서 쓰나
|
|
117
129
|
|
|
@@ -124,11 +136,10 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
124
136
|
| Codex 마켓플레이스 플러그인 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) 설치 |
|
|
125
137
|
| Claude Code · Hermes · OpenClaw | 같은 MCP 서버를 각 클라이언트에 등록 ([`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)) |
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
같은 여정이 됩니다.
|
|
139
|
+
실제로 일반 ChatGPT 대화에 `.hwpx`를 올리고 설치를 부탁했을 때 PyPI 설치와
|
|
140
|
+
편집·되받기까지 동작하는 것을 확인했습니다. 파이썬 실행과 네트워크 허용 범위는
|
|
141
|
+
플랜·설정에 따라 다르며, 네트워크가 막힌 환경에서는 wheel 파일을 함께 올리면
|
|
142
|
+
오프라인 설치로 같은 여정이 됩니다.
|
|
132
143
|
|
|
133
144
|
## 비교
|
|
134
145
|
|
|
@@ -144,11 +155,10 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
144
155
|
|
|
145
156
|
## 알려진 제약
|
|
146
157
|
|
|
147
|
-
- `add_shape()` / `add_control()`은
|
|
148
|
-
|
|
149
|
-
경고 하나뿐입니다(저장도 패키지 검증도 막지 않습니다). 도형은 `add_line()` /
|
|
158
|
+
- `add_shape()` / `add_control()`은 저수준 탈출구라, 그대로 저장하면 한/글이
|
|
159
|
+
열지 못하는 파일이 됩니다(호출 시 경고만 나옵니다). 도형은 `add_line()` /
|
|
150
160
|
`add_rectangle()` / `add_ellipse()`를 쓰세요.
|
|
151
|
-
- 그림은 단순 개체 생성까지
|
|
161
|
+
- 그림은 단순 개체 생성까지 지원합니다 (그룹·효과 미지원).
|
|
152
162
|
- 암호화된 HWPX는 지원하지 않습니다.
|
|
153
163
|
|
|
154
164
|
## 기여하기
|
|
@@ -156,11 +166,12 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
156
166
|
[help wanted](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) ·
|
|
157
167
|
[로드맵](https://github.com/airmang/python-hwpx/milestones) ·
|
|
158
168
|
[Discussions](https://github.com/airmang/python-hwpx/discussions) ·
|
|
159
|
-
[내부 실전 가이드](docs/internals/) ·
|
|
160
169
|
[CONTRIBUTING](CONTRIBUTING.md)
|
|
161
170
|
|
|
162
171
|
HWPX 내부 구조가 처음이라면 [내부 실전 가이드](docs/internals/)부터 — 실제 한/글
|
|
163
172
|
동작에서 확인된 조판 캐시·목차 필드·OPC 재패킹 같은 실전 지식을 정리해 두었습니다.
|
|
173
|
+
공개 API의 안정 범위는 [안정 API 표면](docs/stable-api.md), 계층 간 소유권은
|
|
174
|
+
[제품 경계 문서](docs/architecture/product-boundary.md)에 있습니다.
|
|
164
175
|
|
|
165
176
|
## 감사의 말
|
|
166
177
|
|
|
@@ -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"
|
|
@@ -21,8 +21,11 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.10",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
24
26
|
"Topic :: Software Development :: Libraries",
|
|
25
27
|
"Topic :: Text Processing :: Markup :: XML",
|
|
28
|
+
"Typing :: Typed",
|
|
26
29
|
]
|
|
27
30
|
dependencies = [
|
|
28
31
|
"lxml>=4.9,<7",
|
|
@@ -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,
|