python-hwpx 5.2.0__tar.gz → 5.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-5.2.0/src/python_hwpx.egg-info → python_hwpx-5.4.0}/PKG-INFO +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/pyproject.toml +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/layout.py +12 -1
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/shapes.py +75 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/document.py +76 -2
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/_document_primitives.py +17 -2
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/document_parts.py +188 -4
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/header_part.py +16 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/paragraph.py +133 -4
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/section.py +53 -10
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/table.py +15 -2
- {python_hwpx-5.2.0 → python_hwpx-5.4.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/python_hwpx.egg-info/SOURCES.txt +2 -0
- python_hwpx-5.4.0/tests/test_authoring_defaults.py +229 -0
- python_hwpx-5.4.0/tests/test_chart_authoring.py +172 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_form_fit_integration.py +8 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/LICENSE +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/NOTICE +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/README.md +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/setup.cfg +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_add_control.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_equation_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_equation_converter.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_equation_render.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_golden_api.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_ingest.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_llms_txt.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_new_features.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_product_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-5.2.0 → python_hwpx-5.4.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.4.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -425,17 +425,28 @@ def add_hyperlink(
|
|
|
425
425
|
paragraph: HwpxOxmlParagraph | None = None,
|
|
426
426
|
section: HwpxOxmlSection | None = None,
|
|
427
427
|
section_index: int | None = None,
|
|
428
|
+
char_pr_id_ref: str | int | None = None,
|
|
428
429
|
) -> HwpxOxmlInlineObject:
|
|
429
430
|
"""Insert a hyperlink (fieldBegin + text + fieldEnd).
|
|
430
431
|
|
|
432
|
+
The display text follows the Hancom convention (blue ``#0000FF`` text
|
|
433
|
+
with a blue bottom underline — dominant styling across real-corpus
|
|
434
|
+
hyperlinks) unless ``char_pr_id_ref`` overrides it.
|
|
435
|
+
|
|
431
436
|
Returns the ``<hp:ctrl>`` wrapper containing the ``<hp:fieldBegin>``.
|
|
432
437
|
"""
|
|
438
|
+
if char_pr_id_ref is None:
|
|
439
|
+
char_pr_id_ref = doc.ensure_run_style(
|
|
440
|
+
underline=True,
|
|
441
|
+
color="#0000FF",
|
|
442
|
+
underline_color="#0000FF",
|
|
443
|
+
)
|
|
433
444
|
if paragraph is None:
|
|
434
445
|
paragraph = doc.add_paragraph(
|
|
435
446
|
"", section=section, section_index=section_index,
|
|
436
447
|
include_run=False,
|
|
437
448
|
)
|
|
438
|
-
return paragraph.add_hyperlink(url, display_text)
|
|
449
|
+
return paragraph.add_hyperlink(url, display_text, char_pr_id_ref=char_pr_id_ref)
|
|
439
450
|
|
|
440
451
|
|
|
441
452
|
def _resolve_section(
|
|
@@ -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
|
+
)
|
|
@@ -264,14 +264,21 @@ class HwpxDocument:
|
|
|
264
264
|
border_width: str = "0.12 mm",
|
|
265
265
|
fill_color: str | None = None,
|
|
266
266
|
active_borders: Sequence[str] | None = None,
|
|
267
|
+
border_type: str = "SOLID",
|
|
267
268
|
) -> str:
|
|
268
|
-
"""Return a borderFill id matching the requested border/fill attributes.
|
|
269
|
+
"""Return a borderFill id matching the requested border/fill attributes.
|
|
270
|
+
|
|
271
|
+
``border_type`` selects the OWPML line style (``SOLID``, ``DASH``,
|
|
272
|
+
``DOT``, ``DOUBLE_SLIM``, ``WAVE``, …); values outside the OWPML
|
|
273
|
+
vocabulary are rejected.
|
|
274
|
+
"""
|
|
269
275
|
|
|
270
276
|
return self._root.ensure_border_fill(
|
|
271
277
|
border_color=border_color,
|
|
272
278
|
border_width=border_width,
|
|
273
279
|
fill_color=fill_color,
|
|
274
280
|
active_borders=active_borders,
|
|
281
|
+
border_type=border_type,
|
|
275
282
|
)
|
|
276
283
|
|
|
277
284
|
@property
|
|
@@ -594,9 +601,22 @@ class HwpxDocument:
|
|
|
594
601
|
size: int | float | None = None,
|
|
595
602
|
highlight: str | None = None,
|
|
596
603
|
strike: bool | None = None,
|
|
604
|
+
underline_shape: str | None = None,
|
|
605
|
+
underline_color: str | None = None,
|
|
606
|
+
strike_shape: str | None = None,
|
|
607
|
+
ratio: int | None = None,
|
|
608
|
+
letter_spacing: int | None = None,
|
|
609
|
+
shadow: str | None = None,
|
|
610
|
+
script: str | None = None,
|
|
597
611
|
base_char_pr_id: str | int | None = None,
|
|
598
612
|
) -> str:
|
|
599
|
-
"""Return a ``charPr`` identifier matching the requested flags.
|
|
613
|
+
"""Return a ``charPr`` identifier matching the requested flags.
|
|
614
|
+
|
|
615
|
+
5.4.0 additions (render-verified vocabulary; invalid values are
|
|
616
|
+
rejected): ``underline_shape``/``underline_color``, ``strike_shape``,
|
|
617
|
+
``ratio`` (장평 %), ``letter_spacing`` (자간 %), ``shadow`` (drop
|
|
618
|
+
shadow colour), ``script`` (``"sup"``/``"sub"``).
|
|
619
|
+
"""
|
|
600
620
|
|
|
601
621
|
return self._root.ensure_run_style(
|
|
602
622
|
bold=bold,
|
|
@@ -607,6 +627,13 @@ class HwpxDocument:
|
|
|
607
627
|
size=size,
|
|
608
628
|
highlight=highlight,
|
|
609
629
|
strike=strike,
|
|
630
|
+
underline_shape=underline_shape,
|
|
631
|
+
underline_color=underline_color,
|
|
632
|
+
strike_shape=strike_shape,
|
|
633
|
+
ratio=ratio,
|
|
634
|
+
letter_spacing=letter_spacing,
|
|
635
|
+
shadow=shadow,
|
|
636
|
+
script=script,
|
|
610
637
|
base_char_pr_id=base_char_pr_id,
|
|
611
638
|
)
|
|
612
639
|
|
|
@@ -1373,9 +1400,13 @@ class HwpxDocument:
|
|
|
1373
1400
|
paragraph: HwpxOxmlParagraph | None = None,
|
|
1374
1401
|
section: HwpxOxmlSection | None = None,
|
|
1375
1402
|
section_index: int | None = None,
|
|
1403
|
+
char_pr_id_ref: str | int | None = None,
|
|
1376
1404
|
) -> HwpxOxmlInlineObject:
|
|
1377
1405
|
"""Insert a hyperlink (fieldBegin + text + fieldEnd).
|
|
1378
1406
|
|
|
1407
|
+
The display text follows the Hancom convention (blue underlined)
|
|
1408
|
+
unless ``char_pr_id_ref`` overrides it.
|
|
1409
|
+
|
|
1379
1410
|
Returns the ``<hp:ctrl>`` wrapper containing the ``<hp:fieldBegin>``.
|
|
1380
1411
|
"""
|
|
1381
1412
|
|
|
@@ -1386,6 +1417,7 @@ class HwpxDocument:
|
|
|
1386
1417
|
paragraph=paragraph,
|
|
1387
1418
|
section=section,
|
|
1388
1419
|
section_index=section_index,
|
|
1420
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
1389
1421
|
)
|
|
1390
1422
|
|
|
1391
1423
|
def add_form_field(
|
|
@@ -1431,6 +1463,48 @@ class HwpxDocument:
|
|
|
1431
1463
|
)
|
|
1432
1464
|
|
|
1433
1465
|
|
|
1466
|
+
def add_chart(
|
|
1467
|
+
self,
|
|
1468
|
+
chart_xml: bytes | str,
|
|
1469
|
+
*,
|
|
1470
|
+
paragraph: HwpxOxmlParagraph | None = None,
|
|
1471
|
+
section: HwpxOxmlSection | None = None,
|
|
1472
|
+
section_index: int | None = None,
|
|
1473
|
+
size: tuple[int, int] | None = None,
|
|
1474
|
+
treat_as_char: bool = False,
|
|
1475
|
+
char_pr_id_ref: str | int | None = None,
|
|
1476
|
+
) -> HwpxOxmlInlineObject:
|
|
1477
|
+
"""Insert a native chart from ECMA-376 chartML. **Experimental contract.**
|
|
1478
|
+
|
|
1479
|
+
Stores *chart_xml* as a ``Chart/chartN.xml`` package part and emits the
|
|
1480
|
+
real-Hancom ``<hp:chart>`` anchor referencing it via ``chartIDRef``
|
|
1481
|
+
(contract: ``specs/055-chart-authoring/evidence/p0/chart-contract.md``).
|
|
1482
|
+
Hancom draws the chart from the chartML alone — no OLE fallback or
|
|
1483
|
+
pre-rendered image is written. The chartML must parse and carry the
|
|
1484
|
+
``c:chartSpace`` root (typed rejection otherwise), and the created
|
|
1485
|
+
anchor is re-read through the standard section scan — creation fails
|
|
1486
|
+
loudly if the standard consumer would not see it.
|
|
1487
|
+
|
|
1488
|
+
Args:
|
|
1489
|
+
chart_xml: ECMA-376 chartML document (``c:chartSpace``).
|
|
1490
|
+
paragraph: Target paragraph (e.g. inside a table cell). When
|
|
1491
|
+
omitted a new paragraph is appended to *section*.
|
|
1492
|
+
size: Optional ``(width, height)`` HWPUNIT pair for the anchor.
|
|
1493
|
+
treat_as_char: ``True`` places the chart inline in the text flow;
|
|
1494
|
+
default mirrors the render-verified gold float placement.
|
|
1495
|
+
"""
|
|
1496
|
+
|
|
1497
|
+
return _shapes.add_chart(
|
|
1498
|
+
self,
|
|
1499
|
+
chart_xml,
|
|
1500
|
+
paragraph=paragraph,
|
|
1501
|
+
section=section,
|
|
1502
|
+
section_index=section_index,
|
|
1503
|
+
size=size,
|
|
1504
|
+
treat_as_char=treat_as_char,
|
|
1505
|
+
char_pr_id_ref=char_pr_id_ref,
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1434
1508
|
def add_equation(
|
|
1435
1509
|
self,
|
|
1436
1510
|
script: str,
|
|
@@ -422,9 +422,10 @@ def _border_fill_child_attrs(
|
|
|
422
422
|
active: bool,
|
|
423
423
|
color: str,
|
|
424
424
|
width: str,
|
|
425
|
+
border_type: str = "SOLID",
|
|
425
426
|
) -> dict[str, str]:
|
|
426
427
|
return {
|
|
427
|
-
"type":
|
|
428
|
+
"type": border_type if active else "NONE",
|
|
428
429
|
"width": width,
|
|
429
430
|
"color": color,
|
|
430
431
|
}
|
|
@@ -465,6 +466,7 @@ def _border_fill_matches(
|
|
|
465
466
|
border_width: str,
|
|
466
467
|
fill_color: str | None,
|
|
467
468
|
active_borders: set[str],
|
|
469
|
+
border_type: str = "SOLID",
|
|
468
470
|
) -> bool:
|
|
469
471
|
if _element_local_name(element) != "borderFill":
|
|
470
472
|
return False
|
|
@@ -484,7 +486,7 @@ def _border_fill_matches(
|
|
|
484
486
|
child = element.find(f"{_HH}{child_name}")
|
|
485
487
|
if child is None:
|
|
486
488
|
return False
|
|
487
|
-
expected_type =
|
|
489
|
+
expected_type = border_type if side in active_borders else "NONE"
|
|
488
490
|
if (child.get("type") or "").upper() != expected_type:
|
|
489
491
|
return False
|
|
490
492
|
if _normalize_length(child.get("width")) != _normalize_length(border_width):
|
|
@@ -509,6 +511,7 @@ def _create_border_fill_element(
|
|
|
509
511
|
border_width: str,
|
|
510
512
|
fill_color: str | None,
|
|
511
513
|
active_borders: set[str],
|
|
514
|
+
border_type: str = "SOLID",
|
|
512
515
|
) -> ET.Element:
|
|
513
516
|
element = ET.Element(f"{_HH}borderFill", {"id": border_id, **_BASIC_BORDER_FILL_ATTRIBUTES})
|
|
514
517
|
ET.SubElement(element, f"{_HH}slash", {"type": "NONE", "Crooked": "0", "isCounter": "0"})
|
|
@@ -521,6 +524,7 @@ def _create_border_fill_element(
|
|
|
521
524
|
active=side in active_borders,
|
|
522
525
|
color=border_color,
|
|
523
526
|
width=border_width,
|
|
527
|
+
border_type=border_type,
|
|
524
528
|
),
|
|
525
529
|
)
|
|
526
530
|
ET.SubElement(
|
|
@@ -579,6 +583,17 @@ def _default_cell_margin_attributes() -> dict[str, str]:
|
|
|
579
583
|
return {"left": "0", "right": "0", "top": "0", "bottom": "0"}
|
|
580
584
|
|
|
581
585
|
|
|
586
|
+
def _default_cell_inner_margin_attributes() -> dict[str, str]:
|
|
587
|
+
"""Hancom's new-table cell padding: 1.8 mm left/right, 0.5 mm top/bottom.
|
|
588
|
+
|
|
589
|
+
Measured from real Hancom-authored gold documents
|
|
590
|
+
(specs/056-authoring-fidelity-audit — the audit found our former 0
|
|
591
|
+
padding makes text hug the borders, visibly unlike a Hancom table).
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
return {"left": "510", "right": "510", "top": "141", "bottom": "141"}
|
|
595
|
+
|
|
596
|
+
|
|
582
597
|
def _get_int_attr(element: ET.Element, name: str, default: int = 0) -> int:
|
|
583
598
|
"""Return *name* attribute of *element* as an integer."""
|
|
584
599
|
|
|
@@ -59,6 +59,13 @@ class _RunStyleSpec:
|
|
|
59
59
|
height: str | None
|
|
60
60
|
strike: bool | None
|
|
61
61
|
font_ref: dict[str, str] | None
|
|
62
|
+
underline_shape: str | None = None
|
|
63
|
+
underline_color: str | None = None
|
|
64
|
+
strike_shape: str | None = None
|
|
65
|
+
ratio: int | None = None
|
|
66
|
+
letter_spacing: int | None = None
|
|
67
|
+
shadow_color: str | None = None
|
|
68
|
+
script: str | None = None
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
def _run_style_element_flags(element: ET.Element) -> tuple[bool, bool, bool]:
|
|
@@ -78,6 +85,81 @@ def _run_style_element_strike(element: ET.Element) -> bool:
|
|
|
78
85
|
return strike_element.get("shape", "").upper() != "NONE"
|
|
79
86
|
|
|
80
87
|
|
|
88
|
+
def _validated_line_shape(
|
|
89
|
+
value: str | None, vocabulary: frozenset[str], label: str
|
|
90
|
+
) -> str | None:
|
|
91
|
+
if value is None:
|
|
92
|
+
return None
|
|
93
|
+
normalized = str(value).upper()
|
|
94
|
+
if normalized not in vocabulary:
|
|
95
|
+
raise ValueError(f"unsupported {label} {value!r}")
|
|
96
|
+
return normalized
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_style_underline_matches(element: ET.Element, spec: _RunStyleSpec) -> bool:
|
|
100
|
+
if spec.underline_shape is None and spec.underline_color is None:
|
|
101
|
+
return True
|
|
102
|
+
underline = element.find(f"{_HH}underline")
|
|
103
|
+
if underline is None:
|
|
104
|
+
return False
|
|
105
|
+
if spec.underline_shape is not None and (
|
|
106
|
+
underline.get("shape", "").upper() != spec.underline_shape
|
|
107
|
+
):
|
|
108
|
+
return False
|
|
109
|
+
if spec.underline_color is not None and (
|
|
110
|
+
(underline.get("color") or "").upper() != spec.underline_color.upper()
|
|
111
|
+
):
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _run_style_lang_value_matches(
|
|
117
|
+
element: ET.Element, tag: str, value: int | None
|
|
118
|
+
) -> bool:
|
|
119
|
+
if value is None:
|
|
120
|
+
return True
|
|
121
|
+
node = element.find(f"{_HH}{tag}")
|
|
122
|
+
return node is not None and node.get("hangul") == str(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_style_extensions_match(element: ET.Element, spec: _RunStyleSpec) -> bool:
|
|
126
|
+
if not _run_style_underline_matches(element, spec):
|
|
127
|
+
return False
|
|
128
|
+
if spec.strike_shape is not None:
|
|
129
|
+
strike_el = element.find(f"{_HH}strikeout")
|
|
130
|
+
if strike_el is None or strike_el.get("shape", "").upper() != spec.strike_shape:
|
|
131
|
+
return False
|
|
132
|
+
if not _run_style_lang_value_matches(element, "ratio", spec.ratio):
|
|
133
|
+
return False
|
|
134
|
+
if not _run_style_lang_value_matches(element, "spacing", spec.letter_spacing):
|
|
135
|
+
return False
|
|
136
|
+
if not _run_style_shadow_matches(element, spec.shadow_color):
|
|
137
|
+
return False
|
|
138
|
+
if not _run_style_script_matches(element, spec.script):
|
|
139
|
+
return False
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _run_style_shadow_matches(element: ET.Element, shadow_color: str | None) -> bool:
|
|
144
|
+
if shadow_color is None:
|
|
145
|
+
return True
|
|
146
|
+
shadow_el = element.find(f"{_HH}shadow")
|
|
147
|
+
if shadow_el is None or shadow_el.get("type", "").upper() == "NONE":
|
|
148
|
+
return False
|
|
149
|
+
return (shadow_el.get("color") or "").upper() == shadow_color.upper()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _run_style_script_matches(element: ET.Element, script: str | None) -> bool:
|
|
153
|
+
if script is None:
|
|
154
|
+
return True
|
|
155
|
+
if not _run_style_lang_value_matches(element, "relSz", 67):
|
|
156
|
+
return False
|
|
157
|
+
off_el = element.find(f"{_HH}offset")
|
|
158
|
+
# 실한컴 렌더 실측: offset 음수=위로(위첨자), 양수=아래로(아래첨자).
|
|
159
|
+
wanted_offset = "-30" if script == "sup" else "30"
|
|
160
|
+
return off_el is not None and off_el.get("hangul") == wanted_offset
|
|
161
|
+
|
|
162
|
+
|
|
81
163
|
def _run_style_predicate(element: ET.Element, spec: _RunStyleSpec) -> bool:
|
|
82
164
|
if _run_style_element_flags(element) != spec.flags:
|
|
83
165
|
return False
|
|
@@ -92,6 +174,8 @@ def _run_style_predicate(element: ET.Element, spec: _RunStyleSpec) -> bool:
|
|
|
92
174
|
return False
|
|
93
175
|
if spec.strike is not None and _run_style_element_strike(element) != bool(spec.strike):
|
|
94
176
|
return False
|
|
177
|
+
if not _run_style_extensions_match(element, spec):
|
|
178
|
+
return False
|
|
95
179
|
if spec.font_ref is not None:
|
|
96
180
|
font_ref = element.find(f"{_HH}fontRef")
|
|
97
181
|
if font_ref is None:
|
|
@@ -127,9 +211,10 @@ def _run_style_apply_underline(
|
|
|
127
211
|
) -> None:
|
|
128
212
|
underline_attrs = dict(base_underline_attrs)
|
|
129
213
|
if want_underline:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
214
|
+
# 실한컴 gold 관례: hh:underline type은 위치 어휘(BOTTOM/CENTER/TOP).
|
|
215
|
+
underline_attrs.setdefault("type", "BOTTOM")
|
|
216
|
+
if underline_attrs.get("type", "").upper() in ("NONE", "SOLID"):
|
|
217
|
+
underline_attrs["type"] = "BOTTOM"
|
|
133
218
|
underline_attrs.setdefault(
|
|
134
219
|
"shape", base_underline_attrs.get("shape", "SOLID")
|
|
135
220
|
)
|
|
@@ -159,6 +244,56 @@ def _run_style_apply_strikeout(
|
|
|
159
244
|
_append_child(element, f"{_HH}strikeout", strike_attrs)
|
|
160
245
|
|
|
161
246
|
|
|
247
|
+
def _run_style_set_lang_values(element: ET.Element, tag: str, value: int) -> None:
|
|
248
|
+
node = element.find(f"{_HH}{tag}")
|
|
249
|
+
if node is None:
|
|
250
|
+
node = _append_child(element, f"{_HH}{tag}")
|
|
251
|
+
for lang in ("hangul", "latin", "hanja", "japanese", "other", "symbol", "user"):
|
|
252
|
+
node.set(lang, str(value))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _run_style_apply_underline_extension(
|
|
256
|
+
element: ET.Element, spec: _RunStyleSpec
|
|
257
|
+
) -> None:
|
|
258
|
+
if spec.underline_shape is None and spec.underline_color is None:
|
|
259
|
+
return
|
|
260
|
+
underline = element.find(f"{_HH}underline")
|
|
261
|
+
if underline is None:
|
|
262
|
+
return
|
|
263
|
+
if underline.get("type", "").upper() == "NONE":
|
|
264
|
+
underline.set("type", "BOTTOM")
|
|
265
|
+
if spec.underline_shape is not None:
|
|
266
|
+
underline.set("shape", spec.underline_shape)
|
|
267
|
+
if spec.underline_color is not None:
|
|
268
|
+
underline.set("color", spec.underline_color)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _run_style_apply_extensions(element: ET.Element, spec: _RunStyleSpec) -> None:
|
|
272
|
+
_run_style_apply_underline_extension(element, spec)
|
|
273
|
+
if spec.strike_shape is not None:
|
|
274
|
+
strike_el = element.find(f"{_HH}strikeout")
|
|
275
|
+
if strike_el is None:
|
|
276
|
+
strike_el = _append_child(element, f"{_HH}strikeout")
|
|
277
|
+
strike_el.set("shape", spec.strike_shape)
|
|
278
|
+
if not strike_el.get("color"):
|
|
279
|
+
strike_el.set("color", "#000000")
|
|
280
|
+
if spec.ratio is not None:
|
|
281
|
+
_run_style_set_lang_values(element, "ratio", spec.ratio)
|
|
282
|
+
if spec.letter_spacing is not None:
|
|
283
|
+
_run_style_set_lang_values(element, "spacing", spec.letter_spacing)
|
|
284
|
+
if spec.shadow_color is not None:
|
|
285
|
+
shadow_el = element.find(f"{_HH}shadow")
|
|
286
|
+
if shadow_el is None:
|
|
287
|
+
shadow_el = _append_child(element, f"{_HH}shadow")
|
|
288
|
+
shadow_el.set("type", "DROP")
|
|
289
|
+
shadow_el.set("color", spec.shadow_color)
|
|
290
|
+
shadow_el.set("offsetX", "12")
|
|
291
|
+
shadow_el.set("offsetY", "12")
|
|
292
|
+
if spec.script is not None:
|
|
293
|
+
_run_style_set_lang_values(element, "relSz", 67)
|
|
294
|
+
_run_style_set_lang_values(element, "offset", -30 if spec.script == "sup" else 30)
|
|
295
|
+
|
|
296
|
+
|
|
162
297
|
def _run_style_modifier(element: ET.Element, spec: _RunStyleSpec) -> None:
|
|
163
298
|
underline_nodes = list(element.findall(f"{_HH}underline"))
|
|
164
299
|
base_underline_attrs = (
|
|
@@ -186,6 +321,8 @@ def _run_style_modifier(element: ET.Element, spec: _RunStyleSpec) -> None:
|
|
|
186
321
|
_run_style_apply_underline(element, base_underline_attrs, spec.flags[2])
|
|
187
322
|
_run_style_apply_strikeout(element, base_strike_attrs, spec.strike)
|
|
188
323
|
|
|
324
|
+
_run_style_apply_extensions(element, spec)
|
|
325
|
+
|
|
189
326
|
|
|
190
327
|
class HwpxOxmlDocument:
|
|
191
328
|
"""Aggregates the XML parts that make up an HWPX document."""
|
|
@@ -357,6 +494,12 @@ class HwpxOxmlDocument:
|
|
|
357
494
|
return None
|
|
358
495
|
return cache.get(normalized)
|
|
359
496
|
|
|
497
|
+
_UNDERLINE_SHAPES = frozenset({
|
|
498
|
+
"SOLID", "DASH", "DOT", "DASH_DOT", "DASH_DOT_DOT", "LONG_DASH",
|
|
499
|
+
"CIRCLE", "DOUBLE_SLIM", "SLIM_THICK", "THICK_SLIM",
|
|
500
|
+
"SLIM_THICK_SLIM", "WAVE", "DOUBLEWAVE",
|
|
501
|
+
})
|
|
502
|
+
|
|
360
503
|
def ensure_run_style(
|
|
361
504
|
self,
|
|
362
505
|
*,
|
|
@@ -368,13 +511,45 @@ class HwpxOxmlDocument:
|
|
|
368
511
|
size: int | float | None = None,
|
|
369
512
|
highlight: str | None = None,
|
|
370
513
|
strike: bool | None = None,
|
|
514
|
+
underline_shape: str | None = None,
|
|
515
|
+
underline_color: str | None = None,
|
|
516
|
+
strike_shape: str | None = None,
|
|
517
|
+
ratio: int | None = None,
|
|
518
|
+
letter_spacing: int | None = None,
|
|
519
|
+
shadow: str | None = None,
|
|
520
|
+
script: str | None = None,
|
|
371
521
|
base_char_pr_id: str | int | None = None,
|
|
372
522
|
) -> str:
|
|
373
|
-
"""Return a char property identifier matching the requested flags.
|
|
523
|
+
"""Return a char property identifier matching the requested flags.
|
|
524
|
+
|
|
525
|
+
The 5.4.0 additions mirror what the fidelity audit render-verified on
|
|
526
|
+
real Hancom: ``underline_shape``/``underline_color`` (implies an
|
|
527
|
+
underline), ``strike_shape`` (implies a strikeout), ``ratio`` (장평 %),
|
|
528
|
+
``letter_spacing`` (자간 %), ``shadow`` (drop-shadow colour), and
|
|
529
|
+
``script`` (``"sup"``/``"sub"``). Values outside the OWPML vocabulary
|
|
530
|
+
are rejected — no silent approximation.
|
|
531
|
+
"""
|
|
374
532
|
|
|
375
533
|
if not self._headers:
|
|
376
534
|
raise ValueError("document does not contain any headers")
|
|
377
535
|
|
|
536
|
+
normalized_underline_shape = _validated_line_shape(
|
|
537
|
+
underline_shape, self._UNDERLINE_SHAPES, "underline_shape"
|
|
538
|
+
)
|
|
539
|
+
if normalized_underline_shape is not None or underline_color is not None:
|
|
540
|
+
underline = True
|
|
541
|
+
normalized_strike_shape = _validated_line_shape(
|
|
542
|
+
strike_shape, self._UNDERLINE_SHAPES, "strike_shape"
|
|
543
|
+
)
|
|
544
|
+
if normalized_strike_shape is not None:
|
|
545
|
+
strike = True
|
|
546
|
+
if ratio is not None and not 10 <= int(ratio) <= 400:
|
|
547
|
+
raise ValueError("ratio must be a percentage between 10 and 400")
|
|
548
|
+
if letter_spacing is not None and not -50 <= int(letter_spacing) <= 100:
|
|
549
|
+
raise ValueError("letter_spacing must be between -50 and 100")
|
|
550
|
+
if script is not None and script not in ("sup", "sub"):
|
|
551
|
+
raise ValueError('script must be "sup" or "sub"')
|
|
552
|
+
|
|
378
553
|
header = self._headers[0]
|
|
379
554
|
spec = _RunStyleSpec(
|
|
380
555
|
flags=(bool(bold), bool(italic), bool(underline)),
|
|
@@ -383,6 +558,13 @@ class HwpxOxmlDocument:
|
|
|
383
558
|
height=_char_height_from_points(size),
|
|
384
559
|
strike=strike,
|
|
385
560
|
font_ref=header.font_ref_for_face(font) if font is not None else None,
|
|
561
|
+
underline_shape=normalized_underline_shape,
|
|
562
|
+
underline_color=_normalize_color(underline_color),
|
|
563
|
+
strike_shape=normalized_strike_shape,
|
|
564
|
+
ratio=int(ratio) if ratio is not None else None,
|
|
565
|
+
letter_spacing=int(letter_spacing) if letter_spacing is not None else None,
|
|
566
|
+
shadow_color=_normalize_color(shadow),
|
|
567
|
+
script=script,
|
|
386
568
|
)
|
|
387
569
|
element = header.ensure_char_property(
|
|
388
570
|
predicate=lambda el: _run_style_predicate(el, spec),
|
|
@@ -425,6 +607,7 @@ class HwpxOxmlDocument:
|
|
|
425
607
|
border_width: str = "0.12 mm",
|
|
426
608
|
fill_color: str | None = None,
|
|
427
609
|
active_borders: Iterable[str] | None = None,
|
|
610
|
+
border_type: str = "SOLID",
|
|
428
611
|
) -> str:
|
|
429
612
|
if not self._headers:
|
|
430
613
|
return "0"
|
|
@@ -433,6 +616,7 @@ class HwpxOxmlDocument:
|
|
|
433
616
|
border_width=border_width,
|
|
434
617
|
fill_color=fill_color,
|
|
435
618
|
active_borders=active_borders,
|
|
619
|
+
border_type=border_type,
|
|
436
620
|
)
|
|
437
621
|
|
|
438
622
|
def ensure_shading_border_fill(
|
|
@@ -898,6 +898,13 @@ class HwpxOxmlHeader:
|
|
|
898
898
|
self.mark_dirty()
|
|
899
899
|
return new_id
|
|
900
900
|
|
|
901
|
+
_BORDER_LINE_TYPES = frozenset({
|
|
902
|
+
"SOLID", "DASH", "DOT", "DASH_DOT", "DASH_DOT_DOT", "LONG_DASH",
|
|
903
|
+
"CIRCLE", "DOUBLE_SLIM", "SLIM_THICK", "THICK_SLIM", "SLIM_THICK_SLIM",
|
|
904
|
+
"WAVE", "DOUBLEWAVE", "THICK_3D", "THICK_3D_REVERSE_LIGHTING",
|
|
905
|
+
"SLIM_3D", "SLIM_3D_REVERSE_LIGHTING",
|
|
906
|
+
})
|
|
907
|
+
|
|
901
908
|
def ensure_border_fill(
|
|
902
909
|
self,
|
|
903
910
|
*,
|
|
@@ -905,11 +912,18 @@ class HwpxOxmlHeader:
|
|
|
905
912
|
border_width: str = "0.12 mm",
|
|
906
913
|
fill_color: str | None = None,
|
|
907
914
|
active_borders: Iterable[str] | None = None,
|
|
915
|
+
border_type: str = "SOLID",
|
|
908
916
|
) -> str:
|
|
909
917
|
element = self._border_fills_element(create=True)
|
|
910
918
|
if element is None: # pragma: no cover - defensive branch
|
|
911
919
|
raise RuntimeError("failed to create <borderFills> element")
|
|
912
920
|
|
|
921
|
+
normalized_border_type = str(border_type or "SOLID").upper()
|
|
922
|
+
if normalized_border_type not in self._BORDER_LINE_TYPES:
|
|
923
|
+
raise ValueError(
|
|
924
|
+
f"unsupported border_type {border_type!r}; expected one of "
|
|
925
|
+
+ ", ".join(sorted(self._BORDER_LINE_TYPES))
|
|
926
|
+
)
|
|
913
927
|
normalized_border_color = _normalize_color(border_color) or "#BFBFBF"
|
|
914
928
|
normalized_border_width = str(border_width or "0.12 mm")
|
|
915
929
|
normalized_active_borders = _normalize_border_side_names(active_borders)
|
|
@@ -922,6 +936,7 @@ class HwpxOxmlHeader:
|
|
|
922
936
|
border_width=normalized_border_width,
|
|
923
937
|
fill_color=normalized_fill_color,
|
|
924
938
|
active_borders=normalized_active_borders,
|
|
939
|
+
border_type=normalized_border_type,
|
|
925
940
|
):
|
|
926
941
|
border_id = border_fill.get("id")
|
|
927
942
|
if border_id:
|
|
@@ -934,6 +949,7 @@ class HwpxOxmlHeader:
|
|
|
934
949
|
border_width=normalized_border_width,
|
|
935
950
|
fill_color=normalized_fill_color,
|
|
936
951
|
active_borders=normalized_active_borders,
|
|
952
|
+
border_type=normalized_border_type,
|
|
937
953
|
)
|
|
938
954
|
if isinstance(element, LET._Element):
|
|
939
955
|
new_border_fill = LET.fromstring(ET.tostring(new_border_fill, encoding="utf-8"))
|