python-hwpx 2.11.1__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.1/src/python_hwpx.egg-info → python_hwpx-2.13.0}/PKG-INFO +5 -1
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/pyproject.toml +17 -1
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/__init__.py +8 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/authoring.py +321 -24
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/builder/core.py +5 -1
- {python_hwpx-2.11.1 → 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.1 → 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.1 → python_hwpx-2.13.0}/src/hwpx/oxml/_document_impl.py +242 -1
- {python_hwpx-2.11.1 → 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.1 → 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.1 → python_hwpx-2.13.0/src/python_hwpx.egg-info}/PKG-INFO +5 -1
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/SOURCES.txt +65 -1
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/entry_points.txt +1 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/requires.txt +5 -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.1 → python_hwpx-2.13.0}/tests/test_document_plan.py +109 -0
- {python_hwpx-2.11.1 → 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.1 → 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.1 → python_hwpx-2.13.0}/LICENSE +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/NOTICE +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/README.md +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/setup.cfg +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/package.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_new_features.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.11.1 → python_hwpx-2.13.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-2.11.1 → 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"
|
|
@@ -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,14 +169,23 @@ 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
|
|
171
|
-
title_size: int =
|
|
176
|
+
title_size: int = 20
|
|
172
177
|
subtitle_size: int = 12
|
|
173
178
|
heading_size: int = 14
|
|
174
|
-
|
|
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"
|
|
175
189
|
|
|
176
190
|
def ensure_tokens(self, document: HwpxDocument) -> dict[str, str]:
|
|
177
191
|
"""Create/reuse run styles and return semantic token IDs."""
|
|
@@ -181,22 +195,37 @@ class DocumentStylePreset:
|
|
|
181
195
|
bold=self.title_bold,
|
|
182
196
|
size=self.title_size,
|
|
183
197
|
font=self.font,
|
|
198
|
+
color=self.title_color,
|
|
184
199
|
),
|
|
185
200
|
"subtitle": document.ensure_run_style(
|
|
186
201
|
italic=self.subtitle_italic,
|
|
187
202
|
size=self.subtitle_size,
|
|
188
203
|
font=self.font,
|
|
204
|
+
color=self.subtitle_color,
|
|
189
205
|
),
|
|
190
206
|
"heading": document.ensure_run_style(
|
|
191
207
|
bold=self.heading_bold,
|
|
192
208
|
underline=self.heading_underline,
|
|
193
209
|
size=self.heading_size,
|
|
194
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,
|
|
195
228
|
),
|
|
196
|
-
"body": document.ensure_run_style(),
|
|
197
|
-
"bullet": document.ensure_run_style(),
|
|
198
|
-
"table_header": document.ensure_run_style(bold=self.table_header_bold),
|
|
199
|
-
"table_cell": document.ensure_run_style(),
|
|
200
229
|
}
|
|
201
230
|
|
|
202
231
|
|
|
@@ -714,12 +743,15 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
714
743
|
issues.extend(_computed_field_issues(raw_block.get("text"), path=f"{path}.text"))
|
|
715
744
|
elif block_type == "paragraph":
|
|
716
745
|
issues.extend(_computed_field_issues(raw_block.get("text"), path=f"{path}.text"))
|
|
717
|
-
|
|
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 []):
|
|
718
750
|
if isinstance(child, Mapping):
|
|
719
751
|
issues.extend(
|
|
720
752
|
_computed_field_issues(
|
|
721
753
|
child.get("text"),
|
|
722
|
-
path=f"{path}.
|
|
754
|
+
path=f"{path}.runs[{child_index}].text",
|
|
723
755
|
)
|
|
724
756
|
)
|
|
725
757
|
elif block_type in {"bullets", "bullet", "numbered_list", "numberedList"}:
|
|
@@ -764,32 +796,59 @@ def create_document_from_plan(
|
|
|
764
796
|
else DocumentStylePreset(name=str(preset or normalized.style_preset or DEFAULT_STYLE_PRESET))
|
|
765
797
|
)
|
|
766
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
|
+
)
|
|
767
807
|
tokens = style_preset.ensure_tokens(document)
|
|
768
808
|
builder_document = _lower_plan_to_builder_document(normalized)
|
|
769
809
|
|
|
770
810
|
if normalized.title:
|
|
771
|
-
document.add_paragraph(
|
|
811
|
+
paragraph = document.add_paragraph(
|
|
772
812
|
normalized.title,
|
|
773
813
|
char_pr_id_ref=tokens["title"],
|
|
774
814
|
inherit_style=False,
|
|
775
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
|
+
)
|
|
776
825
|
if normalized.subtitle:
|
|
777
|
-
document.add_paragraph(
|
|
826
|
+
paragraph = document.add_paragraph(
|
|
778
827
|
normalized.subtitle,
|
|
779
828
|
char_pr_id_ref=tokens["subtitle"],
|
|
780
829
|
inherit_style=False,
|
|
781
830
|
)
|
|
831
|
+
_format_para(document, paragraph, line_spacing=130, after_pt=10)
|
|
782
832
|
|
|
783
833
|
if normalized.metadata:
|
|
784
|
-
document.add_paragraph(
|
|
834
|
+
paragraph = document.add_paragraph(
|
|
785
835
|
"문서 정보",
|
|
786
836
|
char_pr_id_ref=tokens["heading"],
|
|
787
837
|
inherit_style=False,
|
|
788
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
|
+
)
|
|
789
848
|
_add_key_value_table(document, normalized.metadata, tokens)
|
|
790
849
|
|
|
791
850
|
for block in builder_document.sections[0].children:
|
|
792
|
-
_render_block(document, block, tokens)
|
|
851
|
+
_render_block(document, block, tokens, style_preset=style_preset)
|
|
793
852
|
|
|
794
853
|
return document
|
|
795
854
|
|
|
@@ -989,6 +1048,14 @@ def _validate_block(raw_block: Any, *, index: int) -> list[PlanValidationIssue]:
|
|
|
989
1048
|
elif block_type == "paragraph":
|
|
990
1049
|
issues.extend(_validate_paragraph_block(raw_block, path=path))
|
|
991
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
|
+
)
|
|
992
1059
|
elif block_type == "bullets":
|
|
993
1060
|
items = _string_list(raw_block.get("items") or raw_block.get("bullets"))
|
|
994
1061
|
if not items:
|
|
@@ -1075,7 +1142,37 @@ def _validate_heading_block(raw_block: Mapping[str, Any], *, path: str) -> list[
|
|
|
1075
1142
|
|
|
1076
1143
|
|
|
1077
1144
|
def _validate_paragraph_block(raw_block: Mapping[str, Any], *, path: str) -> list[PlanValidationIssue]:
|
|
1078
|
-
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",)))
|
|
1079
1176
|
style = str(raw_block.get("style") or "body").strip() or "body"
|
|
1080
1177
|
if style not in _SUPPORTED_STYLE_TOKENS:
|
|
1081
1178
|
issues.append(
|
|
@@ -1314,12 +1411,21 @@ def _normalize_block(raw_block: Any, *, index: int) -> DocumentBlock:
|
|
|
1314
1411
|
return DocumentBlock("heading", {"level": level, "text": replace_computed_fields(text)})
|
|
1315
1412
|
|
|
1316
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
|
|
1317
1426
|
return DocumentBlock(
|
|
1318
1427
|
"paragraph",
|
|
1319
|
-
|
|
1320
|
-
"text": replace_computed_fields(_required_text(raw_block, "text", index)),
|
|
1321
|
-
"style": str(raw_block.get("style") or "body").strip() or "body",
|
|
1322
|
-
},
|
|
1428
|
+
data,
|
|
1323
1429
|
)
|
|
1324
1430
|
|
|
1325
1431
|
if block_type == "bullets":
|
|
@@ -1364,6 +1470,29 @@ def _normalize_block(raw_block: Any, *, index: int) -> DocumentBlock:
|
|
|
1364
1470
|
return DocumentBlock("page_break", {})
|
|
1365
1471
|
|
|
1366
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
|
+
|
|
1367
1496
|
def _normalize_v2_builder_document(plan: Mapping[str, Any]) -> BuilderDocument:
|
|
1368
1497
|
metadata = plan.get("metadata") or {}
|
|
1369
1498
|
builder_metadata = None
|
|
@@ -1491,9 +1620,12 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
|
|
|
1491
1620
|
text=replace_computed_fields(str(raw_block.get("text") or "")),
|
|
1492
1621
|
)
|
|
1493
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")
|
|
1494
1626
|
children = tuple(
|
|
1495
1627
|
child
|
|
1496
|
-
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 [])
|
|
1497
1629
|
if isinstance(child, BuilderRun)
|
|
1498
1630
|
)
|
|
1499
1631
|
return BuilderParagraph(
|
|
@@ -1639,6 +1771,15 @@ def _block_to_builder_nodes(block: DocumentBlock) -> tuple[Any, ...]:
|
|
|
1639
1771
|
),
|
|
1640
1772
|
)
|
|
1641
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
|
+
)
|
|
1642
1783
|
return (
|
|
1643
1784
|
BuilderParagraph(
|
|
1644
1785
|
text=str(block.data["text"]),
|
|
@@ -1675,6 +1816,14 @@ def _block_to_builder_nodes(block: DocumentBlock) -> tuple[Any, ...]:
|
|
|
1675
1816
|
raise ValueError(f"unsupported block type: {block.type!r}")
|
|
1676
1817
|
|
|
1677
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
|
+
|
|
1678
1827
|
def _plan_table_column_widths(columns: list[dict[str, Any]]) -> list[int]:
|
|
1679
1828
|
total = sum(max(int(column.get("widthWeight", 1)), 1) for column in columns)
|
|
1680
1829
|
if total <= 0:
|
|
@@ -1740,34 +1889,134 @@ def _normalize_table_cell_value(value: Any) -> str:
|
|
|
1740
1889
|
return normalize_cell_text(value)
|
|
1741
1890
|
|
|
1742
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
|
+
|
|
1743
1955
|
def _render_block(
|
|
1744
1956
|
document: HwpxDocument,
|
|
1745
1957
|
block: Any,
|
|
1746
1958
|
tokens: Mapping[str, str],
|
|
1959
|
+
*,
|
|
1960
|
+
style_preset: DocumentStylePreset,
|
|
1747
1961
|
) -> None:
|
|
1748
1962
|
if isinstance(block, BuilderHeading):
|
|
1749
|
-
document.add_paragraph(
|
|
1963
|
+
paragraph = document.add_paragraph(
|
|
1750
1964
|
block.text,
|
|
1751
1965
|
char_pr_id_ref=tokens["heading"],
|
|
1752
1966
|
inherit_style=False,
|
|
1753
1967
|
**_outline_style_refs(document, block.level),
|
|
1754
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,
|
|
1977
|
+
)
|
|
1755
1978
|
return
|
|
1756
1979
|
if isinstance(block, BuilderParagraph):
|
|
1757
1980
|
style = str(block.style or "body")
|
|
1758
|
-
|
|
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(
|
|
1759
1999
|
block.text,
|
|
1760
2000
|
char_pr_id_ref=tokens.get(style, tokens["body"]),
|
|
1761
2001
|
inherit_style=False,
|
|
1762
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
|
+
)
|
|
1763
2011
|
return
|
|
1764
2012
|
if isinstance(block, BuilderBullet):
|
|
1765
2013
|
for item in block.items:
|
|
1766
|
-
document.add_paragraph(
|
|
2014
|
+
paragraph = document.add_paragraph(
|
|
1767
2015
|
f"• {item}",
|
|
1768
2016
|
char_pr_id_ref=tokens["bullet"],
|
|
1769
2017
|
inherit_style=False,
|
|
1770
2018
|
)
|
|
2019
|
+
_format_para(document, paragraph, line_spacing=150, after_pt=2)
|
|
1771
2020
|
return
|
|
1772
2021
|
if isinstance(block, BuilderTable):
|
|
1773
2022
|
_add_builder_table(document, block, tokens)
|
|
@@ -1848,6 +2097,7 @@ def _add_plan_table(
|
|
|
1848
2097
|
str(row.get(column["key"], "")),
|
|
1849
2098
|
char_pr_id_ref=tokens["table_cell"],
|
|
1850
2099
|
)
|
|
2100
|
+
_style_plan_table(document, table, header_fill=_TABLE_HEADER_FILL)
|
|
1851
2101
|
|
|
1852
2102
|
|
|
1853
2103
|
def _add_builder_table(
|
|
@@ -1888,6 +2138,12 @@ def _add_builder_table(
|
|
|
1888
2138
|
str(value),
|
|
1889
2139
|
char_pr_id_ref=tokens["table_cell"],
|
|
1890
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
|
+
)
|
|
1891
2147
|
|
|
1892
2148
|
|
|
1893
2149
|
def _set_table_cell_text(
|
|
@@ -1904,6 +2160,47 @@ def _set_table_cell_text(
|
|
|
1904
2160
|
paragraph.char_pr_id_ref = char_pr_id_ref
|
|
1905
2161
|
|
|
1906
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
|
+
|
|
1907
2204
|
def _apply_column_widths(table: Any, columns: list[dict[str, Any]]) -> None:
|
|
1908
2205
|
total = sum(max(int(column.get("widthWeight", 1)), 1) for column in columns)
|
|
1909
2206
|
if total <= 0:
|
|
@@ -769,7 +769,10 @@ class Document:
|
|
|
769
769
|
|
|
770
770
|
def save_to_path(self, path: str | PathLike[str]) -> BuilderSaveReport:
|
|
771
771
|
document = self.lower()
|
|
772
|
-
|
|
772
|
+
# Funnel the write through the single SavePipeline and keep its uniform
|
|
773
|
+
# report (plan §2 Phase B). Transparent policy -> behaviour-identical to
|
|
774
|
+
# the prior ``document.save_to_path`` for a from-scratch (new) document.
|
|
775
|
+
visual_complete = document.save_report(path)
|
|
773
776
|
package_report = validate_package(path)
|
|
774
777
|
document_report = validate_document(path)
|
|
775
778
|
editor_open_safety_report = validate_editor_open_safety(path)
|
|
@@ -799,5 +802,6 @@ class Document:
|
|
|
799
802
|
visual_review_required=visual_review_required,
|
|
800
803
|
feature_flags=feature_flags,
|
|
801
804
|
editor_open_safety=editor_open_safety_report,
|
|
805
|
+
visual_complete=visual_complete,
|
|
802
806
|
)
|
|
803
807
|
return report
|
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from os import PathLike
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from hwpx.quality import VisualCompleteReport
|
|
8
9
|
from hwpx.tools.id_integrity import IdIntegrityReport, check_id_integrity
|
|
9
10
|
from hwpx.tools.package_validator import EditorOpenSafetyReport, PackageValidationReport
|
|
10
11
|
from hwpx.tools.validator import ValidationReport
|
|
@@ -33,6 +34,10 @@ class BuilderSaveReport:
|
|
|
33
34
|
feature_flags: dict[str, bool] = field(default_factory=dict)
|
|
34
35
|
id_integrity: IdIntegrityReport | None = None
|
|
35
36
|
editor_open_safety: EditorOpenSafetyReport | None = None
|
|
37
|
+
# The uniform Phase-B report from the SavePipeline the builder save funnelled
|
|
38
|
+
# through (plan §2 Phase B). Additive: ``None`` only if a caller builds a
|
|
39
|
+
# report by hand without going through ``Document.save_to_path``.
|
|
40
|
+
visual_complete: VisualCompleteReport | None = None
|
|
36
41
|
|
|
37
42
|
def __post_init__(self) -> None:
|
|
38
43
|
hard_gates = dict(self.hard_gates)
|
|
@@ -53,6 +58,9 @@ class BuilderSaveReport:
|
|
|
53
58
|
"hard_gates": dict(self.hard_gates),
|
|
54
59
|
"visual_review_required": self.visual_review_required,
|
|
55
60
|
"feature_flags": dict(self.feature_flags),
|
|
61
|
+
"visual_complete": (
|
|
62
|
+
None if self.visual_complete is None else self.visual_complete.to_dict()
|
|
63
|
+
),
|
|
56
64
|
"editor_open_safety": (
|
|
57
65
|
None
|
|
58
66
|
if self.editor_open_safety is None
|