python-hwpx 2.11.0__tar.gz → 2.13.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-2.11.0/src/python_hwpx.egg-info → python_hwpx-2.13.0}/PKG-INFO +18 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/README.md +13 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/pyproject.toml +17 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/__init__.py +8 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/authoring.py +355 -24
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/builder/core.py +25 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/builder/report.py +8 -0
- python_hwpx-2.13.0/src/hwpx/conformance/__init__.py +54 -0
- python_hwpx-2.13.0/src/hwpx/conformance/badges.py +198 -0
- python_hwpx-2.13.0/src/hwpx/conformance/corpus/corpus.json +53 -0
- python_hwpx-2.13.0/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/conformance/corpus.py +260 -0
- python_hwpx-2.13.0/src/hwpx/conformance/report.py +223 -0
- python_hwpx-2.13.0/src/hwpx/conformance/runner.py +395 -0
- python_hwpx-2.13.0/src/hwpx/design/__init__.py +30 -0
- python_hwpx-2.13.0/src/hwpx/design/_support.py +144 -0
- python_hwpx-2.13.0/src/hwpx/design/composer.py +282 -0
- python_hwpx-2.13.0/src/hwpx/design/harvest.py +305 -0
- python_hwpx-2.13.0/src/hwpx/design/plan.py +69 -0
- python_hwpx-2.13.0/src/hwpx/design/profile.py +88 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/fragments/body.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/fragments/heading.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/fragments/info_table.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/fragments/title.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/profile.json +25 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/fragments/body.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/fragments/heading.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/fragments/title.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/profile.json +25 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/fragments/body.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/fragments/heading.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/fragments/info_table.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/fragments/title.xml +1 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/profile.json +25 -0
- python_hwpx-2.13.0/src/hwpx/design/profiles/report/template.hwpx +0 -0
- python_hwpx-2.13.0/src/hwpx/design/validator.py +107 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/document.py +244 -81
- python_hwpx-2.13.0/src/hwpx/form_fit/__init__.py +51 -0
- python_hwpx-2.13.0/src/hwpx/form_fit/apply.py +96 -0
- python_hwpx-2.13.0/src/hwpx/form_fit/engine.py +294 -0
- python_hwpx-2.13.0/src/hwpx/form_fit/measure.py +369 -0
- python_hwpx-2.13.0/src/hwpx/form_fit/policy.py +84 -0
- python_hwpx-2.13.0/src/hwpx/form_fit/report.py +93 -0
- python_hwpx-2.13.0/src/hwpx/layout/__init__.py +36 -0
- python_hwpx-2.13.0/src/hwpx/layout/lint.py +384 -0
- python_hwpx-2.13.0/src/hwpx/layout/report.py +121 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/_document_impl.py +242 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/patch.py +76 -13
- python_hwpx-2.13.0/src/hwpx/quality/__init__.py +45 -0
- python_hwpx-2.13.0/src/hwpx/quality/ledger.py +111 -0
- python_hwpx-2.13.0/src/hwpx/quality/policy.py +95 -0
- python_hwpx-2.13.0/src/hwpx/quality/report.py +228 -0
- python_hwpx-2.13.0/src/hwpx/quality/save_pipeline.py +556 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/template_formfit.py +5 -1
- python_hwpx-2.13.0/src/hwpx/visual/__init__.py +78 -0
- python_hwpx-2.13.0/src/hwpx/visual/_render_hwpx.ps1 +72 -0
- python_hwpx-2.13.0/src/hwpx/visual/_render_hwpx_mac.applescript +222 -0
- python_hwpx-2.13.0/src/hwpx/visual/detectors.py +152 -0
- python_hwpx-2.13.0/src/hwpx/visual/diff.py +153 -0
- python_hwpx-2.13.0/src/hwpx/visual/masks.py +51 -0
- python_hwpx-2.13.0/src/hwpx/visual/oracle.py +505 -0
- python_hwpx-2.13.0/src/hwpx/visual/report.py +47 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0/src/python_hwpx.egg-info}/PKG-INFO +18 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/SOURCES.txt +65 -1
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/entry_points.txt +1 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/requires.txt +5 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_builder_core.py +9 -0
- python_hwpx-2.13.0/tests/test_conformance.py +426 -0
- python_hwpx-2.13.0/tests/test_design_builder.py +297 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_document_plan.py +157 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_document_save_api.py +3 -1
- python_hwpx-2.13.0/tests/test_form_fit.py +218 -0
- python_hwpx-2.13.0/tests/test_form_fit_integration.py +157 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_kordoc_absorption.py +61 -0
- python_hwpx-2.13.0/tests/test_layout_lint.py +455 -0
- python_hwpx-2.13.0/tests/test_save_pipeline.py +284 -0
- python_hwpx-2.13.0/tests/test_save_pipeline_no_bypass.py +134 -0
- python_hwpx-2.13.0/tests/test_visual_oracle.py +300 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/LICENSE +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/NOTICE +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/setup.cfg +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/package.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_new_features.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-2.11.0 → python_hwpx-2.13.0}/tests/test_version_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,10 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
License-File: NOTICE
|
|
23
23
|
Requires-Dist: lxml<6,>=4.9
|
|
24
|
+
Provides-Extra: visual
|
|
25
|
+
Requires-Dist: pymupdf>=1.24; extra == "visual"
|
|
26
|
+
Requires-Dist: pillow>=10.0; extra == "visual"
|
|
27
|
+
Requires-Dist: numpy>=1.26; extra == "visual"
|
|
24
28
|
Provides-Extra: dev
|
|
25
29
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
26
30
|
Requires-Dist: twine>=4.0; extra == "dev"
|
|
@@ -261,6 +265,19 @@ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python
|
|
|
261
265
|
| 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
|
|
262
266
|
| 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
|
|
263
267
|
| 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
|
|
268
|
+
| 🏗️ **빌더** | 조립형 생성 | `hwpx.builder` — Section/Heading/Table/Image/Header 조립, 하드게이트 저장 리포트 |
|
|
269
|
+
| ✅ **편집기 오픈 안전** | `validate_editor_open_safety` | 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환 |
|
|
270
|
+
| 🧪 **퍼징 수렴 루프** | `hwpx.tools.fuzz` | 시드 결정적 시나리오 생성 · 3중 오라클 러너 · 회귀 fixture 박제 |
|
|
271
|
+
| 🖥️ **레이아웃 프리뷰** | `hwpx.tools.layout_preview` | 페이지 박스·표·여백 근사 HTML/PNG (에이전트 자기검증용) |
|
|
272
|
+
| 🧷 **바이트 보존 패치** | `hwpx.patch` | section XML 바이트 splice — 미수정 영역 바이트 보존 |
|
|
273
|
+
| 📐 **기존 문서 서식 편집** | 문단·페이지 | 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 머리말/쪽번호, 불릿/번호 |
|
|
274
|
+
| 🖊️ **누름틀** | 양식 필드 | 클릭히어 필드 조회·서식 보존 채움 |
|
|
275
|
+
| 🏛️ **공문서 도구** | `official_lint` · 결재란 | 항목기호 위계·"끝." 표시·붙임·날짜 표기 lint, 결재란 프리셋 |
|
|
276
|
+
| 📷 **고급 생성기** | `advanced_generators` | 사진대지(image_grid)·회의 명패·표 기반 조직도 |
|
|
277
|
+
| 🆚 **신구대조** | `doc_diff` | 문단 LCS diff·신구대조표 생성·참조 정합 lint |
|
|
278
|
+
| 📨 **메일머지·표 계산** | `mail_merge` | 템플릿+데이터 N부 대량 생성, 표 합계·평균 |
|
|
279
|
+
| 🪄 **서식 이식** | `style_profile` | 참조 문서 프로파일 추출·적용, 템플릿 레지스트리 |
|
|
280
|
+
| 🛡️ **입력 강건화** | `opc.security` | XML entity 폭탄·ZIP 압축 폭탄 가드 |
|
|
264
281
|
|
|
265
282
|
## 기능 상세
|
|
266
283
|
|
|
@@ -225,6 +225,19 @@ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python
|
|
|
225
225
|
| 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
|
|
226
226
|
| 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
|
|
227
227
|
| 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
|
|
228
|
+
| 🏗️ **빌더** | 조립형 생성 | `hwpx.builder` — Section/Heading/Table/Image/Header 조립, 하드게이트 저장 리포트 |
|
|
229
|
+
| ✅ **편집기 오픈 안전** | `validate_editor_open_safety` | 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환 |
|
|
230
|
+
| 🧪 **퍼징 수렴 루프** | `hwpx.tools.fuzz` | 시드 결정적 시나리오 생성 · 3중 오라클 러너 · 회귀 fixture 박제 |
|
|
231
|
+
| 🖥️ **레이아웃 프리뷰** | `hwpx.tools.layout_preview` | 페이지 박스·표·여백 근사 HTML/PNG (에이전트 자기검증용) |
|
|
232
|
+
| 🧷 **바이트 보존 패치** | `hwpx.patch` | section XML 바이트 splice — 미수정 영역 바이트 보존 |
|
|
233
|
+
| 📐 **기존 문서 서식 편집** | 문단·페이지 | 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 머리말/쪽번호, 불릿/번호 |
|
|
234
|
+
| 🖊️ **누름틀** | 양식 필드 | 클릭히어 필드 조회·서식 보존 채움 |
|
|
235
|
+
| 🏛️ **공문서 도구** | `official_lint` · 결재란 | 항목기호 위계·"끝." 표시·붙임·날짜 표기 lint, 결재란 프리셋 |
|
|
236
|
+
| 📷 **고급 생성기** | `advanced_generators` | 사진대지(image_grid)·회의 명패·표 기반 조직도 |
|
|
237
|
+
| 🆚 **신구대조** | `doc_diff` | 문단 LCS diff·신구대조표 생성·참조 정합 lint |
|
|
238
|
+
| 📨 **메일머지·표 계산** | `mail_merge` | 템플릿+데이터 N부 대량 생성, 표 합계·평균 |
|
|
239
|
+
| 🪄 **서식 이식** | `style_profile` | 참조 문서 프로파일 추출·적용, 템플릿 레지스트리 |
|
|
240
|
+
| 🛡️ **입력 강건화** | `opc.security` | XML entity 폭탄·ZIP 압축 폭탄 가드 |
|
|
228
241
|
|
|
229
242
|
## 기능 상세
|
|
230
243
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.13.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -29,6 +29,11 @@ dependencies = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
|
+
visual = [
|
|
33
|
+
"pymupdf>=1.24",
|
|
34
|
+
"pillow>=10.0",
|
|
35
|
+
"numpy>=1.26",
|
|
36
|
+
]
|
|
32
37
|
dev = [
|
|
33
38
|
"build>=1.0",
|
|
34
39
|
"twine>=4.0",
|
|
@@ -58,6 +63,7 @@ hwpx-page-guard = "hwpx.tools.page_guard:main"
|
|
|
58
63
|
hwpx-analyze-template = "hwpx.tools.template_analyzer:main"
|
|
59
64
|
hwpx-text-extract = "hwpx.tools.text_extract_cli:main"
|
|
60
65
|
hwpx-repair = "hwpx.tools.repair:main"
|
|
66
|
+
hwpx-conformance = "hwpx.conformance.runner:main"
|
|
61
67
|
|
|
62
68
|
[tool.setuptools]
|
|
63
69
|
package-dir = { "" = "src" }
|
|
@@ -71,6 +77,16 @@ include = ["hwpx*"]
|
|
|
71
77
|
"hwpx" = ["py.typed"]
|
|
72
78
|
"hwpx.tools" = ["_schemas/*.xsd"]
|
|
73
79
|
"hwpx.data" = ["Skeleton.hwpx"]
|
|
80
|
+
"hwpx.visual" = ["_render_hwpx.ps1", "_render_hwpx_mac.applescript"]
|
|
81
|
+
"hwpx.design" = [
|
|
82
|
+
"profiles/*/profile.json",
|
|
83
|
+
"profiles/*/template.hwpx",
|
|
84
|
+
"profiles/*/fragments/*.xml",
|
|
85
|
+
]
|
|
86
|
+
"hwpx.conformance" = [
|
|
87
|
+
"corpus/corpus.json",
|
|
88
|
+
"corpus/*.hwpx",
|
|
89
|
+
]
|
|
74
90
|
|
|
75
91
|
[tool.pytest.ini_options]
|
|
76
92
|
pythonpath = ["src"]
|
|
@@ -100,6 +100,11 @@ from .authoring import (
|
|
|
100
100
|
validate_document_plan,
|
|
101
101
|
)
|
|
102
102
|
from .builder import approval_box
|
|
103
|
+
from .quality import (
|
|
104
|
+
QualityPolicy,
|
|
105
|
+
SavePipeline,
|
|
106
|
+
VisualCompleteReport,
|
|
107
|
+
)
|
|
103
108
|
from .template_formfit import (
|
|
104
109
|
TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
|
|
105
110
|
TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
|
|
@@ -108,6 +113,9 @@ from .template_formfit import (
|
|
|
108
113
|
)
|
|
109
114
|
|
|
110
115
|
__all__ = [
|
|
116
|
+
"QualityPolicy",
|
|
117
|
+
"SavePipeline",
|
|
118
|
+
"VisualCompleteReport",
|
|
111
119
|
"__version__",
|
|
112
120
|
"AUTHORING_REPORT_VERSION",
|
|
113
121
|
"DEFAULT_NAMESPACES",
|
|
@@ -30,6 +30,7 @@ from .builder import (
|
|
|
30
30
|
)
|
|
31
31
|
from .builder.core import Toc as BuilderToc
|
|
32
32
|
from .document import HwpxDocument
|
|
33
|
+
from .oxml.namespaces import HP as _HP
|
|
33
34
|
from .tools.package_validator import validate_package
|
|
34
35
|
from .tools.table_cleanup import normalize_cell_text
|
|
35
36
|
from .tools.advanced_generators import build_image_grid
|
|
@@ -48,7 +49,7 @@ DOCUMENT_PLAN_V2_SCHEMA_VERSION = "hwpx.document_plan.v2"
|
|
|
48
49
|
AUTHORING_REPORT_VERSION = "hwpx-authoring-quality-v1"
|
|
49
50
|
OPERATING_PLAN_QUALITY_VERSION = "operating-plan-quality-v1"
|
|
50
51
|
DEFAULT_STYLE_PRESET = "standard_korean_business"
|
|
51
|
-
_DEFAULT_TABLE_WIDTH =
|
|
52
|
+
_DEFAULT_TABLE_WIDTH = 45_000 # ~158.7mm (HWPUNIT): fits A4(210mm) content at 25mm margins (~160mm). 48000(~169mm) overflowed the right margin in Hancom.
|
|
52
53
|
_METADATA_LABELS = {
|
|
53
54
|
"organization": "기관",
|
|
54
55
|
"author": "작성자",
|
|
@@ -62,6 +63,10 @@ _SUPPORTED_STYLE_TOKENS = frozenset(
|
|
|
62
63
|
{"body", "title", "subtitle", "heading", "bullet", "table_header", "table_cell"}
|
|
63
64
|
)
|
|
64
65
|
_SUPPORTED_TABLE_PROFILES = frozenset({"government"})
|
|
66
|
+
_DEFAULT_PAGE_MARGIN_MM = 25
|
|
67
|
+
_TABLE_BORDER_COLOR = "#BFBFBF"
|
|
68
|
+
_TABLE_HEADER_FILL = "#F2F2F2"
|
|
69
|
+
_TABLE_CELL_MARGIN = "425"
|
|
65
70
|
_BOOLEAN_QUALITY_GATES = frozenset(
|
|
66
71
|
{"validatePackage", "validateDocument", "reopen", "visualReviewRequired"}
|
|
67
72
|
)
|
|
@@ -164,28 +169,85 @@ class DocumentStylePreset:
|
|
|
164
169
|
|
|
165
170
|
name: str = DEFAULT_STYLE_PRESET
|
|
166
171
|
title_bold: bool = True
|
|
167
|
-
subtitle_italic: bool =
|
|
172
|
+
subtitle_italic: bool = False
|
|
168
173
|
heading_bold: bool = True
|
|
169
|
-
heading_underline: bool =
|
|
174
|
+
heading_underline: bool = False
|
|
170
175
|
table_header_bold: bool = True
|
|
176
|
+
title_size: int = 20
|
|
177
|
+
subtitle_size: int = 12
|
|
178
|
+
heading_size: int = 14
|
|
179
|
+
body_size: int = 11
|
|
180
|
+
meta_size: int = 10
|
|
181
|
+
font: str = "함초롬돋움"
|
|
182
|
+
title_color: str | None = "#1F3864"
|
|
183
|
+
heading_color: str | None = "#1F3864"
|
|
184
|
+
subtitle_color: str | None = "#595959"
|
|
185
|
+
meta_color: str | None = "#595959"
|
|
186
|
+
title_rule: bool = True
|
|
187
|
+
heading_rule: bool = True
|
|
188
|
+
rule_color: str = "#BFBFBF"
|
|
171
189
|
|
|
172
190
|
def ensure_tokens(self, document: HwpxDocument) -> dict[str, str]:
|
|
173
191
|
"""Create/reuse run styles and return semantic token IDs."""
|
|
174
192
|
|
|
175
193
|
return {
|
|
176
|
-
"title": document.ensure_run_style(
|
|
177
|
-
|
|
194
|
+
"title": document.ensure_run_style(
|
|
195
|
+
bold=self.title_bold,
|
|
196
|
+
size=self.title_size,
|
|
197
|
+
font=self.font,
|
|
198
|
+
color=self.title_color,
|
|
199
|
+
),
|
|
200
|
+
"subtitle": document.ensure_run_style(
|
|
201
|
+
italic=self.subtitle_italic,
|
|
202
|
+
size=self.subtitle_size,
|
|
203
|
+
font=self.font,
|
|
204
|
+
color=self.subtitle_color,
|
|
205
|
+
),
|
|
178
206
|
"heading": document.ensure_run_style(
|
|
179
207
|
bold=self.heading_bold,
|
|
180
208
|
underline=self.heading_underline,
|
|
209
|
+
size=self.heading_size,
|
|
210
|
+
font=self.font,
|
|
211
|
+
color=self.heading_color,
|
|
212
|
+
),
|
|
213
|
+
"body": document.ensure_run_style(size=self.body_size, font=self.font),
|
|
214
|
+
"bullet": document.ensure_run_style(size=self.body_size, font=self.font),
|
|
215
|
+
"meta": document.ensure_run_style(
|
|
216
|
+
size=self.meta_size,
|
|
217
|
+
font=self.font,
|
|
218
|
+
color=self.meta_color,
|
|
219
|
+
),
|
|
220
|
+
"table_header": document.ensure_run_style(
|
|
221
|
+
bold=self.table_header_bold,
|
|
222
|
+
size=self.body_size,
|
|
223
|
+
font=self.font,
|
|
224
|
+
),
|
|
225
|
+
"table_cell": document.ensure_run_style(
|
|
226
|
+
size=self.body_size,
|
|
227
|
+
font=self.font,
|
|
181
228
|
),
|
|
182
|
-
"body": document.ensure_run_style(),
|
|
183
|
-
"bullet": document.ensure_run_style(),
|
|
184
|
-
"table_header": document.ensure_run_style(bold=self.table_header_bold),
|
|
185
|
-
"table_cell": document.ensure_run_style(),
|
|
186
229
|
}
|
|
187
230
|
|
|
188
231
|
|
|
232
|
+
def _outline_style_refs(document: HwpxDocument, level: int) -> dict[str, str | int]:
|
|
233
|
+
"""Return paragraph style refs for a HWP outline heading level, if available."""
|
|
234
|
+
|
|
235
|
+
safe_level = min(10, max(1, int(level)))
|
|
236
|
+
for style in document.styles.values():
|
|
237
|
+
name = str(style.name or "")
|
|
238
|
+
eng_name = str(style.eng_name or "")
|
|
239
|
+
if name == f"개요 {safe_level}" or eng_name == f"Outline {safe_level}":
|
|
240
|
+
refs: dict[str, str | int] = {}
|
|
241
|
+
style_id = style.raw_id if style.raw_id is not None else style.id
|
|
242
|
+
if style_id is None:
|
|
243
|
+
continue
|
|
244
|
+
refs["style_id_ref"] = style_id
|
|
245
|
+
if style.para_pr_id_ref is not None:
|
|
246
|
+
refs["para_pr_id_ref"] = int(style.para_pr_id_ref)
|
|
247
|
+
return refs
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
|
|
189
251
|
def _plan_issue(
|
|
190
252
|
code: str,
|
|
191
253
|
path: str,
|
|
@@ -681,12 +743,15 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
681
743
|
issues.extend(_computed_field_issues(raw_block.get("text"), path=f"{path}.text"))
|
|
682
744
|
elif block_type == "paragraph":
|
|
683
745
|
issues.extend(_computed_field_issues(raw_block.get("text"), path=f"{path}.text"))
|
|
684
|
-
|
|
746
|
+
children = raw_block.get("children")
|
|
747
|
+
if children is None:
|
|
748
|
+
children = raw_block.get("runs")
|
|
749
|
+
for child_index, child in enumerate(children or []):
|
|
685
750
|
if isinstance(child, Mapping):
|
|
686
751
|
issues.extend(
|
|
687
752
|
_computed_field_issues(
|
|
688
753
|
child.get("text"),
|
|
689
|
-
path=f"{path}.
|
|
754
|
+
path=f"{path}.runs[{child_index}].text",
|
|
690
755
|
)
|
|
691
756
|
)
|
|
692
757
|
elif block_type in {"bullets", "bullet", "numbered_list", "numberedList"}:
|
|
@@ -731,32 +796,59 @@ def create_document_from_plan(
|
|
|
731
796
|
else DocumentStylePreset(name=str(preset or normalized.style_preset or DEFAULT_STYLE_PRESET))
|
|
732
797
|
)
|
|
733
798
|
document = HwpxDocument.new()
|
|
799
|
+
document.set_page_setup(
|
|
800
|
+
margins_mm={
|
|
801
|
+
"left": _DEFAULT_PAGE_MARGIN_MM,
|
|
802
|
+
"right": _DEFAULT_PAGE_MARGIN_MM,
|
|
803
|
+
"top": _DEFAULT_PAGE_MARGIN_MM,
|
|
804
|
+
"bottom": _DEFAULT_PAGE_MARGIN_MM,
|
|
805
|
+
}
|
|
806
|
+
)
|
|
734
807
|
tokens = style_preset.ensure_tokens(document)
|
|
735
808
|
builder_document = _lower_plan_to_builder_document(normalized)
|
|
736
809
|
|
|
737
810
|
if normalized.title:
|
|
738
|
-
document.add_paragraph(
|
|
811
|
+
paragraph = document.add_paragraph(
|
|
739
812
|
normalized.title,
|
|
740
813
|
char_pr_id_ref=tokens["title"],
|
|
741
814
|
inherit_style=False,
|
|
742
815
|
)
|
|
816
|
+
_format_para(
|
|
817
|
+
document,
|
|
818
|
+
paragraph,
|
|
819
|
+
alignment="center",
|
|
820
|
+
line_spacing=130,
|
|
821
|
+
after_pt=2,
|
|
822
|
+
bottom_border=style_preset.title_rule,
|
|
823
|
+
border_color=style_preset.rule_color,
|
|
824
|
+
)
|
|
743
825
|
if normalized.subtitle:
|
|
744
|
-
document.add_paragraph(
|
|
826
|
+
paragraph = document.add_paragraph(
|
|
745
827
|
normalized.subtitle,
|
|
746
828
|
char_pr_id_ref=tokens["subtitle"],
|
|
747
829
|
inherit_style=False,
|
|
748
830
|
)
|
|
831
|
+
_format_para(document, paragraph, line_spacing=130, after_pt=10)
|
|
749
832
|
|
|
750
833
|
if normalized.metadata:
|
|
751
|
-
document.add_paragraph(
|
|
834
|
+
paragraph = document.add_paragraph(
|
|
752
835
|
"문서 정보",
|
|
753
836
|
char_pr_id_ref=tokens["heading"],
|
|
754
837
|
inherit_style=False,
|
|
755
838
|
)
|
|
839
|
+
_format_para(
|
|
840
|
+
document,
|
|
841
|
+
paragraph,
|
|
842
|
+
line_spacing=150,
|
|
843
|
+
before_pt=14,
|
|
844
|
+
after_pt=4,
|
|
845
|
+
bottom_border=style_preset.heading_rule,
|
|
846
|
+
border_color=style_preset.rule_color,
|
|
847
|
+
)
|
|
756
848
|
_add_key_value_table(document, normalized.metadata, tokens)
|
|
757
849
|
|
|
758
850
|
for block in builder_document.sections[0].children:
|
|
759
|
-
_render_block(document, block, tokens)
|
|
851
|
+
_render_block(document, block, tokens, style_preset=style_preset)
|
|
760
852
|
|
|
761
853
|
return document
|
|
762
854
|
|
|
@@ -956,6 +1048,14 @@ def _validate_block(raw_block: Any, *, index: int) -> list[PlanValidationIssue]:
|
|
|
956
1048
|
elif block_type == "paragraph":
|
|
957
1049
|
issues.extend(_validate_paragraph_block(raw_block, path=path))
|
|
958
1050
|
issues.extend(_computed_field_issues(raw_block.get("text"), path=f"{path}.text"))
|
|
1051
|
+
for run_index, run in enumerate(raw_block.get("runs") or []):
|
|
1052
|
+
if isinstance(run, Mapping):
|
|
1053
|
+
issues.extend(
|
|
1054
|
+
_computed_field_issues(
|
|
1055
|
+
run.get("text"),
|
|
1056
|
+
path=f"{path}.runs[{run_index}].text",
|
|
1057
|
+
)
|
|
1058
|
+
)
|
|
959
1059
|
elif block_type == "bullets":
|
|
960
1060
|
items = _string_list(raw_block.get("items") or raw_block.get("bullets"))
|
|
961
1061
|
if not items:
|
|
@@ -1042,7 +1142,37 @@ def _validate_heading_block(raw_block: Mapping[str, Any], *, path: str) -> list[
|
|
|
1042
1142
|
|
|
1043
1143
|
|
|
1044
1144
|
def _validate_paragraph_block(raw_block: Mapping[str, Any], *, path: str) -> list[PlanValidationIssue]:
|
|
1045
|
-
issues
|
|
1145
|
+
issues: list[PlanValidationIssue] = []
|
|
1146
|
+
text = str(raw_block.get("text") or "").strip()
|
|
1147
|
+
runs = raw_block.get("runs")
|
|
1148
|
+
has_rich_runs = False
|
|
1149
|
+
if runs is not None:
|
|
1150
|
+
if not isinstance(runs, list):
|
|
1151
|
+
issues.append(
|
|
1152
|
+
_plan_issue(
|
|
1153
|
+
"invalid_runs",
|
|
1154
|
+
f"{path}.runs",
|
|
1155
|
+
f"{path}.runs must be a list of run objects",
|
|
1156
|
+
suggestion="Use runs=[{'text': '...', 'bold': true, 'color': '#1F3864'}].",
|
|
1157
|
+
)
|
|
1158
|
+
)
|
|
1159
|
+
else:
|
|
1160
|
+
for run_index, run in enumerate(runs):
|
|
1161
|
+
run_path = f"{path}.runs[{run_index}]"
|
|
1162
|
+
if not isinstance(run, Mapping):
|
|
1163
|
+
issues.append(
|
|
1164
|
+
_plan_issue(
|
|
1165
|
+
"invalid_run",
|
|
1166
|
+
run_path,
|
|
1167
|
+
f"{run_path} must be a mapping",
|
|
1168
|
+
suggestion="Use a run object with text and optional bold/color fields.",
|
|
1169
|
+
)
|
|
1170
|
+
)
|
|
1171
|
+
continue
|
|
1172
|
+
if str(run.get("text") or "").strip():
|
|
1173
|
+
has_rich_runs = True
|
|
1174
|
+
if not text and not has_rich_runs:
|
|
1175
|
+
issues.extend(_validate_required_text_fields(raw_block, path=path, fields=("text",)))
|
|
1046
1176
|
style = str(raw_block.get("style") or "body").strip() or "body"
|
|
1047
1177
|
if style not in _SUPPORTED_STYLE_TOKENS:
|
|
1048
1178
|
issues.append(
|
|
@@ -1281,12 +1411,21 @@ def _normalize_block(raw_block: Any, *, index: int) -> DocumentBlock:
|
|
|
1281
1411
|
return DocumentBlock("heading", {"level": level, "text": replace_computed_fields(text)})
|
|
1282
1412
|
|
|
1283
1413
|
if block_type == "paragraph":
|
|
1414
|
+
runs = _normalize_paragraph_runs(raw_block.get("runs"), index=index)
|
|
1415
|
+
text = (
|
|
1416
|
+
replace_computed_fields(str(raw_block.get("text") or ""))
|
|
1417
|
+
if runs
|
|
1418
|
+
else replace_computed_fields(_required_text(raw_block, "text", index))
|
|
1419
|
+
)
|
|
1420
|
+
data: dict[str, Any] = {
|
|
1421
|
+
"text": text,
|
|
1422
|
+
"style": str(raw_block.get("style") or "body").strip() or "body",
|
|
1423
|
+
}
|
|
1424
|
+
if runs:
|
|
1425
|
+
data["runs"] = runs
|
|
1284
1426
|
return DocumentBlock(
|
|
1285
1427
|
"paragraph",
|
|
1286
|
-
|
|
1287
|
-
"text": replace_computed_fields(_required_text(raw_block, "text", index)),
|
|
1288
|
-
"style": str(raw_block.get("style") or "body").strip() or "body",
|
|
1289
|
-
},
|
|
1428
|
+
data,
|
|
1290
1429
|
)
|
|
1291
1430
|
|
|
1292
1431
|
if block_type == "bullets":
|
|
@@ -1331,6 +1470,29 @@ def _normalize_block(raw_block: Any, *, index: int) -> DocumentBlock:
|
|
|
1331
1470
|
return DocumentBlock("page_break", {})
|
|
1332
1471
|
|
|
1333
1472
|
|
|
1473
|
+
def _normalize_paragraph_runs(value: Any, *, index: int) -> list[dict[str, Any]]:
|
|
1474
|
+
if value is None:
|
|
1475
|
+
return []
|
|
1476
|
+
if not isinstance(value, list):
|
|
1477
|
+
raise ValueError(f"blocks[{index}].runs must be a list")
|
|
1478
|
+
runs: list[dict[str, Any]] = []
|
|
1479
|
+
for run_index, raw_run in enumerate(value):
|
|
1480
|
+
if not isinstance(raw_run, Mapping):
|
|
1481
|
+
raise ValueError(f"blocks[{index}].runs[{run_index}] must be a mapping")
|
|
1482
|
+
text = replace_computed_fields(str(raw_run.get("text") or ""))
|
|
1483
|
+
if not text:
|
|
1484
|
+
continue
|
|
1485
|
+
run: dict[str, Any] = {"text": text}
|
|
1486
|
+
if "bold" in raw_run:
|
|
1487
|
+
run["bold"] = bool(raw_run.get("bold"))
|
|
1488
|
+
if "color" in raw_run:
|
|
1489
|
+
color = _optional_str(raw_run.get("color"))
|
|
1490
|
+
if color is not None:
|
|
1491
|
+
run["color"] = color
|
|
1492
|
+
runs.append(run)
|
|
1493
|
+
return runs
|
|
1494
|
+
|
|
1495
|
+
|
|
1334
1496
|
def _normalize_v2_builder_document(plan: Mapping[str, Any]) -> BuilderDocument:
|
|
1335
1497
|
metadata = plan.get("metadata") or {}
|
|
1336
1498
|
builder_metadata = None
|
|
@@ -1458,9 +1620,12 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
|
|
|
1458
1620
|
text=replace_computed_fields(str(raw_block.get("text") or "")),
|
|
1459
1621
|
)
|
|
1460
1622
|
if block_type == "paragraph":
|
|
1623
|
+
raw_children = raw_block.get("children")
|
|
1624
|
+
if raw_children is None:
|
|
1625
|
+
raw_children = raw_block.get("runs")
|
|
1461
1626
|
children = tuple(
|
|
1462
1627
|
child
|
|
1463
|
-
for child in (_normalize_v2_paragraph_child(child) for child in
|
|
1628
|
+
for child in (_normalize_v2_paragraph_child(child) for child in raw_children or [])
|
|
1464
1629
|
if isinstance(child, BuilderRun)
|
|
1465
1630
|
)
|
|
1466
1631
|
return BuilderParagraph(
|
|
@@ -1606,6 +1771,15 @@ def _block_to_builder_nodes(block: DocumentBlock) -> tuple[Any, ...]:
|
|
|
1606
1771
|
),
|
|
1607
1772
|
)
|
|
1608
1773
|
if block.type == "paragraph":
|
|
1774
|
+
runs = block.data.get("runs") or []
|
|
1775
|
+
if runs:
|
|
1776
|
+
return (
|
|
1777
|
+
BuilderParagraph(
|
|
1778
|
+
text=str(block.data.get("text") or ""),
|
|
1779
|
+
children=tuple(_builder_run_from_plan(run) for run in runs),
|
|
1780
|
+
style=str(block.data.get("style") or "body"),
|
|
1781
|
+
),
|
|
1782
|
+
)
|
|
1609
1783
|
return (
|
|
1610
1784
|
BuilderParagraph(
|
|
1611
1785
|
text=str(block.data["text"]),
|
|
@@ -1642,6 +1816,14 @@ def _block_to_builder_nodes(block: DocumentBlock) -> tuple[Any, ...]:
|
|
|
1642
1816
|
raise ValueError(f"unsupported block type: {block.type!r}")
|
|
1643
1817
|
|
|
1644
1818
|
|
|
1819
|
+
def _builder_run_from_plan(run: Mapping[str, Any]) -> BuilderRun:
|
|
1820
|
+
return BuilderRun(
|
|
1821
|
+
text=str(run.get("text") or ""),
|
|
1822
|
+
bold=bool(run.get("bold", False)),
|
|
1823
|
+
color=_optional_str(run.get("color")),
|
|
1824
|
+
)
|
|
1825
|
+
|
|
1826
|
+
|
|
1645
1827
|
def _plan_table_column_widths(columns: list[dict[str, Any]]) -> list[int]:
|
|
1646
1828
|
total = sum(max(int(column.get("widthWeight", 1)), 1) for column in columns)
|
|
1647
1829
|
if total <= 0:
|
|
@@ -1707,33 +1889,134 @@ def _normalize_table_cell_value(value: Any) -> str:
|
|
|
1707
1889
|
return normalize_cell_text(value)
|
|
1708
1890
|
|
|
1709
1891
|
|
|
1892
|
+
def _format_para(
|
|
1893
|
+
document: HwpxDocument,
|
|
1894
|
+
paragraph: Any,
|
|
1895
|
+
*,
|
|
1896
|
+
alignment: str | None = None,
|
|
1897
|
+
line_spacing: int | None = None,
|
|
1898
|
+
before_pt: float | None = None,
|
|
1899
|
+
after_pt: float | None = None,
|
|
1900
|
+
bottom_border: bool = False,
|
|
1901
|
+
border_color: str = "#BFBFBF",
|
|
1902
|
+
) -> None:
|
|
1903
|
+
"""Apply breathing-room paragraph spacing to a freshly added paragraph.
|
|
1904
|
+
|
|
1905
|
+
Uses the public ``set_paragraph_format`` so unit conversion and paraPr
|
|
1906
|
+
deduplication are handled by the engine. Failures are non-fatal: spacing is
|
|
1907
|
+
a presentation nicety, never a reason to abort document generation.
|
|
1908
|
+
"""
|
|
1909
|
+
|
|
1910
|
+
kwargs: dict[str, Any] = {}
|
|
1911
|
+
if alignment is not None:
|
|
1912
|
+
kwargs["alignment"] = alignment
|
|
1913
|
+
if line_spacing is not None:
|
|
1914
|
+
kwargs["line_spacing_percent"] = line_spacing
|
|
1915
|
+
if before_pt is not None:
|
|
1916
|
+
kwargs["spacing_before_pt"] = before_pt
|
|
1917
|
+
if after_pt is not None:
|
|
1918
|
+
kwargs["spacing_after_pt"] = after_pt
|
|
1919
|
+
if bottom_border:
|
|
1920
|
+
kwargs["bottom_border"] = True
|
|
1921
|
+
kwargs["border_color"] = border_color
|
|
1922
|
+
if not kwargs:
|
|
1923
|
+
return
|
|
1924
|
+
try:
|
|
1925
|
+
index = document.paragraphs.index(paragraph)
|
|
1926
|
+
document.set_paragraph_format(paragraph_index=index, **kwargs)
|
|
1927
|
+
except (ValueError, KeyError):
|
|
1928
|
+
return
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _add_rich_runs(
|
|
1932
|
+
document: HwpxDocument,
|
|
1933
|
+
paragraph: Any,
|
|
1934
|
+
runs: Any,
|
|
1935
|
+
*,
|
|
1936
|
+
base_char_pr_id: str,
|
|
1937
|
+
) -> None:
|
|
1938
|
+
for run in runs:
|
|
1939
|
+
if not isinstance(run, BuilderRun):
|
|
1940
|
+
raise ValueError(f"unsupported paragraph child: {type(run).__name__}")
|
|
1941
|
+
char_pr_id = document.ensure_run_style(
|
|
1942
|
+
bold=bool(run.bold),
|
|
1943
|
+
italic=bool(run.italic),
|
|
1944
|
+
underline=bool(run.underline),
|
|
1945
|
+
color=run.color,
|
|
1946
|
+
font=run.font,
|
|
1947
|
+
size=run.size,
|
|
1948
|
+
highlight=run.highlight,
|
|
1949
|
+
strike=run.strike,
|
|
1950
|
+
base_char_pr_id=base_char_pr_id,
|
|
1951
|
+
)
|
|
1952
|
+
paragraph.add_run(str(run.text or ""), char_pr_id_ref=char_pr_id)
|
|
1953
|
+
|
|
1954
|
+
|
|
1710
1955
|
def _render_block(
|
|
1711
1956
|
document: HwpxDocument,
|
|
1712
1957
|
block: Any,
|
|
1713
1958
|
tokens: Mapping[str, str],
|
|
1959
|
+
*,
|
|
1960
|
+
style_preset: DocumentStylePreset,
|
|
1714
1961
|
) -> None:
|
|
1715
1962
|
if isinstance(block, BuilderHeading):
|
|
1716
|
-
document.add_paragraph(
|
|
1963
|
+
paragraph = document.add_paragraph(
|
|
1717
1964
|
block.text,
|
|
1718
1965
|
char_pr_id_ref=tokens["heading"],
|
|
1719
1966
|
inherit_style=False,
|
|
1967
|
+
**_outline_style_refs(document, block.level),
|
|
1968
|
+
)
|
|
1969
|
+
_format_para(
|
|
1970
|
+
document,
|
|
1971
|
+
paragraph,
|
|
1972
|
+
line_spacing=150,
|
|
1973
|
+
before_pt=14,
|
|
1974
|
+
after_pt=4,
|
|
1975
|
+
bottom_border=style_preset.heading_rule,
|
|
1976
|
+
border_color=style_preset.rule_color,
|
|
1720
1977
|
)
|
|
1721
1978
|
return
|
|
1722
1979
|
if isinstance(block, BuilderParagraph):
|
|
1723
1980
|
style = str(block.style or "body")
|
|
1724
|
-
|
|
1981
|
+
if block.children:
|
|
1982
|
+
paragraph = document.add_paragraph("", include_run=False, inherit_style=False)
|
|
1983
|
+
_add_rich_runs(
|
|
1984
|
+
document,
|
|
1985
|
+
paragraph,
|
|
1986
|
+
block.children,
|
|
1987
|
+
base_char_pr_id=tokens.get(style, tokens["body"]),
|
|
1988
|
+
)
|
|
1989
|
+
_format_para(
|
|
1990
|
+
document,
|
|
1991
|
+
paragraph,
|
|
1992
|
+
line_spacing=165,
|
|
1993
|
+
after_pt=4,
|
|
1994
|
+
bottom_border=style == "heading" and style_preset.heading_rule,
|
|
1995
|
+
border_color=style_preset.rule_color,
|
|
1996
|
+
)
|
|
1997
|
+
return
|
|
1998
|
+
paragraph = document.add_paragraph(
|
|
1725
1999
|
block.text,
|
|
1726
2000
|
char_pr_id_ref=tokens.get(style, tokens["body"]),
|
|
1727
2001
|
inherit_style=False,
|
|
1728
2002
|
)
|
|
2003
|
+
_format_para(
|
|
2004
|
+
document,
|
|
2005
|
+
paragraph,
|
|
2006
|
+
line_spacing=165,
|
|
2007
|
+
after_pt=4,
|
|
2008
|
+
bottom_border=style == "heading" and style_preset.heading_rule,
|
|
2009
|
+
border_color=style_preset.rule_color,
|
|
2010
|
+
)
|
|
1729
2011
|
return
|
|
1730
2012
|
if isinstance(block, BuilderBullet):
|
|
1731
2013
|
for item in block.items:
|
|
1732
|
-
document.add_paragraph(
|
|
2014
|
+
paragraph = document.add_paragraph(
|
|
1733
2015
|
f"• {item}",
|
|
1734
2016
|
char_pr_id_ref=tokens["bullet"],
|
|
1735
2017
|
inherit_style=False,
|
|
1736
2018
|
)
|
|
2019
|
+
_format_para(document, paragraph, line_spacing=150, after_pt=2)
|
|
1737
2020
|
return
|
|
1738
2021
|
if isinstance(block, BuilderTable):
|
|
1739
2022
|
_add_builder_table(document, block, tokens)
|
|
@@ -1814,6 +2097,7 @@ def _add_plan_table(
|
|
|
1814
2097
|
str(row.get(column["key"], "")),
|
|
1815
2098
|
char_pr_id_ref=tokens["table_cell"],
|
|
1816
2099
|
)
|
|
2100
|
+
_style_plan_table(document, table, header_fill=_TABLE_HEADER_FILL)
|
|
1817
2101
|
|
|
1818
2102
|
|
|
1819
2103
|
def _add_builder_table(
|
|
@@ -1854,6 +2138,12 @@ def _add_builder_table(
|
|
|
1854
2138
|
str(value),
|
|
1855
2139
|
char_pr_id_ref=tokens["table_cell"],
|
|
1856
2140
|
)
|
|
2141
|
+
_style_plan_table(
|
|
2142
|
+
document,
|
|
2143
|
+
table,
|
|
2144
|
+
header_fill=table_node.header_shading or _TABLE_HEADER_FILL,
|
|
2145
|
+
header_rows=1 if table_node.header else 0,
|
|
2146
|
+
)
|
|
1857
2147
|
|
|
1858
2148
|
|
|
1859
2149
|
def _set_table_cell_text(
|
|
@@ -1870,6 +2160,47 @@ def _set_table_cell_text(
|
|
|
1870
2160
|
paragraph.char_pr_id_ref = char_pr_id_ref
|
|
1871
2161
|
|
|
1872
2162
|
|
|
2163
|
+
def _style_plan_table(
|
|
2164
|
+
document: HwpxDocument,
|
|
2165
|
+
table: Any,
|
|
2166
|
+
*,
|
|
2167
|
+
header_fill: str,
|
|
2168
|
+
header_rows: int = 1,
|
|
2169
|
+
) -> None:
|
|
2170
|
+
border_fill_id = document.ensure_border_fill(border_color=_TABLE_BORDER_COLOR)
|
|
2171
|
+
header_fill_id = document.ensure_border_fill(
|
|
2172
|
+
border_color=_TABLE_BORDER_COLOR,
|
|
2173
|
+
fill_color=header_fill,
|
|
2174
|
+
)
|
|
2175
|
+
table.element.set("borderFillIDRef", border_fill_id)
|
|
2176
|
+
center_para_pr_id: str | None = None
|
|
2177
|
+
if header_rows and document.oxml.headers:
|
|
2178
|
+
center_para_pr_id = document.oxml.headers[0].ensure_paragraph_format(alignment="center")
|
|
2179
|
+
|
|
2180
|
+
for row_index, row in enumerate(table.rows):
|
|
2181
|
+
for cell in row.cells:
|
|
2182
|
+
is_header = row_index < header_rows
|
|
2183
|
+
cell.element.set("borderFillIDRef", header_fill_id if is_header else border_fill_id)
|
|
2184
|
+
cell.element.set("hasMargin", "1")
|
|
2185
|
+
_set_cell_margin(cell)
|
|
2186
|
+
sublist = cell.element.find(f"{_HP}subList")
|
|
2187
|
+
if sublist is not None:
|
|
2188
|
+
sublist.set("vertAlign", "CENTER")
|
|
2189
|
+
if is_header and center_para_pr_id is not None:
|
|
2190
|
+
for paragraph in cell.paragraphs:
|
|
2191
|
+
paragraph.para_pr_id_ref = center_para_pr_id
|
|
2192
|
+
table.mark_dirty()
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
def _set_cell_margin(cell: Any) -> None:
|
|
2196
|
+
margin = cell.element.find(f"{_HP}cellMargin")
|
|
2197
|
+
if margin is None:
|
|
2198
|
+
margin = cell.element.makeelement(f"{_HP}cellMargin", {})
|
|
2199
|
+
cell.element.append(margin)
|
|
2200
|
+
for side in ("left", "right", "top", "bottom"):
|
|
2201
|
+
margin.set(side, _TABLE_CELL_MARGIN)
|
|
2202
|
+
|
|
2203
|
+
|
|
1873
2204
|
def _apply_column_widths(table: Any, columns: list[dict[str, Any]]) -> None:
|
|
1874
2205
|
total = sum(max(int(column.get("widthWeight", 1)), 1) for column in columns)
|
|
1875
2206
|
if total <= 0:
|