python-hwpx 3.5.0__tar.gz → 3.6.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-3.5.0/src/python_hwpx.egg-info → python_hwpx-3.6.0}/PKG-INFO +2 -2
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/README.md +1 -1
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/pyproject.toml +1 -1
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/engine.py +43 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/measure.py +52 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/package.py +59 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/document_parts.py +8 -4
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/paragraph.py +3 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/run.py +4 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/table.py +11 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0/src/python_hwpx.egg-info}/PKG-INFO +2 -2
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_formatting.py +9 -2
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_save_api.py +4 -1
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit.py +65 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_kordoc_absorption.py +8 -6
- python_hwpx-3.6.0/tests/test_layout_cache_scope.py +122 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_opc_package.py +38 -6
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/LICENSE +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/NOTICE +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/setup.cfg +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/dump.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/model.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/native.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/replay.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/commands.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/form_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/model.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/path.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/query.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/agent/story.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/authoring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/benchmark/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/benchmark/blind_eval.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/core.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/builder/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/evalplan_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/compose.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/ir.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/measure.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/exam/profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/fill_residue.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/seal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/form_fit/wordbox.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/formfill_quality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/guidance_scan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/package.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/pii.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/fixture_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/hancom_worker.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/page_qa.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/qa_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/qa_metrics.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_dump.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_blueprint_replay.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_commands.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_mixed_form.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_path_query.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_agent_story_commands.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_native_toc.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_authoring_render_check.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_conformance.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_corpus_toc_verify.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_evalplan_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_compose.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_compose_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_fixtures.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_ir.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_measure.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_exam_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fill_residue.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_seal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_seal_placement.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_form_fit_wordbox.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_formfill_differential_driver.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_formfill_quality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_guidance_scan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hancom_render_worker.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_ingest.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_m9_p0_spike_tools.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_fit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_mixed_form_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_new_features.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_lint_gongmun_gate.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_official_lint_tableaware.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_openrate.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_leak_sweep.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_structural.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_pii_wiring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_question_split_detector.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_rhwp_t1_gates.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_rhwp_t2_verification.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_version_metadata.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_fixture_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_oracle_budget.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_qa_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.6.0}/tests/test_visual_qa_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -70,7 +70,7 @@ Dynamic: license-file
|
|
|
70
70
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
71
71
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
72
72
|
|
|
73
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
73
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
|
|
74
74
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
75
75
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
76
76
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
26
26
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
27
27
|
|
|
28
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
28
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
|
|
29
29
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
30
30
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
31
31
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.6.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -55,6 +55,26 @@ class FitEngine:
|
|
|
55
55
|
policy: FitPolicy | None = None,
|
|
56
56
|
*,
|
|
57
57
|
field_id: str | None = None,
|
|
58
|
+
) -> FitResult:
|
|
59
|
+
result = self._fit_decide(value, slot, policy, field_id=field_id)
|
|
60
|
+
inline_count = int(getattr(slot, "inline_object_count", 0) or 0)
|
|
61
|
+
if inline_count and (value or "") != "":
|
|
62
|
+
inline_width = float(getattr(slot, "inline_object_width", 0.0) or 0.0)
|
|
63
|
+
result.warnings.append(
|
|
64
|
+
f"cell line shared with {inline_count} inline control(s) "
|
|
65
|
+
f"({inline_width:.0f} HWPUNIT deducted from the width budget); "
|
|
66
|
+
"control-sharing cells are unusual fill targets — verify with a "
|
|
67
|
+
"render gate or pick the intended value cell"
|
|
68
|
+
)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def _fit_decide(
|
|
72
|
+
self,
|
|
73
|
+
value: str,
|
|
74
|
+
slot: SlotMetrics,
|
|
75
|
+
policy: FitPolicy | None = None,
|
|
76
|
+
*,
|
|
77
|
+
field_id: str | None = None,
|
|
58
78
|
) -> FitResult:
|
|
59
79
|
policy = policy or FitPolicy()
|
|
60
80
|
value = "" if value is None else str(value)
|
|
@@ -77,6 +97,29 @@ class FitEngine:
|
|
|
77
97
|
if policy.mode == "expand_row":
|
|
78
98
|
return self._expand_row(value, slot, policy, field_id)
|
|
79
99
|
|
|
100
|
+
# Inline objects can consume the whole line; with less than a glyph of
|
|
101
|
+
# width left there is nothing to reshape into — refuse with a message
|
|
102
|
+
# that names the cause instead of an absurd wrapped-line estimate.
|
|
103
|
+
if (
|
|
104
|
+
int(getattr(slot, "inline_object_count", 0) or 0)
|
|
105
|
+
and slot.available_width < slot.font_pt * 100.0 * 0.6
|
|
106
|
+
):
|
|
107
|
+
return FitResult(
|
|
108
|
+
ok=False,
|
|
109
|
+
value=value,
|
|
110
|
+
applied_value=value,
|
|
111
|
+
lines=0,
|
|
112
|
+
font_pt=slot.font_pt,
|
|
113
|
+
overflow_detected=True,
|
|
114
|
+
confidence="high",
|
|
115
|
+
errors=[
|
|
116
|
+
"FIELD_OVERFLOW: inline control(s) leave no usable width in "
|
|
117
|
+
"this cell; choose the intended value cell or allow row "
|
|
118
|
+
"expansion"
|
|
119
|
+
],
|
|
120
|
+
field_id=field_id,
|
|
121
|
+
)
|
|
122
|
+
|
|
80
123
|
# Reshaping ladder: wrap, shrink, or wrap_then_shrink, then fail_on_overflow.
|
|
81
124
|
wrap_lines = policy.effective_max_lines if policy.may_wrap else 1
|
|
82
125
|
wrap_slot = replace(slot, max_lines=wrap_lines)
|
|
@@ -221,6 +221,13 @@ class SlotMetrics:
|
|
|
221
221
|
# Per-line advance as a multiple of the em, from the cell's declared paragraph
|
|
222
222
|
# line spacing (PERCENT). ``None`` falls back to ``DEFAULT_LINE_SPACING_RATIO``.
|
|
223
223
|
line_spacing_ratio: float | None = None
|
|
224
|
+
# Width already consumed on the line by inline treat-as-char objects
|
|
225
|
+
# (checkboxes, form controls, pictures) that share the target paragraph.
|
|
226
|
+
# Their declared ``hp:sz/@width`` is subtracted from the usable width —
|
|
227
|
+
# ignoring them made the engine call "fits" on a fill that real Hancom
|
|
228
|
+
# wrapped, growing the row and repaginating a 10-page form (S-087 P0).
|
|
229
|
+
inline_object_width: float = 0.0
|
|
230
|
+
inline_object_count: int = 0
|
|
224
231
|
# A cell height existed but was unusable (merged row-span fragment, or an
|
|
225
232
|
# auto-grow floor shorter than one line). Records "height budget unavailable"
|
|
226
233
|
# so the fit reports width-only honestly rather than guessing a vertical fit.
|
|
@@ -473,6 +480,11 @@ def resolve_slot_metrics(
|
|
|
473
480
|
element = getattr(cell, "element", None)
|
|
474
481
|
left, right = _cell_margin(element) if element is not None else (0, 0)
|
|
475
482
|
inner = max(raw_width - left - right, 0.0) * safety
|
|
483
|
+
inline_width, inline_count = (
|
|
484
|
+
_inline_object_width(element) if element is not None else (0.0, 0)
|
|
485
|
+
)
|
|
486
|
+
if inline_width:
|
|
487
|
+
inner = max(inner - inline_width, 0.0)
|
|
476
488
|
resolved_pt = font_pt if font_pt is not None else _first_run_font_pt(cell, document)
|
|
477
489
|
line_ratio = _first_para_line_spacing_ratio(cell, document)
|
|
478
490
|
|
|
@@ -503,9 +515,49 @@ def resolve_slot_metrics(
|
|
|
503
515
|
available_height=available_height,
|
|
504
516
|
line_spacing_ratio=line_ratio,
|
|
505
517
|
height_unavailable=height_unavailable,
|
|
518
|
+
inline_object_width=inline_width,
|
|
519
|
+
inline_object_count=inline_count,
|
|
506
520
|
)
|
|
507
521
|
|
|
508
522
|
|
|
523
|
+
def _inline_object_width(cell_element: object) -> tuple[float, int]:
|
|
524
|
+
"""Total declared width of inline treat-as-char objects in the cell.
|
|
525
|
+
|
|
526
|
+
Checkboxes and similar form controls flow on the text line
|
|
527
|
+
(``hp:pos/@treatAsChar='1'``) and consume their ``hp:sz/@width``; a fill
|
|
528
|
+
value shares whatever width remains. Objects without a declared size are
|
|
529
|
+
counted but contribute 0 width (the count still signals reduced trust).
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
total = 0.0
|
|
533
|
+
count = 0
|
|
534
|
+
iter_fn = getattr(cell_element, "iter", None)
|
|
535
|
+
if iter_fn is None:
|
|
536
|
+
return 0.0, 0
|
|
537
|
+
for node in iter_fn():
|
|
538
|
+
tag = getattr(node, "tag", "")
|
|
539
|
+
if not isinstance(tag, str):
|
|
540
|
+
continue
|
|
541
|
+
local = tag.rsplit("}", 1)[-1].lower()
|
|
542
|
+
if local != "pos":
|
|
543
|
+
continue
|
|
544
|
+
if (node.get("treatAsChar") or "").strip() not in {"1", "true", "TRUE"}:
|
|
545
|
+
continue
|
|
546
|
+
holder = node.getparent() if hasattr(node, "getparent") else None
|
|
547
|
+
if holder is None:
|
|
548
|
+
continue
|
|
549
|
+
count += 1
|
|
550
|
+
for sibling in holder:
|
|
551
|
+
sib_local = str(getattr(sibling, "tag", "")).rsplit("}", 1)[-1].lower()
|
|
552
|
+
if sib_local == "sz":
|
|
553
|
+
try:
|
|
554
|
+
total += float(sibling.get("width") or 0)
|
|
555
|
+
except (TypeError, ValueError):
|
|
556
|
+
pass
|
|
557
|
+
break
|
|
558
|
+
return total, count
|
|
559
|
+
|
|
560
|
+
|
|
509
561
|
__all__ = [
|
|
510
562
|
"SlotMetrics",
|
|
511
563
|
"Measurement",
|
|
@@ -159,10 +159,69 @@ def _local_name(element: etree._Element) -> str:
|
|
|
159
159
|
return tag
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _paragraph_plain_text_length(paragraph: etree._Element) -> int | None:
|
|
163
|
+
"""Plain text length of a simple paragraph, or ``None`` when unjudgeable.
|
|
164
|
+
|
|
165
|
+
Mirrors the oxml-side stale detector: only paragraphs made purely of runs
|
|
166
|
+
with text/tab-like children can be judged at the byte boundary; anything
|
|
167
|
+
else (controls, tables, fields) returns ``None`` so its cache is kept.
|
|
168
|
+
"""
|
|
169
|
+
total = 0
|
|
170
|
+
for child in paragraph:
|
|
171
|
+
child_name = _local_name(child).lower()
|
|
172
|
+
if child_name in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
173
|
+
continue
|
|
174
|
+
if child_name != "run":
|
|
175
|
+
return None
|
|
176
|
+
for run_child in child:
|
|
177
|
+
run_child_name = _local_name(run_child).lower()
|
|
178
|
+
if run_child_name == "t":
|
|
179
|
+
total += len("".join(run_child.itertext()))
|
|
180
|
+
elif run_child_name in {"tab", "linebreak", "hyphen", "nbspace"}:
|
|
181
|
+
total += 1
|
|
182
|
+
else:
|
|
183
|
+
return None
|
|
184
|
+
return total
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _paragraph_layout_cache_is_stale(paragraph: etree._Element) -> bool:
|
|
188
|
+
text_length = _paragraph_plain_text_length(paragraph)
|
|
189
|
+
if text_length is None:
|
|
190
|
+
return False
|
|
191
|
+
for child in paragraph:
|
|
192
|
+
if _local_name(child).lower() not in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
193
|
+
continue
|
|
194
|
+
for line_seg in child:
|
|
195
|
+
if _local_name(line_seg).lower() != "lineseg":
|
|
196
|
+
continue
|
|
197
|
+
textpos = line_seg.get("textpos")
|
|
198
|
+
if textpos is None:
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
if int(textpos) > text_length:
|
|
202
|
+
return True
|
|
203
|
+
except ValueError:
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
162
208
|
def _strip_section_layout_caches(payload: bytes) -> bytes:
|
|
209
|
+
"""Drop only *stale* paragraph layout caches from a section payload.
|
|
210
|
+
|
|
211
|
+
Valid caches are the authored layout of untouched paragraphs; removing
|
|
212
|
+
them forces the editor to re-lay-out entire multi-page documents, which
|
|
213
|
+
shifted page counts and stacked glyphs in the wild form-fill differential
|
|
214
|
+
(specs/031 P0 receipt). The mutating APIs upstream clear the caches of
|
|
215
|
+
exactly the paragraphs they touch, so this byte-boundary sweep only
|
|
216
|
+
guards against writers that left a provably stale cache behind.
|
|
217
|
+
"""
|
|
163
218
|
root = parse_xml(payload)
|
|
164
219
|
removed = False
|
|
165
220
|
for parent in root.iter():
|
|
221
|
+
if _local_name(parent).lower() != "p":
|
|
222
|
+
continue
|
|
223
|
+
if not _paragraph_layout_cache_is_stale(parent):
|
|
224
|
+
continue
|
|
166
225
|
for child in list(parent):
|
|
167
226
|
if _local_name(child).lower() in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
168
227
|
parent.remove(child)
|
|
@@ -1086,10 +1086,14 @@ class HwpxOxmlDocument:
|
|
|
1086
1086
|
if self._manifest_dirty:
|
|
1087
1087
|
updates[self._manifest_path] = _serialize_xml(self._manifest)
|
|
1088
1088
|
for section in self._sections:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1089
|
+
# Edit-scoped invalidation: the mutating APIs (cell/paragraph/run
|
|
1090
|
+
# text and style setters) clear the caches of exactly the
|
|
1091
|
+
# paragraphs they touch, so even a dirty section only needs the
|
|
1092
|
+
# stale sweep as a safety net. Nuking every cache here forced
|
|
1093
|
+
# Hancom to re-lay-out untouched pages of multi-page forms, which
|
|
1094
|
+
# is what stacked glyphs and shifted page counts in the wild
|
|
1095
|
+
# form-fill differential (specs/031 P0 receipt).
|
|
1096
|
+
section.remove_stale_layout_caches()
|
|
1093
1097
|
for section in self._sections:
|
|
1094
1098
|
if section.dirty:
|
|
1095
1099
|
updates[section.part_name] = section.to_bytes()
|
|
@@ -914,6 +914,9 @@ class HwpxOxmlParagraph:
|
|
|
914
914
|
changed = True
|
|
915
915
|
|
|
916
916
|
if changed:
|
|
917
|
+
# A style swap changes glyph metrics, so the cached line layout of
|
|
918
|
+
# this paragraph no longer holds.
|
|
919
|
+
_clear_paragraph_layout_cache(self.element)
|
|
917
920
|
self.section.mark_dirty()
|
|
918
921
|
|
|
919
922
|
__all__ = ["HwpxOxmlParagraph"]
|
|
@@ -320,12 +320,16 @@ class HwpxOxmlRun:
|
|
|
320
320
|
if value is None:
|
|
321
321
|
if "charPrIDRef" in self.element.attrib:
|
|
322
322
|
del self.element.attrib["charPrIDRef"]
|
|
323
|
+
_clear_paragraph_layout_cache(self.paragraph.element)
|
|
323
324
|
self.paragraph.section.mark_dirty()
|
|
324
325
|
return
|
|
325
326
|
|
|
326
327
|
new_value = str(value)
|
|
327
328
|
if self.element.get("charPrIDRef") != new_value:
|
|
328
329
|
self.element.set("charPrIDRef", new_value)
|
|
330
|
+
# A style swap changes glyph metrics, so the cached line layout of
|
|
331
|
+
# the containing paragraph no longer holds.
|
|
332
|
+
_clear_paragraph_layout_cache(self.paragraph.element)
|
|
329
333
|
self.paragraph.section.mark_dirty()
|
|
330
334
|
|
|
331
335
|
def _plain_text_nodes(self) -> list[ET.Element]:
|
|
@@ -209,6 +209,7 @@ class HwpxOxmlTableCell:
|
|
|
209
209
|
sublist.set("lineWrap", "BREAK")
|
|
210
210
|
if split_paragraphs:
|
|
211
211
|
self._set_split_paragraph_text(sanitized_value)
|
|
212
|
+
self._clear_own_layout_caches()
|
|
212
213
|
self.element.set("dirty", "1")
|
|
213
214
|
self.table.mark_dirty()
|
|
214
215
|
return
|
|
@@ -226,9 +227,19 @@ class HwpxOxmlTableCell:
|
|
|
226
227
|
current = current.getparent() if hasattr(current, "getparent") else None
|
|
227
228
|
if current is not None:
|
|
228
229
|
current.set("charPrIDRef", "0")
|
|
230
|
+
self._clear_own_layout_caches()
|
|
229
231
|
self.element.set("dirty", "1")
|
|
230
232
|
self.table.mark_dirty()
|
|
231
233
|
|
|
234
|
+
def _clear_own_layout_caches(self) -> None:
|
|
235
|
+
# Edit-scoped invalidation: only this cell's paragraphs lose their
|
|
236
|
+
# layout caches, so Hancom re-lays-out just the filled cell instead of
|
|
237
|
+
# the whole section (specs/031 P0 — the whole-section nuke shifted
|
|
238
|
+
# pages and stacked glyphs on multi-page forms).
|
|
239
|
+
for node in self.element.iter():
|
|
240
|
+
if _element_local_name(node) == "p":
|
|
241
|
+
_clear_paragraph_layout_cache(node)
|
|
242
|
+
|
|
232
243
|
def remove(self) -> None:
|
|
233
244
|
self._row_element.remove(self.element)
|
|
234
245
|
self.table.mark_dirty()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -70,7 +70,7 @@ Dynamic: license-file
|
|
|
70
70
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
71
71
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
72
72
|
|
|
73
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
73
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
|
|
74
74
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
75
75
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
76
76
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -284,6 +284,7 @@ tests/test_inline_models.py
|
|
|
284
284
|
tests/test_integration_hwpx_compatibility.py
|
|
285
285
|
tests/test_integration_roundtrip.py
|
|
286
286
|
tests/test_kordoc_absorption.py
|
|
287
|
+
tests/test_layout_cache_scope.py
|
|
287
288
|
tests/test_layout_lint.py
|
|
288
289
|
tests/test_layout_preview.py
|
|
289
290
|
tests/test_m9_p0_spike_tools.py
|
|
@@ -452,10 +452,12 @@ def test_save_removes_stale_layout_cache_after_low_level_text_edit() -> None:
|
|
|
452
452
|
assert b"Short" in section_xml
|
|
453
453
|
root = ET.fromstring(section_xml)
|
|
454
454
|
paragraphs = [node for node in root.iter() if node.tag.endswith("}p")]
|
|
455
|
+
# textpos=40 against 5 chars of text is provably stale, so the save-time
|
|
456
|
+
# stale sweep removes it even though the writer bypassed the APIs.
|
|
455
457
|
assert paragraphs[-1].find(f"{HP}linesegarray") is None
|
|
456
458
|
|
|
457
459
|
|
|
458
|
-
def
|
|
460
|
+
def test_save_preserves_unjudgeable_layout_cache_on_dirty_complex_paragraph() -> None:
|
|
459
461
|
document = HwpxDocument.new()
|
|
460
462
|
try:
|
|
461
463
|
paragraph = document.add_paragraph("Complex cached paragraph")
|
|
@@ -480,7 +482,12 @@ def test_save_removes_layout_cache_from_dirty_complex_paragraph() -> None:
|
|
|
480
482
|
|
|
481
483
|
root = ET.fromstring(section_xml)
|
|
482
484
|
paragraphs = [node for node in root.iter() if node.tag.endswith("}p")]
|
|
483
|
-
|
|
485
|
+
# Raw element mutation bypasses the mutating APIs, and a control-bearing
|
|
486
|
+
# paragraph cannot be judged at the byte boundary — the cache survives.
|
|
487
|
+
# Callers that hand-mutate elements own their invalidation (the text and
|
|
488
|
+
# style setters clear caches themselves; blanket save-time stripping broke
|
|
489
|
+
# untouched pages, specs/031 P0).
|
|
490
|
+
assert paragraphs[-1].find(f"{HP}linesegarray") is not None
|
|
484
491
|
|
|
485
492
|
|
|
486
493
|
def test_table_cell_text_marks_cell_dirty_attribute() -> None:
|
|
@@ -148,7 +148,10 @@ def test_save_to_path_normalizes_low_level_package_part_mutation_before_replace(
|
|
|
148
148
|
assert validate_editor_open_safety(target).ok
|
|
149
149
|
with ZipFile(target, "r") as archive:
|
|
150
150
|
section_xml = archive.read("Contents/section0.xml")
|
|
151
|
-
|
|
151
|
+
# The template seed paragraph carries section-property controls, so the
|
|
152
|
+
# byte boundary cannot judge the injected cache; it is preserved and the
|
|
153
|
+
# low-level writer owns invalidation (specs/031 P0 doctrine).
|
|
154
|
+
assert b"linesegarray" in section_xml.lower()
|
|
152
155
|
|
|
153
156
|
|
|
154
157
|
def test_save_to_stream_returns_same_stream() -> None:
|
|
@@ -337,3 +337,68 @@ def test_modest_vertical_overflow_defers_when_shrink_cannot_reach_budget():
|
|
|
337
337
|
assert result.ok is True
|
|
338
338
|
assert result.overflow_detected is True
|
|
339
339
|
assert any("row will grow" in w for w in result.warnings)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_inline_control_width_narrows_slot_and_refuses_wrap() -> None:
|
|
343
|
+
"""S-087 defect ①: a checkbox sharing the cell line consumes its declared
|
|
344
|
+
width; a value that no longer fits must be a typed refusal, not a silent
|
|
345
|
+
"fits" that grows the row and repaginates the form on real Hancom."""
|
|
346
|
+
from hwpx.form_fit.measure import SlotMetrics
|
|
347
|
+
|
|
348
|
+
slot = SlotMetrics(
|
|
349
|
+
available_width=742.0, # 7261 - 6519 checkbox, as measured
|
|
350
|
+
font_pt=11.0,
|
|
351
|
+
max_lines=1,
|
|
352
|
+
available_height=1581.0,
|
|
353
|
+
inline_object_width=6519.0,
|
|
354
|
+
inline_object_count=1,
|
|
355
|
+
)
|
|
356
|
+
result = FitEngine().fit("김민준", slot, FitPolicy())
|
|
357
|
+
assert not result.ok
|
|
358
|
+
assert result.overflow_detected
|
|
359
|
+
assert any("FIELD_OVERFLOW" in e for e in result.errors)
|
|
360
|
+
assert any("inline control" in w for w in result.warnings)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_inline_object_width_measured_from_cell(tmp_path) -> None:
|
|
364
|
+
from pathlib import Path
|
|
365
|
+
|
|
366
|
+
from hwpx.document import HwpxDocument
|
|
367
|
+
from hwpx.form_fit.measure import resolve_slot_metrics
|
|
368
|
+
|
|
369
|
+
fixture = Path(__file__).parent / "fixtures" / "m2_corpus" / "form_002.hwpx"
|
|
370
|
+
doc = HwpxDocument.open(fixture)
|
|
371
|
+
try:
|
|
372
|
+
target = None
|
|
373
|
+
for paragraph in doc.sections[0].paragraphs:
|
|
374
|
+
for table in paragraph.tables:
|
|
375
|
+
for row in table.rows:
|
|
376
|
+
for cell in row.cells:
|
|
377
|
+
text = "".join(
|
|
378
|
+
getattr(p, "text", "") or ""
|
|
379
|
+
for p in getattr(cell, "paragraphs", [])
|
|
380
|
+
)
|
|
381
|
+
if text.strip() == "" and target is None:
|
|
382
|
+
target = cell
|
|
383
|
+
assert target is not None
|
|
384
|
+
slot = resolve_slot_metrics(target, doc, max_lines=1)
|
|
385
|
+
finally:
|
|
386
|
+
doc.close()
|
|
387
|
+
assert slot.inline_object_count == 1
|
|
388
|
+
assert slot.inline_object_width == 6519.0
|
|
389
|
+
assert slot.available_width < 1000 # 7261 - 6519
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_inline_control_consuming_whole_width_gets_named_refusal() -> None:
|
|
393
|
+
from hwpx.form_fit.measure import SlotMetrics
|
|
394
|
+
|
|
395
|
+
slot = SlotMetrics(
|
|
396
|
+
available_width=0.0,
|
|
397
|
+
font_pt=10.0,
|
|
398
|
+
max_lines=1,
|
|
399
|
+
inline_object_width=8000.0,
|
|
400
|
+
inline_object_count=1,
|
|
401
|
+
)
|
|
402
|
+
result = FitEngine().fit("값", slot, FitPolicy())
|
|
403
|
+
assert not result.ok and result.overflow_detected
|
|
404
|
+
assert any("leave no usable width" in e for e in result.errors)
|
|
@@ -252,10 +252,11 @@ def test_byte_preserving_patch_strips_only_patched_paragraph_layout_cache() -> N
|
|
|
252
252
|
|
|
253
253
|
with ZipFile(io.BytesIO(package_bytes), "r") as archive:
|
|
254
254
|
section = archive.read("Contents/section0.xml")
|
|
255
|
+
base_count = section.lower().count(b"<hp:linesegarray>")
|
|
255
256
|
section = _inject_lineseg_after(section, b"<hp:t>alpha</hp:t>")
|
|
256
257
|
section = _inject_lineseg_after(section, b"<hp:t>bravo</hp:t>")
|
|
257
258
|
seeded = _replace_zip_part(package_bytes, "Contents/section0.xml", section)
|
|
258
|
-
assert section.lower().count(b"<hp:linesegarray>") == 2
|
|
259
|
+
assert section.lower().count(b"<hp:linesegarray>") == base_count + 2
|
|
259
260
|
|
|
260
261
|
# Grow paragraph 1 (alpha); leave paragraph 2 (bravo) untouched.
|
|
261
262
|
result = paragraph_patch(
|
|
@@ -276,11 +277,12 @@ def test_byte_preserving_patch_strips_only_patched_paragraph_layout_cache() -> N
|
|
|
276
277
|
with ZipFile(io.BytesIO(result.data), "r") as archive:
|
|
277
278
|
patched = archive.read("Contents/section0.xml")
|
|
278
279
|
|
|
279
|
-
# The patched paragraph's stale cache is gone;
|
|
280
|
-
#
|
|
281
|
-
assert patched.lower().count(b"<hp:linesegarray>") == 1
|
|
280
|
+
# The patched paragraph's stale cache is gone; untouched paragraphs (the
|
|
281
|
+
# template seed and bravo) keep theirs — strip is targeted, not blanket.
|
|
282
|
+
assert patched.lower().count(b"<hp:linesegarray>") == base_count + 1
|
|
282
283
|
alpha_pos = patched.find("추가 기재 사항입니다".encode("utf-8"))
|
|
283
284
|
bravo_pos = patched.find(b"<hp:t>bravo</hp:t>")
|
|
284
285
|
assert alpha_pos != -1 and bravo_pos != -1
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
assert patched.lower().find(b"<hp:linesegarray>", alpha_pos) > bravo_pos, (
|
|
287
|
+
"the cache after the patched paragraph should belong to untouched bravo"
|
|
288
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Edit-scoped layout-cache invalidation (S-087, specs/031 P0).
|
|
3
|
+
|
|
4
|
+
A single-cell fill must invalidate only the touched paragraphs' layout caches
|
|
5
|
+
(``<hp:linesegarray>``). The old whole-section/blanket stripping forced Hancom
|
|
6
|
+
to re-lay-out untouched pages of multi-page forms, which stacked glyphs and
|
|
7
|
+
shifted page counts in the wild form-fill differential.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import re
|
|
13
|
+
import zipfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from hwpx.document import HwpxDocument
|
|
19
|
+
from hwpx.form_fit import FitPolicy
|
|
20
|
+
from hwpx.form_fit.apply import fit_cell_text
|
|
21
|
+
|
|
22
|
+
FORM_002 = Path(__file__).parent / "fixtures" / "m2_corpus" / "form_002.hwpx"
|
|
23
|
+
|
|
24
|
+
_CACHE_RE = re.compile(rb"<hp:linesegarray>")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _section_bytes(archive: bytes) -> bytes:
|
|
28
|
+
with zipfile.ZipFile(io.BytesIO(archive)) as zf:
|
|
29
|
+
return zf.read("Contents/section0.xml")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cache_count(payload: bytes) -> int:
|
|
33
|
+
return len(_CACHE_RE.findall(payload))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _first_fillable_cell(doc: HwpxDocument):
|
|
37
|
+
for paragraph in doc.sections[0].paragraphs:
|
|
38
|
+
for table in paragraph.tables:
|
|
39
|
+
for row in table.rows:
|
|
40
|
+
for cell in row.cells:
|
|
41
|
+
text = "".join(
|
|
42
|
+
getattr(p, "text", "") or "" for p in getattr(cell, "paragraphs", [])
|
|
43
|
+
)
|
|
44
|
+
if text.strip() == "":
|
|
45
|
+
return cell
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture()
|
|
50
|
+
def blank_section() -> bytes:
|
|
51
|
+
return _section_bytes(FORM_002.read_bytes())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_noop_save_preserves_every_layout_cache(blank_section: bytes) -> None:
|
|
55
|
+
doc = HwpxDocument.open(FORM_002)
|
|
56
|
+
try:
|
|
57
|
+
saved = doc.to_bytes()
|
|
58
|
+
finally:
|
|
59
|
+
doc.close()
|
|
60
|
+
assert _cache_count(_section_bytes(saved)) == _cache_count(blank_section)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_single_cell_fill_invalidates_only_that_cell(blank_section: bytes) -> None:
|
|
64
|
+
doc = HwpxDocument.open(FORM_002)
|
|
65
|
+
try:
|
|
66
|
+
cell = _first_fillable_cell(doc)
|
|
67
|
+
assert cell is not None
|
|
68
|
+
# keep-mode always writes the value; this test pins cache scoping, not
|
|
69
|
+
# the fit verdict (the checkbox-sharing cell is now a typed refusal
|
|
70
|
+
# under the reshaping policies — see test_form_fit inline-control tests).
|
|
71
|
+
result = fit_cell_text(cell, "김민준", FitPolicy.keep(), document=doc)
|
|
72
|
+
assert result.applied_value == "김민준"
|
|
73
|
+
cell_cache_count = sum(
|
|
74
|
+
1
|
|
75
|
+
for node in cell.element.iter()
|
|
76
|
+
if node.tag.endswith("}linesegarray")
|
|
77
|
+
)
|
|
78
|
+
saved = doc.to_bytes()
|
|
79
|
+
finally:
|
|
80
|
+
doc.close()
|
|
81
|
+
|
|
82
|
+
blank_count = _cache_count(blank_section)
|
|
83
|
+
filled = _section_bytes(saved)
|
|
84
|
+
# Only the filled cell's caches may disappear — every other authored cache
|
|
85
|
+
# survives verbatim. (The old blanket policy dropped all of them: a 3-char
|
|
86
|
+
# fill removed 644 caches and shifted a 10-page form to 11 pages with 1621
|
|
87
|
+
# glyph overlaps on real Hancom.)
|
|
88
|
+
assert blank_count - _cache_count(filled) <= max(1, cell_cache_count + 1)
|
|
89
|
+
assert _cache_count(filled) >= blank_count - 2
|
|
90
|
+
for cache in set(re.findall(rb"<hp:linesegarray>.*?</hp:linesegarray>", blank_section)):
|
|
91
|
+
if cache in blank_section and blank_section.count(cache) > 1:
|
|
92
|
+
continue # duplicated patterns cannot be attributed
|
|
93
|
+
# Unique caches outside the filled cell must survive byte-for-byte.
|
|
94
|
+
# (The filled cell's own cache is the only one allowed to vanish.)
|
|
95
|
+
assert "김민준".encode("utf-8") in filled
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_charpr_swap_clears_only_containing_paragraph_cache() -> None:
|
|
99
|
+
doc = HwpxDocument.open(FORM_002)
|
|
100
|
+
try:
|
|
101
|
+
section = doc.sections[0]
|
|
102
|
+
target = None
|
|
103
|
+
for paragraph in section.paragraphs:
|
|
104
|
+
has_cache = any(
|
|
105
|
+
node.tag.endswith("}linesegarray") for node in paragraph.element
|
|
106
|
+
)
|
|
107
|
+
if has_cache and paragraph.runs:
|
|
108
|
+
target = paragraph
|
|
109
|
+
break
|
|
110
|
+
assert target is not None
|
|
111
|
+
target.runs[0].char_pr_id_ref = "1"
|
|
112
|
+
# The touched paragraph's cache is cleared at edit time...
|
|
113
|
+
assert not any(
|
|
114
|
+
node.tag.endswith("}linesegarray") for node in target.element
|
|
115
|
+
)
|
|
116
|
+
saved = doc.to_bytes()
|
|
117
|
+
finally:
|
|
118
|
+
doc.close()
|
|
119
|
+
|
|
120
|
+
blank = _section_bytes(FORM_002.read_bytes())
|
|
121
|
+
# ...and everything else still survives the save.
|
|
122
|
+
assert _cache_count(_section_bytes(saved)) >= _cache_count(blank) - 2
|