python-hwpx 5.2.0__tar.gz → 5.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-5.2.0/src/python_hwpx.egg-info → python_hwpx-5.3.0}/PKG-INFO +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/pyproject.toml +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/shapes.py +75 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/document.py +42 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/paragraph.py +72 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- python_hwpx-5.3.0/tests/test_chart_authoring.py +172 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/LICENSE +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/NOTICE +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/README.md +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/setup.cfg +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_add_control.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_ingest.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_llms_txt.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_new_features.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.3.0}/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.3.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -282,3 +282,78 @@ def add_equation(
|
|
|
282
282
|
raise RuntimeError(
|
|
283
283
|
"created equation was not recognized by the standard section scan"
|
|
284
284
|
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def add_chart(
|
|
288
|
+
doc: "HwpxDocument",
|
|
289
|
+
chart_xml: bytes | str,
|
|
290
|
+
*,
|
|
291
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
292
|
+
section: HwpxOxmlSection | None = None,
|
|
293
|
+
section_index: int | None = None,
|
|
294
|
+
size: tuple[int, int] | None = None,
|
|
295
|
+
treat_as_char: bool = False,
|
|
296
|
+
char_pr_id_ref: str | int | None = None,
|
|
297
|
+
) -> HwpxOxmlInlineObject:
|
|
298
|
+
"""Add a chartML part and its ``<hp:chart>`` anchor, then prove recognition.
|
|
299
|
+
|
|
300
|
+
The part is stored under ``Chart/chartN.xml`` and addressed directly by
|
|
301
|
+
the anchor's ``chartIDRef`` — real Hancom registers chart parts in no
|
|
302
|
+
manifest and draws the chart from the ECMA-376 chartML alone
|
|
303
|
+
(specs/055-chart-authoring/evidence/p0/chart-contract.md). The chartML is
|
|
304
|
+
validated to parse and to carry the ``c:chartSpace`` root before any part
|
|
305
|
+
is written; after insertion the anchor is re-read through the standard
|
|
306
|
+
section scan — creation fails loudly if it did not land (no
|
|
307
|
+
special-casing by design).
|
|
308
|
+
"""
|
|
309
|
+
from lxml import etree as _etree # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
|
|
310
|
+
|
|
311
|
+
from ..oxml.namespaces import HP
|
|
312
|
+
|
|
313
|
+
_CHART_SPACE = "{http://schemas.openxmlformats.org/drawingml/2006/chart}chartSpace"
|
|
314
|
+
|
|
315
|
+
data = chart_xml.encode("utf-8") if isinstance(chart_xml, str) else bytes(chart_xml)
|
|
316
|
+
if not data.strip():
|
|
317
|
+
raise ValueError("chart_xml must be non-empty chartML")
|
|
318
|
+
try:
|
|
319
|
+
root = _etree.fromstring(data)
|
|
320
|
+
except _etree.XMLSyntaxError as exc:
|
|
321
|
+
raise ValueError(f"chart_xml is not well-formed XML: {exc}") from exc
|
|
322
|
+
if root.tag != _CHART_SPACE:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
"chart_xml root must be the ECMA-376 c:chartSpace element, "
|
|
325
|
+
f"got {root.tag!r}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
existing = {name for name in doc._package.part_names() if name.startswith("Chart/")}
|
|
329
|
+
n = 1
|
|
330
|
+
while f"Chart/chart{n}.xml" in existing:
|
|
331
|
+
n += 1
|
|
332
|
+
part_path = f"Chart/chart{n}.xml"
|
|
333
|
+
|
|
334
|
+
if paragraph is None:
|
|
335
|
+
paragraph = doc.add_paragraph(
|
|
336
|
+
"", section=section, section_index=section_index,
|
|
337
|
+
include_run=False,
|
|
338
|
+
)
|
|
339
|
+
doc._package.write(part_path, data)
|
|
340
|
+
inline_object = paragraph.add_chart(
|
|
341
|
+
part_path,
|
|
342
|
+
size=size,
|
|
343
|
+
treat_as_char=treat_as_char,
|
|
344
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
created_id = inline_object.element.get("id", "")
|
|
348
|
+
for owning_section in doc.sections:
|
|
349
|
+
for candidate in owning_section.element.iter(f"{HP}chart"):
|
|
350
|
+
if candidate.get("id") != created_id:
|
|
351
|
+
continue
|
|
352
|
+
if candidate.get("chartIDRef") != part_path:
|
|
353
|
+
raise RuntimeError(
|
|
354
|
+
"created chart anchor does not reference its part"
|
|
355
|
+
)
|
|
356
|
+
return inline_object
|
|
357
|
+
raise RuntimeError(
|
|
358
|
+
"created chart was not recognized by the standard section scan"
|
|
359
|
+
)
|
|
@@ -1431,6 +1431,48 @@ class HwpxDocument:
|
|
|
1431
1431
|
)
|
|
1432
1432
|
|
|
1433
1433
|
|
|
1434
|
+
def add_chart(
|
|
1435
|
+
self,
|
|
1436
|
+
chart_xml: bytes | str,
|
|
1437
|
+
*,
|
|
1438
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
1439
|
+
section: HwpxOxmlSection | None = None,
|
|
1440
|
+
section_index: int | None = None,
|
|
1441
|
+
size: tuple[int, int] | None = None,
|
|
1442
|
+
treat_as_char: bool = False,
|
|
1443
|
+
char_pr_id_ref: str | int | None = None,
|
|
1444
|
+
) -> HwpxOxmlInlineObject:
|
|
1445
|
+
"""Insert a native chart from ECMA-376 chartML. **Experimental contract.**
|
|
1446
|
+
|
|
1447
|
+
Stores *chart_xml* as a ``Chart/chartN.xml`` package part and emits the
|
|
1448
|
+
real-Hancom ``<hp:chart>`` anchor referencing it via ``chartIDRef``
|
|
1449
|
+
(contract: ``specs/055-chart-authoring/evidence/p0/chart-contract.md``).
|
|
1450
|
+
Hancom draws the chart from the chartML alone — no OLE fallback or
|
|
1451
|
+
pre-rendered image is written. The chartML must parse and carry the
|
|
1452
|
+
``c:chartSpace`` root (typed rejection otherwise), and the created
|
|
1453
|
+
anchor is re-read through the standard section scan — creation fails
|
|
1454
|
+
loudly if the standard consumer would not see it.
|
|
1455
|
+
|
|
1456
|
+
Args:
|
|
1457
|
+
chart_xml: ECMA-376 chartML document (``c:chartSpace``).
|
|
1458
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
1459
|
+
omitted a new paragraph is appended to *section*.
|
|
1460
|
+
size: Optional ``(width, height)`` HWPUNIT pair for the anchor.
|
|
1461
|
+
treat_as_char: ``True`` places the chart inline in the text flow;
|
|
1462
|
+
default mirrors the render-verified gold float placement.
|
|
1463
|
+
"""
|
|
1464
|
+
|
|
1465
|
+
return _shapes.add_chart(
|
|
1466
|
+
self,
|
|
1467
|
+
chart_xml,
|
|
1468
|
+
paragraph=paragraph,
|
|
1469
|
+
section=section,
|
|
1470
|
+
section_index=section_index,
|
|
1471
|
+
size=size,
|
|
1472
|
+
treat_as_char=treat_as_char,
|
|
1473
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1434
1476
|
def add_equation(
|
|
1435
1477
|
self,
|
|
1436
1478
|
script: str,
|
|
@@ -834,6 +834,78 @@ class HwpxOxmlParagraph:
|
|
|
834
834
|
self.section.mark_dirty()
|
|
835
835
|
return HwpxOxmlInlineObject(ctrl1, self)
|
|
836
836
|
|
|
837
|
+
def add_chart(
|
|
838
|
+
self,
|
|
839
|
+
chart_id_ref: str,
|
|
840
|
+
*,
|
|
841
|
+
size: tuple[int, int] | None = None,
|
|
842
|
+
treat_as_char: bool = False,
|
|
843
|
+
char_pr_id_ref: str | int | None = None,
|
|
844
|
+
run_attributes: dict[str, str] | None = None,
|
|
845
|
+
) -> HwpxOxmlInlineObject:
|
|
846
|
+
"""Insert a native ``<hp:chart>`` anchor referencing a chartML part.
|
|
847
|
+
|
|
848
|
+
Emits the real-Hancom chart anchor (contract reverse-engineered in
|
|
849
|
+
specs/055-chart-authoring/evidence/p0/chart-contract.md): the chart
|
|
850
|
+
content lives in an ECMA-376 chartML part addressed directly by
|
|
851
|
+
``chartIDRef`` (real Hancom registers the part in no manifest), and
|
|
852
|
+
Hancom lays the chart out from the chartML alone — no OLE fallback or
|
|
853
|
+
pre-rendered image is required.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
chart_id_ref: Package path of the chartML part
|
|
857
|
+
(e.g. ``Chart/chart1.xml``).
|
|
858
|
+
size: Optional ``(width, height)`` HWPUNIT pair for ``<hp:sz>``;
|
|
859
|
+
defaults to the gold document's 32250x18750.
|
|
860
|
+
treat_as_char: ``True`` places the chart inline in the text flow;
|
|
861
|
+
the default mirrors the gold float placement.
|
|
862
|
+
"""
|
|
863
|
+
reference = chart_id_ref.strip()
|
|
864
|
+
if not reference:
|
|
865
|
+
raise ValueError("chart_id_ref must be a non-empty package path")
|
|
866
|
+
width, height = size if size is not None else (32250, 18750)
|
|
867
|
+
run = self._create_run_for_object(
|
|
868
|
+
run_attributes, char_pr_id_ref=char_pr_id_ref
|
|
869
|
+
)
|
|
870
|
+
chart = _append_child(run, f"{_HP}chart", {
|
|
871
|
+
"id": _object_id(),
|
|
872
|
+
"zOrder": "0",
|
|
873
|
+
"numberingType": "PICTURE",
|
|
874
|
+
"textWrap": "TOP_AND_BOTTOM" if treat_as_char else "SQUARE",
|
|
875
|
+
"textFlow": "BOTH_SIDES",
|
|
876
|
+
"lock": "0",
|
|
877
|
+
"dropcapstyle": "None",
|
|
878
|
+
"chartIDRef": reference,
|
|
879
|
+
})
|
|
880
|
+
_append_child(chart, f"{_HP}sz", {
|
|
881
|
+
"width": str(width),
|
|
882
|
+
"widthRelTo": "ABSOLUTE",
|
|
883
|
+
"height": str(height),
|
|
884
|
+
"heightRelTo": "ABSOLUTE",
|
|
885
|
+
"protect": "0",
|
|
886
|
+
})
|
|
887
|
+
_append_child(chart, f"{_HP}pos", {
|
|
888
|
+
"treatAsChar": "1" if treat_as_char else "0",
|
|
889
|
+
"affectLSpacing": "0",
|
|
890
|
+
"flowWithText": "1",
|
|
891
|
+
"allowOverlap": "0",
|
|
892
|
+
"holdAnchorAndSO": "0",
|
|
893
|
+
"vertRelTo": "PARA",
|
|
894
|
+
"horzRelTo": "PARA" if treat_as_char else "COLUMN",
|
|
895
|
+
"vertAlign": "TOP",
|
|
896
|
+
"horzAlign": "LEFT",
|
|
897
|
+
"vertOffset": "0",
|
|
898
|
+
"horzOffset": "0",
|
|
899
|
+
})
|
|
900
|
+
_append_child(chart, f"{_HP}outMargin", {
|
|
901
|
+
"left": "0",
|
|
902
|
+
"right": "0",
|
|
903
|
+
"top": "0",
|
|
904
|
+
"bottom": "0",
|
|
905
|
+
})
|
|
906
|
+
self.section.mark_dirty()
|
|
907
|
+
return HwpxOxmlInlineObject(chart, self)
|
|
908
|
+
|
|
837
909
|
def add_equation(
|
|
838
910
|
self,
|
|
839
911
|
script: str,
|
|
@@ -120,6 +120,7 @@ tests/test_authoring_quality_corpus.py
|
|
|
120
120
|
tests/test_blind_eval_fixture.py
|
|
121
121
|
tests/test_byte_patch_identity.py
|
|
122
122
|
tests/test_cell_line_spacing.py
|
|
123
|
+
tests/test_chart_authoring.py
|
|
123
124
|
tests/test_comment_node_robustness.py
|
|
124
125
|
tests/test_corpus_read_fidelity.py
|
|
125
126
|
tests/test_coverage_promotion.py
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hwpx import HwpxDocument
|
|
8
|
+
from hwpx.tools.package_validator import validate_editor_open_safety
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
HP = "{http://www.hancom.co.kr/hwpml/2011/paragraph}"
|
|
13
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
14
|
+
GOLD_HANCOM = FIXTURES / "hwpxlib_corpus" / "error__20230426__HwpxTest1.hwpx"
|
|
15
|
+
|
|
16
|
+
CHART_HEAD = (
|
|
17
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>'
|
|
18
|
+
'<c:chartSpace xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
|
|
19
|
+
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
|
20
|
+
'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">'
|
|
21
|
+
"<c:chart><c:plotArea><c:layout/>"
|
|
22
|
+
)
|
|
23
|
+
PIE_CHARTML = (
|
|
24
|
+
CHART_HEAD
|
|
25
|
+
+ '<c:pieChart><c:varyColors val="1"/><c:ser><c:idx val="0"/><c:order val="0"/>'
|
|
26
|
+
'<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$3</c:f><c:strCache><c:ptCount val="2"/>'
|
|
27
|
+
'<c:pt idx="0"><c:v>A</c:v></c:pt><c:pt idx="1"><c:v>B</c:v></c:pt></c:strCache></c:strRef></c:cat>'
|
|
28
|
+
'<c:val><c:numRef><c:f>Sheet1!$B$2:$B$3</c:f><c:numCache><c:formatCode>General</c:formatCode>'
|
|
29
|
+
'<c:ptCount val="2"/><c:pt idx="0"><c:v>60</c:v></c:pt><c:pt idx="1"><c:v>40</c:v></c:pt>'
|
|
30
|
+
"</c:numCache></c:numRef></c:val></c:ser></c:pieChart>"
|
|
31
|
+
"</c:plotArea></c:chart></c:chartSpace>"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _roundtrip(doc: HwpxDocument) -> tuple[HwpxDocument, bytes]:
|
|
36
|
+
buffer = io.BytesIO()
|
|
37
|
+
doc.save_to_stream(buffer)
|
|
38
|
+
payload = buffer.getvalue()
|
|
39
|
+
return HwpxDocument.open(io.BytesIO(payload)), payload
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _anchors(doc: HwpxDocument):
|
|
43
|
+
found = []
|
|
44
|
+
for section in doc.sections:
|
|
45
|
+
found.extend(section.element.iter(f"{HP}chart"))
|
|
46
|
+
return found
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestGoldContractShape:
|
|
50
|
+
"""The emitted anchor must match the real-Hancom chart contract
|
|
51
|
+
(specs/055-chart-authoring/evidence/p0/chart-contract.md)."""
|
|
52
|
+
|
|
53
|
+
def test_anchor_attributes_and_part(self) -> None:
|
|
54
|
+
doc = HwpxDocument.new()
|
|
55
|
+
doc.add_chart(PIE_CHARTML)
|
|
56
|
+
(anchor,) = _anchors(doc)
|
|
57
|
+
assert anchor.get("chartIDRef") == "Chart/chart1.xml"
|
|
58
|
+
assert anchor.get("numberingType") == "PICTURE"
|
|
59
|
+
assert anchor.get("textWrap") == "SQUARE"
|
|
60
|
+
assert anchor.get("id")
|
|
61
|
+
local_names = [child.tag.rsplit("}", 1)[-1] for child in anchor]
|
|
62
|
+
assert local_names == ["sz", "pos", "outMargin"]
|
|
63
|
+
assert anchor.find(f"{HP}pos").get("treatAsChar") == "0"
|
|
64
|
+
|
|
65
|
+
def test_part_written_verbatim_and_unregistered(self) -> None:
|
|
66
|
+
doc = HwpxDocument.new()
|
|
67
|
+
doc.add_chart(PIE_CHARTML)
|
|
68
|
+
_reopened, payload = _roundtrip(doc)
|
|
69
|
+
with zipfile.ZipFile(io.BytesIO(payload)) as package:
|
|
70
|
+
assert package.read("Chart/chart1.xml") == PIE_CHARTML.encode("utf-8")
|
|
71
|
+
hpf = package.read("Contents/content.hpf").decode("utf-8")
|
|
72
|
+
# Real Hancom registers chart parts in no manifest (gold contract).
|
|
73
|
+
assert "chart1" not in hpf
|
|
74
|
+
|
|
75
|
+
def test_part_paths_allocate_sequentially(self) -> None:
|
|
76
|
+
doc = HwpxDocument.new()
|
|
77
|
+
doc.add_chart(PIE_CHARTML)
|
|
78
|
+
doc.add_chart(PIE_CHARTML)
|
|
79
|
+
refs = [anchor.get("chartIDRef") for anchor in _anchors(doc)]
|
|
80
|
+
assert refs == ["Chart/chart1.xml", "Chart/chart2.xml"]
|
|
81
|
+
|
|
82
|
+
def test_inline_placement(self) -> None:
|
|
83
|
+
doc = HwpxDocument.new()
|
|
84
|
+
doc.add_chart(PIE_CHARTML, treat_as_char=True, size=(20000, 12000))
|
|
85
|
+
(anchor,) = _anchors(doc)
|
|
86
|
+
assert anchor.get("textWrap") == "TOP_AND_BOTTOM"
|
|
87
|
+
pos = anchor.find(f"{HP}pos")
|
|
88
|
+
assert pos.get("treatAsChar") == "1"
|
|
89
|
+
sz = anchor.find(f"{HP}sz")
|
|
90
|
+
assert (sz.get("width"), sz.get("height")) == ("20000", "12000")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestSelfRoundtrip:
|
|
94
|
+
def test_reopen_keeps_anchor_and_part_bytes(self) -> None:
|
|
95
|
+
doc = HwpxDocument.new()
|
|
96
|
+
doc.add_chart(PIE_CHARTML)
|
|
97
|
+
reopened, _payload = _roundtrip(doc)
|
|
98
|
+
(anchor,) = _anchors(reopened)
|
|
99
|
+
assert anchor.get("chartIDRef") == "Chart/chart1.xml"
|
|
100
|
+
# patch-grade resave must keep the chart part byte-identical
|
|
101
|
+
_again, payload2 = _roundtrip(reopened)
|
|
102
|
+
with zipfile.ZipFile(io.BytesIO(payload2)) as package:
|
|
103
|
+
assert package.read("Chart/chart1.xml") == PIE_CHARTML.encode("utf-8")
|
|
104
|
+
|
|
105
|
+
def test_chart_in_table_cell(self) -> None:
|
|
106
|
+
doc = HwpxDocument.new()
|
|
107
|
+
table = doc.add_table(2, 2)
|
|
108
|
+
cell_paragraph = table.cell(0, 0).paragraphs[0]
|
|
109
|
+
doc.add_chart(PIE_CHARTML, paragraph=cell_paragraph)
|
|
110
|
+
reopened, _payload = _roundtrip(doc)
|
|
111
|
+
(anchor,) = _anchors(reopened)
|
|
112
|
+
ancestors = []
|
|
113
|
+
parent = anchor.getparent()
|
|
114
|
+
while parent is not None:
|
|
115
|
+
ancestors.append(parent.tag.rsplit("}", 1)[-1])
|
|
116
|
+
parent = parent.getparent()
|
|
117
|
+
assert "tc" in ancestors
|
|
118
|
+
|
|
119
|
+
def test_byte_preservation_of_unrelated_parts(self) -> None:
|
|
120
|
+
doc = HwpxDocument.open(GOLD_HANCOM)
|
|
121
|
+
with zipfile.ZipFile(GOLD_HANCOM) as package:
|
|
122
|
+
before = {name: package.read(name) for name in package.namelist()}
|
|
123
|
+
doc.add_chart(PIE_CHARTML)
|
|
124
|
+
buffer = io.BytesIO()
|
|
125
|
+
doc.save_to_stream(buffer)
|
|
126
|
+
with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as package:
|
|
127
|
+
after = {name: package.read(name) for name in package.namelist()}
|
|
128
|
+
changed = {
|
|
129
|
+
name for name in before if name in after and before[name] != after[name]
|
|
130
|
+
}
|
|
131
|
+
assert changed <= {"Contents/section0.xml"}
|
|
132
|
+
# The gold file already carries Chart/chart1.xml — ours must not clobber it.
|
|
133
|
+
assert after["Chart/chart1.xml"] == before["Chart/chart1.xml"]
|
|
134
|
+
assert _anchors(HwpxDocument.open(io.BytesIO(buffer.getvalue())))[-1].get(
|
|
135
|
+
"chartIDRef"
|
|
136
|
+
) == "Chart/chart2.xml"
|
|
137
|
+
|
|
138
|
+
def test_open_safety(self) -> None:
|
|
139
|
+
doc = HwpxDocument.new()
|
|
140
|
+
doc.add_chart(PIE_CHARTML)
|
|
141
|
+
buffer = io.BytesIO()
|
|
142
|
+
doc.save_to_stream(buffer)
|
|
143
|
+
report = validate_editor_open_safety(buffer.getvalue())
|
|
144
|
+
assert report.ok, report
|
|
145
|
+
|
|
146
|
+
def test_gold_fixture_anchor_still_reads(self) -> None:
|
|
147
|
+
doc = HwpxDocument.open(GOLD_HANCOM)
|
|
148
|
+
anchors = _anchors(doc)
|
|
149
|
+
assert anchors
|
|
150
|
+
assert anchors[0].get("chartIDRef") == "Chart/chart1.xml"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestValidation:
|
|
154
|
+
def test_empty_chartml_rejected(self) -> None:
|
|
155
|
+
doc = HwpxDocument.new()
|
|
156
|
+
with pytest.raises(ValueError):
|
|
157
|
+
doc.add_chart(" ")
|
|
158
|
+
|
|
159
|
+
def test_malformed_xml_rejected_before_any_write(self) -> None:
|
|
160
|
+
doc = HwpxDocument.new()
|
|
161
|
+
with pytest.raises(ValueError):
|
|
162
|
+
doc.add_chart("<c:chartSpace")
|
|
163
|
+
assert _anchors(doc) == []
|
|
164
|
+
buffer = io.BytesIO()
|
|
165
|
+
doc.save_to_stream(buffer)
|
|
166
|
+
with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as package:
|
|
167
|
+
assert not [n for n in package.namelist() if n.startswith("Chart/")]
|
|
168
|
+
|
|
169
|
+
def test_wrong_root_rejected(self) -> None:
|
|
170
|
+
doc = HwpxDocument.new()
|
|
171
|
+
with pytest.raises(ValueError, match="chartSpace"):
|
|
172
|
+
doc.add_chart("<not-a-chart/>")
|
|
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
|
|
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
|