python-hwpx 6.4.0__tar.gz → 6.5.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-6.4.0/src/python_hwpx.egg-info → python_hwpx-6.5.0}/PKG-INFO +1 -1
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/pyproject.toml +1 -1
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/authoring.py +5 -0
- python_hwpx-6.5.0/src/hwpx/equation/mathml.py +135 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/render.py +2 -2
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/tokens.py +11 -0
- python_hwpx-6.5.0/src/hwpx/oxml/_paragraph_text_edit.py +42 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/paragraph.py +37 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/table.py +24 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0/src/python_hwpx.egg-info}/PKG-INFO +1 -1
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/SOURCES.txt +2 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_authoring.py +2 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_converter.py +25 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_equation_render.py +47 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_golden_api.py +25 -6
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_product_boundary.py +2 -1
- python_hwpx-6.5.0/tests/test_table_page_flow.py +18 -0
- python_hwpx-6.4.0/src/hwpx/equation/mathml.py +0 -69
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/LICENSE +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/NOTICE +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/README.md +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/setup.cfg +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_legacy.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_resolve.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/headings.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/highlight.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/_base.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/fields.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/media.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/notes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/page.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/parts.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/refs.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/shapes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/styles.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/tables.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/text.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/ns/tracking.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/capabilities.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/mutation-semantics.md +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/recipes-traversal.md +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/data/contract_docs/support-matrix.md +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/document.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/measure.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/model.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/binary_item.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/checkbox.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/form_field.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/highlight.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/results.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/objects/tracked.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document_metadata.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/drop_cap.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/dutmal_compose.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/field_marks.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header_compat.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/history_part.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/master_page.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/master_page_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/note_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/numbering_kinds.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_layout.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/settings.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/oxml/version_part.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/package.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_execute.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_model.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_schema.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/plan/_validate.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/document_merge.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_add_control.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_add_heading.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_arc_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_authoring_defaults.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_border_fill_image_gradient.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_build_element_census.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_capabilities_surface.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_chart_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_check_box_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_column_break.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_container_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_contract_docs_sync.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_ledger.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_merge.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_metadata.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_documentation_code_fences.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_drop_cap_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_dutmal_compose_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_edit_fidelity_corpus.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_edit_plan.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_editor_surface_inventory.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_engine_surface_6_0.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_error_contract.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_field_marks.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_header_compat_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_highlight_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_index_mark.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ingest.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_instid_attribute_case.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_llms_txt.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_master_page_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_memo_shape_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_namespace_hygiene.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_native_edit_contract.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_new_features.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_note_authoring_contract.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_fields.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_media.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_notes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_page.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_refs.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_shapes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_tables.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_ns_tracking.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_outline_numbering.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_page_number_control.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_para_pr_dedup.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_property_version_switch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_part_hierarchy_read_models.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_polygon_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_release_workflow_safety.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_return_contract.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_roundtrip_fidelity_corpus.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_run_choice_atom_authoring.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_safe_edit_example.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_section_properties_real_roundtrip.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_shape_open_safety_risk_check.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_style_resolution.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tab_definition_version_switch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_cell_equalize.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_label.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_table_split_merge.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_text_extractor_run_choice_atoms.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_title_mark.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_tracked_overlap_render_smoke.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-6.4.0 → python_hwpx-6.5.0}/tests/test_version_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.5.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -78,6 +78,11 @@ _LATEX_TO_EQEDIT.update(
|
|
|
78
78
|
"\\forall": "FORALL",
|
|
79
79
|
"\\iint": "dint",
|
|
80
80
|
"\\iiint": "tint",
|
|
81
|
+
# ``triangle`` is accepted when reading (observed in real documents) but
|
|
82
|
+
# only the documented uppercase spelling is emitted when authoring: the
|
|
83
|
+
# lowercase form has not been render-verified, and lowercase ``forall``
|
|
84
|
+
# already turned out to render as literal text.
|
|
85
|
+
"\\triangle": "TRIANGLE",
|
|
81
86
|
# Common LaTeX aliases sharing a verified target.
|
|
82
87
|
"\\le": "leq",
|
|
83
88
|
"\\ge": "geq",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""LaTeX → MathML conversion via the optional ``latex2mathml`` dependency.
|
|
3
|
+
|
|
4
|
+
MathML keeps the preview self-contained: browsers render ``<math>`` natively, so
|
|
5
|
+
no script or font bundle is shipped. ``latex2mathml`` is an optional extra
|
|
6
|
+
(``python-hwpx[preview]``); when it is absent the caller fails closed to a LaTeX
|
|
7
|
+
code block rather than dropping the equation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
from .tokens import EQEDIT_MATHML_OPERATOR_COMMANDS
|
|
16
|
+
|
|
17
|
+
# ``False`` marks a resolved-but-unavailable converter; ``None`` means "not yet
|
|
18
|
+
# probed" so the import is attempted lazily on first use.
|
|
19
|
+
_converter: Callable[[str], str] | bool | None = None
|
|
20
|
+
_LATEX_WORD_COMMAND_RE = re.compile(r"\\[A-Za-z]+")
|
|
21
|
+
_TEXT_COMMAND = "\\text{"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MathMLUnavailableError(RuntimeError):
|
|
25
|
+
"""Raised when ``latex2mathml`` is not installed."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_converter() -> Callable[[str], str] | bool:
|
|
29
|
+
global _converter
|
|
30
|
+
cached = _converter
|
|
31
|
+
if cached is not None:
|
|
32
|
+
return cached
|
|
33
|
+
try:
|
|
34
|
+
from latex2mathml.converter import convert
|
|
35
|
+
except ImportError:
|
|
36
|
+
_converter = False
|
|
37
|
+
return False
|
|
38
|
+
_converter = convert
|
|
39
|
+
return convert
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def latex2mathml_available() -> bool:
|
|
43
|
+
"""Return ``True`` when the optional ``latex2mathml`` extra is importable."""
|
|
44
|
+
|
|
45
|
+
return _load_converter() is not False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def latex_to_mathml(latex: str) -> str:
|
|
49
|
+
"""Convert a LaTeX fragment to an inline ``<math>`` MathML string.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
MathMLUnavailableError: when ``latex2mathml`` is not installed.
|
|
53
|
+
ValueError: when ``latex2mathml`` cannot parse the fragment.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
convert = _load_converter()
|
|
57
|
+
if convert is False:
|
|
58
|
+
raise MathMLUnavailableError(
|
|
59
|
+
"latex2mathml is required for MathML rendering; install python-hwpx[preview]"
|
|
60
|
+
)
|
|
61
|
+
assert not isinstance(convert, bool) # narrowed for type-checkers
|
|
62
|
+
try:
|
|
63
|
+
return convert(latex)
|
|
64
|
+
except MathMLUnavailableError:
|
|
65
|
+
raise
|
|
66
|
+
except Exception as exc: # latex2mathml raises bare Exception on bad input
|
|
67
|
+
raise ValueError(f"latex2mathml could not render fragment: {exc}") from exc
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def eqedit_latex_to_mathml(latex: str) -> str:
|
|
71
|
+
"""Convert EqEdit-derived LaTeX while preserving known token roles.
|
|
72
|
+
|
|
73
|
+
``latex2mathml`` classifies plain ``\\triangle`` as an identifier. EqEdit's
|
|
74
|
+
token map identifies it as a mathematical symbol, so the MathML-only input
|
|
75
|
+
receives an operator wrapper before conversion. The returned/public LaTeX
|
|
76
|
+
is not rewritten, and serialized MathML is never patched afterward.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def annotate_role(match: re.Match[str]) -> str:
|
|
80
|
+
command = match.group(0)
|
|
81
|
+
if command in EQEDIT_MATHML_OPERATOR_COMMANDS:
|
|
82
|
+
return rf"\mathop{{{command}}}"
|
|
83
|
+
return command
|
|
84
|
+
|
|
85
|
+
return latex_to_mathml(
|
|
86
|
+
"".join(
|
|
87
|
+
segment if literal else _LATEX_WORD_COMMAND_RE.sub(annotate_role, segment)
|
|
88
|
+
for segment, literal in _split_text_literals(latex)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _split_text_literals(latex: str) -> list[tuple[str, bool]]:
|
|
94
|
+
"""Split ``latex`` into ``(segment, is_text_literal)`` pieces.
|
|
95
|
+
|
|
96
|
+
Quoted EqEdit literals become ``\\text{...}``; their content is prose, so
|
|
97
|
+
the role annotation must not rewrite a ``\\triangle`` typed inside quotes.
|
|
98
|
+
Braces inside the literal are balanced; an unterminated group is passed
|
|
99
|
+
through untouched so ``latex2mathml`` reports it instead of this splitter.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
pieces: list[tuple[str, bool]] = []
|
|
103
|
+
cursor = 0
|
|
104
|
+
while True:
|
|
105
|
+
start = latex.find(_TEXT_COMMAND, cursor)
|
|
106
|
+
if start < 0:
|
|
107
|
+
break
|
|
108
|
+
depth = 0
|
|
109
|
+
end = -1
|
|
110
|
+
for index in range(start + len(_TEXT_COMMAND) - 1, len(latex)):
|
|
111
|
+
char = latex[index]
|
|
112
|
+
if char == "{":
|
|
113
|
+
depth += 1
|
|
114
|
+
elif char == "}":
|
|
115
|
+
depth -= 1
|
|
116
|
+
if depth == 0:
|
|
117
|
+
end = index + 1
|
|
118
|
+
break
|
|
119
|
+
if end < 0:
|
|
120
|
+
break
|
|
121
|
+
if start > cursor:
|
|
122
|
+
pieces.append((latex[cursor:start], False))
|
|
123
|
+
pieces.append((latex[start:end], True))
|
|
124
|
+
cursor = end
|
|
125
|
+
if cursor < len(latex):
|
|
126
|
+
pieces.append((latex[cursor:], False))
|
|
127
|
+
return pieces
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"MathMLUnavailableError",
|
|
132
|
+
"eqedit_latex_to_mathml",
|
|
133
|
+
"latex2mathml_available",
|
|
134
|
+
"latex_to_mathml",
|
|
135
|
+
]
|
|
@@ -18,7 +18,7 @@ from dataclasses import dataclass
|
|
|
18
18
|
import html
|
|
19
19
|
|
|
20
20
|
from .eqedit import EquationConversionError, eqedit_to_latex
|
|
21
|
-
from .mathml import MathMLUnavailableError,
|
|
21
|
+
from .mathml import MathMLUnavailableError, eqedit_latex_to_mathml
|
|
22
22
|
|
|
23
23
|
# Fidelity labels surfaced in the preview (Constitution IX, honest reporting).
|
|
24
24
|
LABEL_MATHML = "수식 MathML 렌더"
|
|
@@ -63,7 +63,7 @@ def render_equation(script: str) -> EquationRender:
|
|
|
63
63
|
return _fallback_block("script", LABEL_SCRIPT, script, latex=None)
|
|
64
64
|
|
|
65
65
|
try:
|
|
66
|
-
mathml =
|
|
66
|
+
mathml = eqedit_latex_to_mathml(latex)
|
|
67
67
|
except MathMLUnavailableError:
|
|
68
68
|
return _fallback_block("latex", LABEL_LATEX_NO_LIB, latex, latex=latex)
|
|
69
69
|
except ValueError:
|
|
@@ -116,6 +116,11 @@ OPERATORS: dict[str, str] = {
|
|
|
116
116
|
"partial": r"\partial",
|
|
117
117
|
"nabla": r"\nabla",
|
|
118
118
|
"angle": r"\angle",
|
|
119
|
+
# Hancom's symbol guide spells this token in uppercase, while legacy
|
|
120
|
+
# documents also contain the exact lowercase form. Mixed-case variants are
|
|
121
|
+
# intentionally not accepted because the reader uses exact token lookup.
|
|
122
|
+
"triangle": r"\triangle",
|
|
123
|
+
"TRIANGLE": r"\triangle",
|
|
119
124
|
"cdots": r"\cdots",
|
|
120
125
|
"ldots": r"\ldots",
|
|
121
126
|
"vdots": r"\vdots",
|
|
@@ -254,11 +259,17 @@ DELIMITERS: dict[str, str] = {
|
|
|
254
259
|
# Structural keywords handled directly by the parser (not simple substitution).
|
|
255
260
|
STRUCTURAL = frozenset({"over", "atop", "sqrt", "root", "of", "LEFT", "RIGHT", "left", "right"})
|
|
256
261
|
|
|
262
|
+
# LaTeX commands emitted from EqEdit operator tokens that latex2mathml otherwise
|
|
263
|
+
# classifies as identifiers. The render boundary uses this metadata only while
|
|
264
|
+
# producing MathML; the public LaTeX remains the canonical command above.
|
|
265
|
+
EQEDIT_MATHML_OPERATOR_COMMANDS = frozenset({OPERATORS["triangle"]})
|
|
266
|
+
|
|
257
267
|
|
|
258
268
|
__all__ = [
|
|
259
269
|
"ACCENTS",
|
|
260
270
|
"BIG_OPERATORS",
|
|
261
271
|
"DELIMITERS",
|
|
272
|
+
"EQEDIT_MATHML_OPERATOR_COMMANDS",
|
|
262
273
|
"FUNCTIONS",
|
|
263
274
|
"GREEK",
|
|
264
275
|
"MATRIX_ENVIRONMENTS",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Small helpers for a style-preserving paragraph text edit."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
|
|
8
|
+
from ..errors import HwpxValueError
|
|
9
|
+
from ._document_primitives import _is_tab_control_element
|
|
10
|
+
from .namespaces import tag_local_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plain_text_nodes_for_edit(runs: list[ET.Element]) -> list[ET.Element]:
|
|
14
|
+
nodes: list[ET.Element] = []
|
|
15
|
+
for run in runs:
|
|
16
|
+
for child in run:
|
|
17
|
+
if tag_local_name(child.tag) == "t":
|
|
18
|
+
if len(child):
|
|
19
|
+
raise HwpxValueError(
|
|
20
|
+
"mixed text markup cannot be edited safely",
|
|
21
|
+
code="paragraph-mixed-text-unsupported",
|
|
22
|
+
)
|
|
23
|
+
nodes.append(child)
|
|
24
|
+
elif tag_local_name(child.tag) == "tab" or _is_tab_control_element(child):
|
|
25
|
+
raise HwpxValueError(
|
|
26
|
+
"tab controls require an explicit run target",
|
|
27
|
+
code="paragraph-tab-target-required",
|
|
28
|
+
)
|
|
29
|
+
return nodes
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def edit_node_candidates(
|
|
33
|
+
nodes: list[ET.Element], prefix: int, end: int
|
|
34
|
+
) -> list[tuple[ET.Element, int]]:
|
|
35
|
+
offset = 0
|
|
36
|
+
candidates: list[tuple[ET.Element, int]] = []
|
|
37
|
+
for node in nodes:
|
|
38
|
+
length = len(node.text or "")
|
|
39
|
+
if offset <= prefix and end <= offset + length:
|
|
40
|
+
candidates.append((node, offset))
|
|
41
|
+
offset += length
|
|
42
|
+
return candidates
|
|
@@ -40,6 +40,7 @@ from .note_authoring import (
|
|
|
40
40
|
_paragraph_endnotes,
|
|
41
41
|
_paragraph_footnotes,
|
|
42
42
|
)
|
|
43
|
+
from ._paragraph_text_edit import edit_node_candidates, plain_text_nodes_for_edit
|
|
43
44
|
from .objects import (
|
|
44
45
|
HwpxOxmlInlineObject,
|
|
45
46
|
_create_picture_element,
|
|
@@ -336,6 +337,42 @@ class HwpxOxmlParagraph:
|
|
|
336
337
|
_clear_paragraph_layout_cache(self.element)
|
|
337
338
|
self.section.mark_dirty()
|
|
338
339
|
|
|
340
|
+
def set_text_preserving_runs(self, value: str) -> None:
|
|
341
|
+
"""Replace one unambiguous text span without changing run boundaries.
|
|
342
|
+
|
|
343
|
+
The edit must fit inside one plain ``hp:t`` node. This covers a
|
|
344
|
+
correction within a styled run while retaining every other run and
|
|
345
|
+
control; cross-style replacements require an explicit run target.
|
|
346
|
+
"""
|
|
347
|
+
from ..errors import HwpxValueError
|
|
348
|
+
|
|
349
|
+
nodes = plain_text_nodes_for_edit(self._run_elements())
|
|
350
|
+
old = "".join(node.text or "" for node in nodes)
|
|
351
|
+
if old == value:
|
|
352
|
+
return
|
|
353
|
+
if not nodes:
|
|
354
|
+
raise HwpxValueError("paragraph has no plain text node", code="paragraph-text-missing")
|
|
355
|
+
prefix = 0
|
|
356
|
+
while prefix < min(len(old), len(value)) and old[prefix] == value[prefix]:
|
|
357
|
+
prefix += 1
|
|
358
|
+
suffix = 0
|
|
359
|
+
while (suffix < len(old) - prefix and suffix < len(value) - prefix
|
|
360
|
+
and old[len(old) - suffix - 1] == value[len(value) - suffix - 1]):
|
|
361
|
+
suffix += 1
|
|
362
|
+
end = len(old) - suffix
|
|
363
|
+
replacement = value[prefix:len(value) - suffix if suffix else len(value)]
|
|
364
|
+
candidates = edit_node_candidates(nodes, prefix, end)
|
|
365
|
+
if len(candidates) != 1:
|
|
366
|
+
raise HwpxValueError(
|
|
367
|
+
"text edit crosses run boundaries or has ambiguous insertion style",
|
|
368
|
+
code="paragraph-text-style-ambiguous",
|
|
369
|
+
)
|
|
370
|
+
node, offset = candidates[0]
|
|
371
|
+
current = node.text or ""
|
|
372
|
+
node.text = _sanitize_text(current[:prefix - offset] + replacement + current[end - offset:])
|
|
373
|
+
_clear_paragraph_layout_cache(self.element)
|
|
374
|
+
self.section.mark_dirty()
|
|
375
|
+
|
|
339
376
|
def clear_text(self) -> None:
|
|
340
377
|
"""Remove all text content while preserving styles and non-text elements.
|
|
341
378
|
|
|
@@ -464,6 +464,30 @@ class HwpxOxmlTable:
|
|
|
464
464
|
")"
|
|
465
465
|
)
|
|
466
466
|
|
|
467
|
+
@property
|
|
468
|
+
def height(self) -> int:
|
|
469
|
+
"""Declared table height in HWP units."""
|
|
470
|
+
size = self.element.find(f"{_HP}sz")
|
|
471
|
+
return int(size.get("height", "0")) if size is not None else 0
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def treat_as_char(self) -> bool:
|
|
475
|
+
"""Whether Hancom lays out this table as one inline character."""
|
|
476
|
+
position = self.element.find(f"{_HP}pos")
|
|
477
|
+
return position is not None and position.get("treatAsChar", "1") == "1"
|
|
478
|
+
|
|
479
|
+
def set_treat_as_char(self, value: bool) -> None:
|
|
480
|
+
"""Choose inline layout or flowing table layout across pages."""
|
|
481
|
+
position = self.element.find(f"{_HP}pos")
|
|
482
|
+
if position is None:
|
|
483
|
+
from ..errors import HwpxValueError
|
|
484
|
+
|
|
485
|
+
raise HwpxValueError("table has no hp:pos", code="table-position-missing")
|
|
486
|
+
desired = "1" if value else "0"
|
|
487
|
+
if position.get("treatAsChar") != desired:
|
|
488
|
+
position.set("treatAsChar", desired)
|
|
489
|
+
self.mark_dirty()
|
|
490
|
+
|
|
467
491
|
# --- caption (hp:caption) -----------------------------------------
|
|
468
492
|
|
|
469
493
|
@property
|
|
@@ -78,6 +78,7 @@ src/hwpx/opc/xml_utils.py
|
|
|
78
78
|
src/hwpx/oxml/__init__.py
|
|
79
79
|
src/hwpx/oxml/_document_impl.py
|
|
80
80
|
src/hwpx/oxml/_document_primitives.py
|
|
81
|
+
src/hwpx/oxml/_paragraph_text_edit.py
|
|
81
82
|
src/hwpx/oxml/body.py
|
|
82
83
|
src/hwpx/oxml/canonical_defaults.py
|
|
83
84
|
src/hwpx/oxml/color.py
|
|
@@ -306,6 +307,7 @@ tests/test_table_cell_equalize.py
|
|
|
306
307
|
tests/test_table_cleanup.py
|
|
307
308
|
tests/test_table_label.py
|
|
308
309
|
tests/test_table_navigation.py
|
|
310
|
+
tests/test_table_page_flow.py
|
|
309
311
|
tests/test_table_patch.py
|
|
310
312
|
tests/test_table_patch_dryrun.py
|
|
311
313
|
tests/test_table_patch_m105.py
|
|
@@ -222,6 +222,7 @@ class TestLatexToEqedit:
|
|
|
222
222
|
(r"$x + 1$", "x + 1"),
|
|
223
223
|
(r"\le \ge \ne", "leq geq neq"),
|
|
224
224
|
(r"a \times b \cdot c \div d", "a times b cdot c div d"),
|
|
225
|
+
(r"\triangle P_1 P_2 Q", "TRIANGLE P _{1} P _{2} Q"),
|
|
225
226
|
(r"\lim_{x \to 0} \frac{1}{x}", "lim _{x -> 0} {1} over {x}"),
|
|
226
227
|
(r"a \to b", "a -> b"),
|
|
227
228
|
(r"x \rightarrow y , u \leftarrow v", "x -> y , u leftarrow v"),
|
|
@@ -274,6 +275,7 @@ class TestRoundtripStability:
|
|
|
274
275
|
r"\begin{vmatrix} a & b \\ c & d \end{vmatrix}",
|
|
275
276
|
r"\log_{2} x + \ln y",
|
|
276
277
|
r"a \times b \pm c \mp d",
|
|
278
|
+
r"\triangle P_1 P_2 Q",
|
|
277
279
|
r"\infty + \partial + \nabla",
|
|
278
280
|
]
|
|
279
281
|
|
|
@@ -112,6 +112,31 @@ def test_times_and_relations() -> None:
|
|
|
112
112
|
assert eqedit_to_latex("a >= b") == r"a \geq b"
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
@pytest.mark.parametrize("token", ["triangle", "TRIANGLE"])
|
|
116
|
+
def test_triangle_symbol_uses_exact_evidence_backed_tokens(token: str) -> None:
|
|
117
|
+
assert eqedit_to_latex(f"{token} P_1 P_2 Q") == r"\triangle P_1 P_2 Q"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize(
|
|
121
|
+
"script",
|
|
122
|
+
[
|
|
123
|
+
"Triangle PQR",
|
|
124
|
+
"triAngle PQR",
|
|
125
|
+
"triangles PQR",
|
|
126
|
+
"mytriangle PQR",
|
|
127
|
+
"triangleidentifier PQR",
|
|
128
|
+
"rmtriangle PQR",
|
|
129
|
+
"rm text",
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
def test_triangle_support_does_not_split_identifiers_or_rm_text(script: str) -> None:
|
|
133
|
+
assert eqedit_to_latex(script) == script
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_existing_angle_symbol_remains_distinct_from_triangle() -> None:
|
|
137
|
+
assert eqedit_to_latex("angle ABC") == r"\angle ABC"
|
|
138
|
+
|
|
139
|
+
|
|
115
140
|
def test_parentheses_and_braces_passthrough() -> None:
|
|
116
141
|
assert eqedit_to_latex("( a + b )") == "( a + b )"
|
|
117
142
|
assert eqedit_to_latex("LEFT ( x RIGHT )") == r"\left( x \right)"
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
from xml.etree import ElementTree
|
|
7
|
+
|
|
6
8
|
import pytest
|
|
7
9
|
|
|
10
|
+
from hwpx.equation.mathml import _split_text_literals
|
|
8
11
|
from hwpx.equation import (
|
|
9
12
|
LABEL_LATEX_ERROR,
|
|
10
13
|
LABEL_LATEX_NO_LIB,
|
|
@@ -25,6 +28,37 @@ def test_success_path_renders_inline_mathml() -> None:
|
|
|
25
28
|
assert result.latex == r"\frac{\alpha}{\beta} + \pi"
|
|
26
29
|
|
|
27
30
|
|
|
31
|
+
@pytest.mark.parametrize("token", ["triangle", "TRIANGLE"])
|
|
32
|
+
def test_triangle_renders_as_mathml_operator_without_changing_latex(token: str) -> None:
|
|
33
|
+
pytest.importorskip("latex2mathml")
|
|
34
|
+
result = render_equation(f"{token} P_1 P_2 Q")
|
|
35
|
+
assert result.mode == "mathml"
|
|
36
|
+
assert result.latex == r"\triangle P_1 P_2 Q"
|
|
37
|
+
root = ElementTree.fromstring(result.html)
|
|
38
|
+
namespace = "{http://www.w3.org/1998/Math/MathML}"
|
|
39
|
+
triangle_operators = [
|
|
40
|
+
operator
|
|
41
|
+
for operator in root.iter(f"{namespace}mo")
|
|
42
|
+
if "△" in "".join(operator.itertext())
|
|
43
|
+
]
|
|
44
|
+
assert len(triangle_operators) == 1
|
|
45
|
+
assert "triangle" not in "".join(root.itertext()).lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_quoted_triangle_literal_stays_text_and_is_not_annotated() -> None:
|
|
49
|
+
pytest.importorskip("latex2mathml")
|
|
50
|
+
# A quoted EqEdit literal becomes \text{...}; its content is prose and must
|
|
51
|
+
# not receive the operator wrapper (review on #93).
|
|
52
|
+
result = render_equation(r'"\triangle" + x')
|
|
53
|
+
assert result.mode == "mathml"
|
|
54
|
+
root = ElementTree.fromstring(result.html)
|
|
55
|
+
namespace = "{http://www.w3.org/1998/Math/MathML}"
|
|
56
|
+
texts = ["".join(node.itertext()) for node in root.iter(f"{namespace}mtext")]
|
|
57
|
+
assert texts == [r"\triangle"]
|
|
58
|
+
assert "mathop" not in result.html
|
|
59
|
+
assert "△" not in result.html
|
|
60
|
+
|
|
61
|
+
|
|
28
62
|
def test_eqedit_failure_falls_back_to_original_script_block() -> None:
|
|
29
63
|
# A brace/frac bomb exceeds the depth guard -> EqEdit->LaTeX fails.
|
|
30
64
|
bomb = "{" * 200 + "x" + "}" * 200
|
|
@@ -67,3 +101,16 @@ def test_no_mode_ever_returns_empty_html() -> None:
|
|
|
67
101
|
def test_latex2mathml_available_reflects_installation() -> None:
|
|
68
102
|
# In the test extra latex2mathml is installed, so this is True.
|
|
69
103
|
assert latex2mathml_available() is True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.parametrize(
|
|
107
|
+
("latex", "expected"),
|
|
108
|
+
[
|
|
109
|
+
(r"\triangle + x", [(r"\triangle + x", False)]),
|
|
110
|
+
(r"\text{\triangle} + x", [(r"\text{\triangle}", True), (" + x", False)]),
|
|
111
|
+
(r"a \text{b {c} d} \triangle", [("a ", False), (r"\text{b {c} d}", True), (r" \triangle", False)]),
|
|
112
|
+
(r"\text{open", [(r"\text{open", False)]),
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
def test_split_text_literals_keeps_quoted_content_opaque(latex: str, expected: list[tuple[str, bool]]) -> None:
|
|
116
|
+
assert _split_text_literals(latex) == expected
|
|
@@ -9,6 +9,7 @@ belong in this suite.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
from collections.abc import Callable
|
|
14
15
|
from io import BytesIO
|
|
@@ -19,7 +20,6 @@ import pytest
|
|
|
19
20
|
|
|
20
21
|
from hwpx import HwpxDocument
|
|
21
22
|
|
|
22
|
-
|
|
23
23
|
CONTRACT_PATH = Path(__file__).parent / "data" / "golden_api_contract.json"
|
|
24
24
|
Scenario = Callable[[Path], None]
|
|
25
25
|
|
|
@@ -278,9 +278,28 @@ def test_golden_api_contract_is_complete() -> None:
|
|
|
278
278
|
|
|
279
279
|
@pytest.mark.parametrize("scenario_id", CONTRACT["scenarios"])
|
|
280
280
|
def test_core_only_golden_scenario(scenario_id: str, tmp_path: Path) -> None:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
# A fresh interpreter tests core-only imports even when another collected
|
|
282
|
+
# test imported automation into the pytest process.
|
|
283
|
+
code = """
|
|
284
|
+
import importlib.util
|
|
285
|
+
import json
|
|
286
|
+
import sys
|
|
287
|
+
from pathlib import Path
|
|
288
|
+
spec = importlib.util.spec_from_file_location('golden_api_scenarios', sys.argv[1])
|
|
289
|
+
module = importlib.util.module_from_spec(spec)
|
|
290
|
+
sys.modules[spec.name] = module
|
|
291
|
+
spec.loader.exec_module(module)
|
|
292
|
+
module.SCENARIOS[sys.argv[2]](Path(sys.argv[3]))
|
|
293
|
+
assert not any(
|
|
294
|
+
name == prefix or name.startswith(prefix + '.')
|
|
295
|
+
for prefix in module.CONTRACT['forbiddenImportPrefixes']
|
|
296
|
+
for name in sys.modules
|
|
297
|
+
)
|
|
298
|
+
"""
|
|
299
|
+
result = subprocess.run(
|
|
300
|
+
[sys.executable, "-c", code, str(Path(__file__).resolve()), scenario_id, str(tmp_path)],
|
|
301
|
+
capture_output=True,
|
|
302
|
+
text=True,
|
|
303
|
+
check=False,
|
|
286
304
|
)
|
|
305
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
@@ -298,7 +298,8 @@ def test_real_tree_gate_runs_from_a_gitless_source_copy(tmp_path: Path) -> None:
|
|
|
298
298
|
# (oxml/note_authoring.py) — restores real headroom (1452/1600)
|
|
299
299
|
# instead of the 5 lines left after train 48/㊽b's PATH delegator.
|
|
300
300
|
# 전부 module-ownership.json 에 개별 rationale 과 함께 등재돼 있다.
|
|
301
|
-
|
|
301
|
+
# +1: format-level plain-text edit helpers extracted from paragraph.py.
|
|
302
|
+
assert report["classifiedFiles"] == 148
|
|
302
303
|
|
|
303
304
|
|
|
304
305
|
def test_gitless_cli_reproduces_literal_dynamic_import_failure_without_mutating_source(
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from hwpx.document import HwpxDocument
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_table_treat_as_char_is_explicit_and_persists() -> None:
|
|
7
|
+
with HwpxDocument.new() as document:
|
|
8
|
+
table = document.add_table(2, 1)
|
|
9
|
+
assert table.treat_as_char is True
|
|
10
|
+
original_height = table.height
|
|
11
|
+
table.set_treat_as_char(False)
|
|
12
|
+
assert table.treat_as_char is False
|
|
13
|
+
reopened = HwpxDocument.open(document.to_bytes())
|
|
14
|
+
try:
|
|
15
|
+
assert reopened.tables.all[0].treat_as_char is False
|
|
16
|
+
assert reopened.tables.all[0].height == original_height
|
|
17
|
+
finally:
|
|
18
|
+
reopened.close()
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
"""LaTeX → MathML conversion via the optional ``latex2mathml`` dependency.
|
|
3
|
-
|
|
4
|
-
MathML keeps the preview self-contained: browsers render ``<math>`` natively, so
|
|
5
|
-
no script or font bundle is shipped. ``latex2mathml`` is an optional extra
|
|
6
|
-
(``python-hwpx[preview]``); when it is absent the caller fails closed to a LaTeX
|
|
7
|
-
code block rather than dropping the equation.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from typing import Callable
|
|
13
|
-
|
|
14
|
-
# ``False`` marks a resolved-but-unavailable converter; ``None`` means "not yet
|
|
15
|
-
# probed" so the import is attempted lazily on first use.
|
|
16
|
-
_converter: Callable[[str], str] | bool | None = None
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class MathMLUnavailableError(RuntimeError):
|
|
20
|
-
"""Raised when ``latex2mathml`` is not installed."""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _load_converter() -> Callable[[str], str] | bool:
|
|
24
|
-
global _converter
|
|
25
|
-
cached = _converter
|
|
26
|
-
if cached is not None:
|
|
27
|
-
return cached
|
|
28
|
-
try:
|
|
29
|
-
from latex2mathml.converter import convert
|
|
30
|
-
except ImportError:
|
|
31
|
-
_converter = False
|
|
32
|
-
return False
|
|
33
|
-
_converter = convert
|
|
34
|
-
return convert
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def latex2mathml_available() -> bool:
|
|
38
|
-
"""Return ``True`` when the optional ``latex2mathml`` extra is importable."""
|
|
39
|
-
|
|
40
|
-
return _load_converter() is not False
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def latex_to_mathml(latex: str) -> str:
|
|
44
|
-
"""Convert a LaTeX fragment to an inline ``<math>`` MathML string.
|
|
45
|
-
|
|
46
|
-
Raises:
|
|
47
|
-
MathMLUnavailableError: when ``latex2mathml`` is not installed.
|
|
48
|
-
ValueError: when ``latex2mathml`` cannot parse the fragment.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
convert = _load_converter()
|
|
52
|
-
if convert is False:
|
|
53
|
-
raise MathMLUnavailableError(
|
|
54
|
-
"latex2mathml is required for MathML rendering; install python-hwpx[preview]"
|
|
55
|
-
)
|
|
56
|
-
assert not isinstance(convert, bool) # narrowed for type-checkers
|
|
57
|
-
try:
|
|
58
|
-
return convert(latex)
|
|
59
|
-
except MathMLUnavailableError:
|
|
60
|
-
raise
|
|
61
|
-
except Exception as exc: # latex2mathml raises bare Exception on bad input
|
|
62
|
-
raise ValueError(f"latex2mathml could not render fragment: {exc}") from exc
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
__all__ = [
|
|
66
|
-
"MathMLUnavailableError",
|
|
67
|
-
"latex2mathml_available",
|
|
68
|
-
"latex_to_mathml",
|
|
69
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|