docvortex 0.2.1__py3-none-any.whl
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.
- docvortex/__init__.py +21 -0
- docvortex/analyzers/__init__.py +3 -0
- docvortex/analyzers/native/__init__.py +37 -0
- docvortex/analyzers/native/_shared/__init__.py +3 -0
- docvortex/analyzers/native/_shared/hyperlink.py +16 -0
- docvortex/analyzers/native/_shared/image.py +8 -0
- docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
- docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
- docvortex/analyzers/native/_shared/markup/formula.py +37 -0
- docvortex/analyzers/native/_shared/markup/projector.py +57 -0
- docvortex/analyzers/native/_shared/markup/styles.py +19 -0
- docvortex/analyzers/native/_shared/mathml.py +17 -0
- docvortex/analyzers/native/_shared/names.py +7 -0
- docvortex/analyzers/native/_shared/xycut.py +414 -0
- docvortex/analyzers/native/contracts.py +44 -0
- docvortex/analyzers/native/csv.py +351 -0
- docvortex/analyzers/native/epub/__init__.py +18 -0
- docvortex/analyzers/native/epub/constants.py +45 -0
- docvortex/analyzers/native/epub/converter.py +80 -0
- docvortex/analyzers/native/epub/errors.py +20 -0
- docvortex/analyzers/native/epub/metadata.py +31 -0
- docvortex/analyzers/native/epub/package.py +649 -0
- docvortex/analyzers/native/epub/xhtml.py +306 -0
- docvortex/analyzers/native/html/__init__.py +6 -0
- docvortex/analyzers/native/html/anchors.py +271 -0
- docvortex/analyzers/native/html/constants.py +25 -0
- docvortex/analyzers/native/html/contracts.py +7 -0
- docvortex/analyzers/native/html/converter.py +117 -0
- docvortex/analyzers/native/html/document.py +389 -0
- docvortex/analyzers/native/html/errors.py +12 -0
- docvortex/analyzers/native/html/resources.py +365 -0
- docvortex/analyzers/native/html/selector.py +421 -0
- docvortex/analyzers/native/models.py +209 -0
- docvortex/analyzers/native/ofd/__init__.py +14 -0
- docvortex/analyzers/native/ofd/constants.py +59 -0
- docvortex/analyzers/native/ofd/converter.py +31 -0
- docvortex/analyzers/native/ofd/errors.py +16 -0
- docvortex/analyzers/native/ofd/geometry.py +215 -0
- docvortex/analyzers/native/ofd/images.py +124 -0
- docvortex/analyzers/native/ofd/metadata.py +53 -0
- docvortex/analyzers/native/ofd/models.py +170 -0
- docvortex/analyzers/native/ofd/package.py +346 -0
- docvortex/analyzers/native/ofd/path.py +201 -0
- docvortex/analyzers/native/ofd/reading_order.py +293 -0
- docvortex/analyzers/native/ofd/resources.py +122 -0
- docvortex/analyzers/native/ofd/scene.py +347 -0
- docvortex/analyzers/native/ofd/table.py +240 -0
- docvortex/analyzers/native/ofd/text.py +521 -0
- docvortex/analyzers/native/office/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
- docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
- docvortex/analyzers/native/office/doc/fib.py +261 -0
- docvortex/analyzers/native/office/doc/fields.py +101 -0
- docvortex/analyzers/native/office/doc/formatting.py +171 -0
- docvortex/analyzers/native/office/doc/images.py +162 -0
- docvortex/analyzers/native/office/doc/lists.py +356 -0
- docvortex/analyzers/native/office/doc/models.py +164 -0
- docvortex/analyzers/native/office/doc/parser.py +851 -0
- docvortex/analyzers/native/office/doc/pieces.py +256 -0
- docvortex/analyzers/native/office/doc/records.py +40 -0
- docvortex/analyzers/native/office/doc/sprm.py +269 -0
- docvortex/analyzers/native/office/doc/styles.py +215 -0
- docvortex/analyzers/native/office/docx/__init__.py +3 -0
- docvortex/analyzers/native/office/docx/context.py +71 -0
- docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
- docvortex/analyzers/native/office/docx/equationxml.py +119 -0
- docvortex/analyzers/native/office/docx/fields.py +816 -0
- docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
- docvortex/analyzers/native/office/docx/main.py +50 -0
- docvortex/analyzers/native/office/docx/numbering.py +491 -0
- docvortex/analyzers/native/office/docx/office_xml.py +57 -0
- docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
- docvortex/analyzers/native/office/docx/resources.py +526 -0
- docvortex/analyzers/native/office/docx/styles.py +693 -0
- docvortex/analyzers/native/office/docx/tables.py +515 -0
- docvortex/analyzers/native/office/equation/__init__.py +3 -0
- docvortex/analyzers/native/office/equation/image.py +470 -0
- docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
- docvortex/analyzers/native/office/equation/mtef.py +885 -0
- docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
- docvortex/analyzers/native/office/equation/omml.py +561 -0
- docvortex/analyzers/native/office/equation/ooxml.py +62 -0
- docvortex/analyzers/native/office/errors.py +33 -0
- docvortex/analyzers/native/office/image.py +307 -0
- docvortex/analyzers/native/office/legacy/__init__.py +3 -0
- docvortex/analyzers/native/office/legacy/binary.py +43 -0
- docvortex/analyzers/native/office/legacy/officeart.py +362 -0
- docvortex/analyzers/native/office/legacy/ole.py +110 -0
- docvortex/analyzers/native/office/limits.py +12 -0
- docvortex/analyzers/native/office/odf/__init__.py +3 -0
- docvortex/analyzers/native/office/odf/chart.py +76 -0
- docvortex/analyzers/native/office/odf/constants.py +70 -0
- docvortex/analyzers/native/office/odf/converters.py +403 -0
- docvortex/analyzers/native/office/odf/errors.py +18 -0
- docvortex/analyzers/native/office/odf/metadata.py +91 -0
- docvortex/analyzers/native/office/odf/models.py +176 -0
- docvortex/analyzers/native/office/odf/package.py +270 -0
- docvortex/analyzers/native/office/odf/styles.py +329 -0
- docvortex/analyzers/native/office/odf/table.py +469 -0
- docvortex/analyzers/native/office/odf/text.py +1002 -0
- docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
- docvortex/analyzers/native/office/opc.py +38 -0
- docvortex/analyzers/native/office/ppt/__init__.py +3 -0
- docvortex/analyzers/native/office/ppt/models.py +118 -0
- docvortex/analyzers/native/office/ppt/parser.py +1895 -0
- docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
- docvortex/analyzers/native/office/ppt/records.py +131 -0
- docvortex/analyzers/native/office/ppt/style_text.py +247 -0
- docvortex/analyzers/native/office/pptx/__init__.py +3 -0
- docvortex/analyzers/native/office/pptx/context.py +113 -0
- docvortex/analyzers/native/office/pptx/lists.py +558 -0
- docvortex/analyzers/native/office/pptx/main.py +20 -0
- docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
- docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
- docvortex/analyzers/native/office/pptx/resources.py +329 -0
- docvortex/analyzers/native/office/pptx/shapes.py +393 -0
- docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
- docvortex/analyzers/native/office/pptx/titles.py +178 -0
- docvortex/analyzers/native/office/rich_text.py +420 -0
- docvortex/analyzers/native/office/rtf/__init__.py +3 -0
- docvortex/analyzers/native/office/rtf/converter.py +708 -0
- docvortex/analyzers/native/office/rtf/lexer.py +213 -0
- docvortex/analyzers/native/office/rtf/math.py +339 -0
- docvortex/analyzers/native/office/rtf/models.py +185 -0
- docvortex/analyzers/native/office/rtf/parser.py +1553 -0
- docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
- docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
- docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
- docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
- docvortex/analyzers/native/office/streams.py +18 -0
- docvortex/analyzers/native/office/xls/__init__.py +3 -0
- docvortex/analyzers/native/office/xls/chart.py +132 -0
- docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
- docvortex/analyzers/native/office/xls/models.py +109 -0
- docvortex/analyzers/native/office/xls/number_format.py +521 -0
- docvortex/analyzers/native/office/xls/parser.py +1145 -0
- docvortex/analyzers/native/office/xls/records.py +201 -0
- docvortex/analyzers/native/office/xls/strings.py +205 -0
- docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
- docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
- docvortex/analyzers/native/office/xlsx/main.py +20 -0
- docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
- docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
- docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
- docvortex/analyzers/native/pdf/__init__.py +3 -0
- docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
- docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
- docvortex/analyzers/native/pdf/code_blocks.py +535 -0
- docvortex/analyzers/native/pdf/formulas.py +1985 -0
- docvortex/analyzers/native/pdf/geometry.py +281 -0
- docvortex/analyzers/native/pdf/graphics.py +1501 -0
- docvortex/analyzers/native/pdf/index_blocks.py +268 -0
- docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
- docvortex/analyzers/native/pdf/inline/common.py +112 -0
- docvortex/analyzers/native/pdf/inline/detection.py +590 -0
- docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
- docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
- docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
- docvortex/analyzers/native/pdf/inline/types.py +385 -0
- docvortex/analyzers/native/pdf/line_layout.py +1106 -0
- docvortex/analyzers/native/pdf/line_merging.py +1223 -0
- docvortex/analyzers/native/pdf/models.py +246 -0
- docvortex/analyzers/native/pdf/native_text.py +1004 -0
- docvortex/analyzers/native/pdf/pipeline.py +1390 -0
- docvortex/analyzers/native/pdf/script_geometry.py +636 -0
- docvortex/analyzers/native/pdf/shared.py +30 -0
- docvortex/analyzers/native/pdf/spatial_text.py +383 -0
- docvortex/analyzers/native/pdf/table_annotations.py +446 -0
- docvortex/analyzers/native/pdf/table_constants.py +48 -0
- docvortex/analyzers/native/pdf/table_detection.py +227 -0
- docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
- docvortex/analyzers/native/pdf/table_geometry.py +40 -0
- docvortex/analyzers/native/pdf/table_materialization.py +444 -0
- docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
- docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
- docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
- docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
- docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
- docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
- docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
- docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
- docvortex/analyzers/native/pdf/table_rows.py +34 -0
- docvortex/analyzers/native/pdf/table_rules.py +1129 -0
- docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
- docvortex/analyzers/native/pdf/tables.py +147 -0
- docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
- docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
- docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
- docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
- docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
- docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
- docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
- docvortex/analyzers/native/pdf/text_blocks.py +82 -0
- docvortex/analyzers/native/pdf/text_styles.py +55 -0
- docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
- docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
- docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
- docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
- docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
- docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
- docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
- docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
- docvortex/analyzers/native/pdf/titles.py +75 -0
- docvortex/analyzers/native/pdf/typography.py +19 -0
- docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
- docvortex/api.py +180 -0
- docvortex/assets/__init__.py +5 -0
- docvortex/assets/store.py +51 -0
- docvortex/cli.py +54 -0
- docvortex/codecs/__init__.py +3 -0
- docvortex/codecs/html/__init__.py +22 -0
- docvortex/codecs/html/contracts.py +236 -0
- docvortex/codecs/html/materializer.py +331 -0
- docvortex/codecs/html/parser.py +763 -0
- docvortex/codecs/html/resources.py +34 -0
- docvortex/codecs/json.py +17 -0
- docvortex/content/__init__.py +5 -0
- docvortex/content/inline.py +248 -0
- docvortex/content/markup/__init__.py +44 -0
- docvortex/content/markup/anchors.py +188 -0
- docvortex/content/markup/formula.py +280 -0
- docvortex/content/markup/projector.py +1237 -0
- docvortex/content/markup/styles.py +327 -0
- docvortex/content/mathml.py +167 -0
- docvortex/content/normalization.py +188 -0
- docvortex/content/spans.py +183 -0
- docvortex/content/table/__init__.py +18 -0
- docvortex/content/table/blocks.py +152 -0
- docvortex/content/table/content.py +425 -0
- docvortex/content/table/document.py +104 -0
- docvortex/content/table/html.py +399 -0
- docvortex/content/table/models.py +76 -0
- docvortex/content/table/rules.py +42 -0
- docvortex/content/table/structure.py +221 -0
- docvortex/content/tree.py +5 -0
- docvortex/document/__init__.py +3 -0
- docvortex/document/contracts.py +22 -0
- docvortex/document/detection.py +389 -0
- docvortex/document/filetypes.py +175 -0
- docvortex/document/page_range.py +167 -0
- docvortex/document/pdf/__init__.py +19 -0
- docvortex/document/pdf/classify.py +1138 -0
- docvortex/document/pdf/constants.py +132 -0
- docvortex/document/pdf/diagnostics.py +350 -0
- docvortex/document/pdf/document.py +582 -0
- docvortex/document/pdf/font_runtime.py +335 -0
- docvortex/document/pdf/geometry.py +31 -0
- docvortex/document/pdf/images.py +654 -0
- docvortex/document/pdf/native_annotations.py +367 -0
- docvortex/document/pdf/native_contracts.py +169 -0
- docvortex/document/pdf/native_coordinates.py +216 -0
- docvortex/document/pdf/native_lifecycle.py +16 -0
- docvortex/document/pdf/native_objects.py +902 -0
- docvortex/document/pdf/native_text_geometry.py +315 -0
- docvortex/document/pdf/pdfium.py +325 -0
- docvortex/document/pdf/raster.py +46 -0
- docvortex/document/pdf/text/__init__.py +62 -0
- docvortex/document/pdf/text/contracts.py +211 -0
- docvortex/document/pdf/text/extract.py +165 -0
- docvortex/document/pdf/text/geometry.py +16 -0
- docvortex/document/pdf/text/groups.py +162 -0
- docvortex/document/pdf/visual_geometry.py +201 -0
- docvortex/document/pdf/visuals.py +343 -0
- docvortex/document/source.py +92 -0
- docvortex/errors.py +21 -0
- docvortex/export/__init__.py +3 -0
- docvortex/export/bundle.py +98 -0
- docvortex/export/files.py +66 -0
- docvortex/export/middle.py +208 -0
- docvortex/foundation/__init__.py +3 -0
- docvortex/foundation/geometry.py +125 -0
- docvortex/foundation/hyperlink.py +65 -0
- docvortex/foundation/image.py +48 -0
- docvortex/foundation/image_encoding.py +30 -0
- docvortex/foundation/image_payload.py +280 -0
- docvortex/foundation/language.py +92 -0
- docvortex/foundation/platform.py +38 -0
- docvortex/foundation/text.py +153 -0
- docvortex/foundation/type_identity.py +20 -0
- docvortex/foundation/xml_names.py +20 -0
- docvortex/options.py +30 -0
- docvortex/postprocess/__init__.py +3 -0
- docvortex/postprocess/content.py +53 -0
- docvortex/postprocess/document.py +19 -0
- docvortex/postprocess/lists.py +236 -0
- docvortex/postprocess/page_blocks.py +214 -0
- docvortex/postprocess/pages.py +95 -0
- docvortex/postprocess/paragraphs.py +580 -0
- docvortex/postprocess/visual.py +715 -0
- docvortex/render/__init__.py +48 -0
- docvortex/render/_internal/__init__.py +3 -0
- docvortex/render/_internal/common/__init__.py +3 -0
- docvortex/render/_internal/common/context.py +43 -0
- docvortex/render/_internal/common/html_table.py +178 -0
- docvortex/render/_internal/common/index.py +33 -0
- docvortex/render/_internal/common/list_items.py +158 -0
- docvortex/render/_internal/common/planner.py +140 -0
- docvortex/render/_internal/docx/__init__.py +3 -0
- docvortex/render/_internal/docx/assets.py +202 -0
- docvortex/render/_internal/docx/inline.py +434 -0
- docvortex/render/_internal/docx/math.py +220 -0
- docvortex/render/_internal/docx/renderer.py +905 -0
- docvortex/render/_internal/docx/styles.py +195 -0
- docvortex/render/_internal/docx/table.py +442 -0
- docvortex/render/_internal/epub/__init__.py +5 -0
- docvortex/render/_internal/epub/assets.py +173 -0
- docvortex/render/_internal/epub/package.py +249 -0
- docvortex/render/_internal/epub/renderer.py +1156 -0
- docvortex/render/_internal/html/__init__.py +3 -0
- docvortex/render/_internal/html/inline.py +346 -0
- docvortex/render/_internal/html/renderer.py +1041 -0
- docvortex/render/_internal/html/sanitizer.py +478 -0
- docvortex/render/_internal/html/table.py +121 -0
- docvortex/render/_internal/latex/__init__.py +1 -0
- docvortex/render/_internal/latex/assets.py +85 -0
- docvortex/render/_internal/latex/inline.py +145 -0
- docvortex/render/_internal/latex/renderer.py +506 -0
- docvortex/render/_internal/latex/table.py +347 -0
- docvortex/render/_internal/markdown/__init__.py +3 -0
- docvortex/render/_internal/markdown/assets.py +78 -0
- docvortex/render/_internal/markdown/blocks.py +635 -0
- docvortex/render/_internal/markdown/escaping.py +51 -0
- docvortex/render/_internal/markdown/inline.py +260 -0
- docvortex/render/_internal/markdown/renderer.py +93 -0
- docvortex/render/_internal/markdown/table.py +281 -0
- docvortex/render/_internal/pdf/__init__.py +3 -0
- docvortex/render/_internal/pdf/assets.py +197 -0
- docvortex/render/_internal/pdf/formula.py +417 -0
- docvortex/render/_internal/pdf/inline.py +343 -0
- docvortex/render/_internal/pdf/renderer.py +734 -0
- docvortex/render/_internal/pdf/styles.py +206 -0
- docvortex/render/_internal/pdf/table.py +272 -0
- docvortex/render/_internal/structured_content/__init__.py +3 -0
- docvortex/render/_internal/structured_content/renderer.py +193 -0
- docvortex/render/api.py +199 -0
- docvortex/render/contracts.py +205 -0
- docvortex/render/docx.py +38 -0
- docvortex/render/epub.py +35 -0
- docvortex/render/fragments.py +70 -0
- docvortex/render/html.py +29 -0
- docvortex/render/latex.py +24 -0
- docvortex/render/markdown.py +50 -0
- docvortex/render/pdf.py +25 -0
- docvortex/render/structured_content.py +23 -0
- docvortex/resources/epub/docvortex.css +91 -0
- docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
- docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
- docvortex/resources/fonts/NOTICE +190 -0
- docvortex/resources/fonts/manifest.json +9 -0
- docvortex/resources/html/docvortex.css +601 -0
- docvortex/resources/html/docvortex.min.css +1 -0
- docvortex/result.py +91 -0
- docvortex/schema.py +1141 -0
- docvortex/version.py +3 -0
- docvortex-0.2.1.dist-info/METADATA +193 -0
- docvortex-0.2.1.dist-info/RECORD +364 -0
- docvortex-0.2.1.dist-info/WHEEL +5 -0
- docvortex-0.2.1.dist-info/entry_points.txt +2 -0
- docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
- docvortex-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""解析 XHTML/HTML 使用的有限语义 CSS 子集。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
from docvortex.foundation.xml_names import local_name
|
|
11
|
+
from docvortex.foundation.type_identity import preserve_type_module
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_CSS_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
|
|
15
|
+
_CSS_IMPORTANT_RE = re.compile(r"!\s*important\s*$", re.IGNORECASE)
|
|
16
|
+
_TEXT_STYLE_FIELDS = ("bold", "italic", "underline", "strikethrough", "superscript", "subscript")
|
|
17
|
+
_VISIBILITY_FIELDS = ("display", "visibility", "opacity")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class TextStyle:
|
|
22
|
+
"""保存可投影到 Middle JSON 行内协议的文字样式。"""
|
|
23
|
+
|
|
24
|
+
bold: bool = False
|
|
25
|
+
italic: bool = False
|
|
26
|
+
underline: bool = False
|
|
27
|
+
strikethrough: bool = False
|
|
28
|
+
superscript: bool = False
|
|
29
|
+
subscript: bool = False
|
|
30
|
+
|
|
31
|
+
def merge(self, other: TextStyle) -> TextStyle:
|
|
32
|
+
"""合并继承样式和当前元素显式开启的样式。"""
|
|
33
|
+
return TextStyle(
|
|
34
|
+
bold=self.bold or other.bold,
|
|
35
|
+
italic=self.italic or other.italic,
|
|
36
|
+
underline=self.underline or other.underline,
|
|
37
|
+
strikethrough=self.strikethrough or other.strikethrough,
|
|
38
|
+
superscript=self.superscript or other.superscript,
|
|
39
|
+
subscript=self.subscript or other.subscript,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def names(self) -> tuple[str, ...]:
|
|
43
|
+
"""按稳定顺序返回现有行内协议识别的样式名称。"""
|
|
44
|
+
return tuple(
|
|
45
|
+
name
|
|
46
|
+
for enabled, name in (
|
|
47
|
+
(self.bold, "bold"),
|
|
48
|
+
(self.italic, "italic"),
|
|
49
|
+
(self.underline, "underline"),
|
|
50
|
+
(self.strikethrough, "strikethrough"),
|
|
51
|
+
(self.superscript, "superscript"),
|
|
52
|
+
(self.subscript, "subscript"),
|
|
53
|
+
)
|
|
54
|
+
if enabled
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class TextStyleDelta:
|
|
60
|
+
"""保存 CSS 对各文字样式的显式开启、关闭或未声明状态。"""
|
|
61
|
+
|
|
62
|
+
bold: bool | None = None
|
|
63
|
+
italic: bool | None = None
|
|
64
|
+
underline: bool | None = None
|
|
65
|
+
strikethrough: bool | None = None
|
|
66
|
+
superscript: bool | None = None
|
|
67
|
+
subscript: bool | None = None
|
|
68
|
+
|
|
69
|
+
def apply(self, base: TextStyle) -> TextStyle:
|
|
70
|
+
"""把当前声明覆盖到已解析的继承/标签样式。"""
|
|
71
|
+
return TextStyle(
|
|
72
|
+
bold=base.bold if self.bold is None else self.bold,
|
|
73
|
+
italic=base.italic if self.italic is None else self.italic,
|
|
74
|
+
underline=base.underline if self.underline is None else self.underline,
|
|
75
|
+
strikethrough=base.strikethrough if self.strikethrough is None else self.strikethrough,
|
|
76
|
+
superscript=base.superscript if self.superscript is None else self.superscript,
|
|
77
|
+
subscript=base.subscript if self.subscript is None else self.subscript,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def is_empty(self) -> bool:
|
|
81
|
+
"""返回当前声明是否没有触及任何受支持样式。"""
|
|
82
|
+
return all(
|
|
83
|
+
value is None
|
|
84
|
+
for value in (
|
|
85
|
+
self.bold,
|
|
86
|
+
self.italic,
|
|
87
|
+
self.underline,
|
|
88
|
+
self.strikethrough,
|
|
89
|
+
self.superscript,
|
|
90
|
+
self.subscript,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class ElementStyle:
|
|
97
|
+
"""保存元素最终文字样式、整树隐藏状态和继承可见性。"""
|
|
98
|
+
|
|
99
|
+
text: TextStyle
|
|
100
|
+
subtree_hidden: bool = False
|
|
101
|
+
visibility_hidden: bool = False
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def hidden(self) -> bool:
|
|
105
|
+
"""返回当前元素是否因任一种受支持的隐藏语义而不可见。"""
|
|
106
|
+
return self.subtree_hidden or self.visibility_hidden
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(slots=True)
|
|
110
|
+
class _SelectorCascade:
|
|
111
|
+
"""按 selector 聚合各属性最后一次声明及其源码顺序。"""
|
|
112
|
+
|
|
113
|
+
priority: int
|
|
114
|
+
declarations: dict[str, tuple[bool, int, bool]] = field(default_factory=dict)
|
|
115
|
+
visibility: dict[str, tuple[bool, int, bool]] = field(default_factory=dict)
|
|
116
|
+
|
|
117
|
+
def update(self, parsed: _ParsedDeclarations, order: int) -> None:
|
|
118
|
+
"""按 importance 和源码顺序更新同 selector 的逐属性级联结果。"""
|
|
119
|
+
for name, (important, value) in parsed.text.items():
|
|
120
|
+
current = self.declarations.get(name)
|
|
121
|
+
if current is None or (important, order) >= current[:2]:
|
|
122
|
+
self.declarations[name] = (important, order, value)
|
|
123
|
+
for name, (important, value) in parsed.visibility.items():
|
|
124
|
+
current = self.visibility.get(name)
|
|
125
|
+
if current is None or (important, order) >= current[:2]:
|
|
126
|
+
self.visibility[name] = (important, order, value)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True, slots=True)
|
|
130
|
+
class _ParsedDeclarations:
|
|
131
|
+
"""保存已投影 CSS 属性的 importance 与布尔值。"""
|
|
132
|
+
|
|
133
|
+
text: dict[str, tuple[bool, bool]]
|
|
134
|
+
visibility: dict[str, tuple[bool, bool]]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _numeric_font_weight(value: str) -> int | None:
|
|
138
|
+
"""在整数转换前解析 CSS Fonts 允许的一到一千字重。"""
|
|
139
|
+
if not value.isascii() or not value.isdigit() or len(value) > 4:
|
|
140
|
+
return None
|
|
141
|
+
weight = int(value)
|
|
142
|
+
return weight if 1 <= weight <= 1_000 else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _parse_declarations(value: str) -> _ParsedDeclarations:
|
|
146
|
+
"""从声明串逐属性提取字体语义、隐藏状态和 important 优先级。"""
|
|
147
|
+
text: dict[str, tuple[bool, bool]] = {}
|
|
148
|
+
visibility: dict[str, tuple[bool, bool]] = {}
|
|
149
|
+
for raw_declaration in value.split(";"):
|
|
150
|
+
if ":" not in raw_declaration:
|
|
151
|
+
continue
|
|
152
|
+
name, raw_value = raw_declaration.split(":", 1)
|
|
153
|
+
name = name.strip().casefold()
|
|
154
|
+
important_match = _CSS_IMPORTANT_RE.search(raw_value)
|
|
155
|
+
important = important_match is not None
|
|
156
|
+
normalized = raw_value[: important_match.start() if important_match is not None else None].strip().casefold()
|
|
157
|
+
text_updates: dict[str, bool] = {}
|
|
158
|
+
visibility_update: tuple[str, bool] | None = None
|
|
159
|
+
if name == "font-weight":
|
|
160
|
+
if normalized in {"bold", "bolder"}:
|
|
161
|
+
text_updates["bold"] = True
|
|
162
|
+
elif normalized in {"normal", "lighter"}:
|
|
163
|
+
text_updates["bold"] = False
|
|
164
|
+
elif (weight := _numeric_font_weight(normalized)) is not None:
|
|
165
|
+
text_updates["bold"] = weight >= 600
|
|
166
|
+
elif name == "font-style":
|
|
167
|
+
text_updates["italic"] = normalized in {"italic", "oblique"}
|
|
168
|
+
elif name in {"text-decoration", "text-decoration-line"}:
|
|
169
|
+
if normalized == "none":
|
|
170
|
+
text_updates["underline"] = False
|
|
171
|
+
text_updates["strikethrough"] = False
|
|
172
|
+
else:
|
|
173
|
+
text_updates["underline"] = "underline" in normalized
|
|
174
|
+
text_updates["strikethrough"] = "line-through" in normalized
|
|
175
|
+
elif name == "vertical-align":
|
|
176
|
+
text_updates["superscript"] = normalized in {"super", "text-top"}
|
|
177
|
+
text_updates["subscript"] = normalized in {"sub", "text-bottom"}
|
|
178
|
+
elif name == "display":
|
|
179
|
+
visibility_update = ("display", normalized == "none")
|
|
180
|
+
elif name == "visibility":
|
|
181
|
+
if normalized in {"hidden", "collapse"}:
|
|
182
|
+
visibility_update = ("visibility", True)
|
|
183
|
+
elif normalized in {"visible", "initial"}:
|
|
184
|
+
visibility_update = ("visibility", False)
|
|
185
|
+
elif name == "opacity":
|
|
186
|
+
try:
|
|
187
|
+
opacity = float(normalized)
|
|
188
|
+
except ValueError:
|
|
189
|
+
pass
|
|
190
|
+
else:
|
|
191
|
+
visibility_update = ("opacity", opacity <= 0)
|
|
192
|
+
for field_name, field_value in text_updates.items():
|
|
193
|
+
current = text.get(field_name)
|
|
194
|
+
if current is None or important or not current[0]:
|
|
195
|
+
text[field_name] = (important, field_value)
|
|
196
|
+
if visibility_update is not None:
|
|
197
|
+
field_name, field_value = visibility_update
|
|
198
|
+
current = visibility.get(field_name)
|
|
199
|
+
if current is None or important or not current[0]:
|
|
200
|
+
visibility[field_name] = (important, field_value)
|
|
201
|
+
return _ParsedDeclarations(text=text, visibility=visibility)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class MarkupStylesheet:
|
|
205
|
+
"""保存按文档顺序解析的简单 tag/class CSS 规则。"""
|
|
206
|
+
|
|
207
|
+
def __init__(self) -> None:
|
|
208
|
+
"""初始化按 tag、class 与 tag.class 分桶的 selector 索引。"""
|
|
209
|
+
self._tag_cascades: dict[str, _SelectorCascade] = {}
|
|
210
|
+
self._class_cascades: dict[str, _SelectorCascade] = {}
|
|
211
|
+
self._tag_class_cascades: dict[tuple[str, str], _SelectorCascade] = {}
|
|
212
|
+
self._source_order = 0
|
|
213
|
+
|
|
214
|
+
def _selector_cascade(self, tag: str | None, class_name: str | None, priority: int) -> _SelectorCascade:
|
|
215
|
+
"""返回指定简单 selector 的聚合级联槽。"""
|
|
216
|
+
if class_name is None:
|
|
217
|
+
assert tag is not None
|
|
218
|
+
return self._tag_cascades.setdefault(tag, _SelectorCascade(priority))
|
|
219
|
+
if tag is None:
|
|
220
|
+
return self._class_cascades.setdefault(class_name, _SelectorCascade(priority))
|
|
221
|
+
return self._tag_class_cascades.setdefault((tag, class_name), _SelectorCascade(priority))
|
|
222
|
+
|
|
223
|
+
def add(self, css: str) -> None:
|
|
224
|
+
"""追加一个 stylesheet 中受支持的简单 selector 规则。"""
|
|
225
|
+
normalized_css = _CSS_COMMENT_RE.sub("", css)
|
|
226
|
+
for chunk in normalized_css.split("}"):
|
|
227
|
+
if "{" not in chunk:
|
|
228
|
+
continue
|
|
229
|
+
selectors, declarations = chunk.split("{", 1)
|
|
230
|
+
parsed_declarations = _parse_declarations(declarations)
|
|
231
|
+
if not parsed_declarations.text and not parsed_declarations.visibility:
|
|
232
|
+
continue
|
|
233
|
+
for selector in selectors.split(","):
|
|
234
|
+
parsed = self._parse_selector(selector)
|
|
235
|
+
if parsed is None:
|
|
236
|
+
continue
|
|
237
|
+
tag, class_name, priority = parsed
|
|
238
|
+
cascade = self._selector_cascade(tag, class_name, priority)
|
|
239
|
+
cascade.update(parsed_declarations, self._source_order)
|
|
240
|
+
self._source_order += 1
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _parse_selector(selector: str) -> tuple[str | None, str | None, int] | None:
|
|
244
|
+
"""只接受 tag、.class 和 tag.class,拒绝组合器及伪类。"""
|
|
245
|
+
normalized = selector.strip()
|
|
246
|
+
if not normalized or any(token in normalized for token in (" ", ">", "+", "~", ":", "[", "#")):
|
|
247
|
+
return None
|
|
248
|
+
if "." in normalized:
|
|
249
|
+
tag_text, class_name = normalized.split(".", 1)
|
|
250
|
+
if not class_name or "." in class_name:
|
|
251
|
+
return None
|
|
252
|
+
tag = tag_text.casefold() or None
|
|
253
|
+
return tag, class_name, 10 + (1 if tag else 0)
|
|
254
|
+
return normalized.casefold(), None, 1
|
|
255
|
+
|
|
256
|
+
def resolve(
|
|
257
|
+
self,
|
|
258
|
+
element: etree._Element,
|
|
259
|
+
inherited: TextStyle,
|
|
260
|
+
inherited_visibility_hidden: bool = False,
|
|
261
|
+
) -> ElementStyle:
|
|
262
|
+
"""计算元素的继承样式、标签默认样式、CSS 规则和 inline style。"""
|
|
263
|
+
tag = local_name(element)
|
|
264
|
+
classes = frozenset((element.get("class") or "").split())
|
|
265
|
+
tag_style = TextStyle(
|
|
266
|
+
bold=tag in {"b", "strong"},
|
|
267
|
+
italic=tag in {"cite", "dfn", "em", "i", "var"},
|
|
268
|
+
strikethrough=tag in {"del", "s", "strike"},
|
|
269
|
+
superscript=tag == "sup",
|
|
270
|
+
subscript=tag == "sub",
|
|
271
|
+
)
|
|
272
|
+
style = inherited.merge(tag_style)
|
|
273
|
+
subtree_hidden = element.get("hidden") is not None or (element.get("aria-hidden") or "").casefold() == "true"
|
|
274
|
+
matching: list[_SelectorCascade] = []
|
|
275
|
+
if cascade := self._tag_cascades.get(tag):
|
|
276
|
+
matching.append(cascade)
|
|
277
|
+
for class_name in classes:
|
|
278
|
+
if cascade := self._class_cascades.get(class_name):
|
|
279
|
+
matching.append(cascade)
|
|
280
|
+
if cascade := self._tag_class_cascades.get((tag, class_name)):
|
|
281
|
+
matching.append(cascade)
|
|
282
|
+
|
|
283
|
+
inline = _parse_declarations(element.get("style") or "")
|
|
284
|
+
resolved_values: dict[str, bool] = {}
|
|
285
|
+
for name in _TEXT_STYLE_FIELDS:
|
|
286
|
+
candidates = [
|
|
287
|
+
(important, cascade.priority, order, value)
|
|
288
|
+
for cascade in matching
|
|
289
|
+
if (declaration := cascade.declarations.get(name)) is not None
|
|
290
|
+
for important, order, value in (declaration,)
|
|
291
|
+
]
|
|
292
|
+
if (inline_declaration := inline.text.get(name)) is not None:
|
|
293
|
+
important, value = inline_declaration
|
|
294
|
+
candidates.append((important, 1_000, self._source_order, value))
|
|
295
|
+
if candidates:
|
|
296
|
+
resolved_values[name] = max(candidates, key=lambda item: item[:3])[3]
|
|
297
|
+
style = TextStyleDelta(**resolved_values).apply(style)
|
|
298
|
+
|
|
299
|
+
resolved_visibility: dict[str, bool] = {}
|
|
300
|
+
for name in _VISIBILITY_FIELDS:
|
|
301
|
+
candidates = [
|
|
302
|
+
(important, cascade.priority, order, value)
|
|
303
|
+
for cascade in matching
|
|
304
|
+
if (declaration := cascade.visibility.get(name)) is not None
|
|
305
|
+
for important, order, value in (declaration,)
|
|
306
|
+
]
|
|
307
|
+
if (inline_declaration := inline.visibility.get(name)) is not None:
|
|
308
|
+
important, value = inline_declaration
|
|
309
|
+
candidates.append((important, 1_000, self._source_order, value))
|
|
310
|
+
if candidates:
|
|
311
|
+
resolved_visibility[name] = max(candidates, key=lambda item: item[:3])[3]
|
|
312
|
+
subtree_hidden = (
|
|
313
|
+
subtree_hidden or resolved_visibility.get("display", False) or resolved_visibility.get("opacity", False)
|
|
314
|
+
)
|
|
315
|
+
visibility_hidden = resolved_visibility.get("visibility", inherited_visibility_hidden)
|
|
316
|
+
return ElementStyle(style, subtree_hidden, visibility_hidden)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
__all__ = ["ElementStyle", "MarkupStylesheet", "TextStyle", "TextStyleDelta"]
|
|
320
|
+
|
|
321
|
+
# 保持既有公开类型的 pickle 路径,所有旧、新入口指向同一个类。
|
|
322
|
+
preserve_type_module(TextStyle, "docvortex.analyzers.native._shared.markup.styles")
|
|
323
|
+
preserve_type_module(TextStyleDelta, "docvortex.analyzers.native._shared.markup.styles")
|
|
324
|
+
preserve_type_module(ElementStyle, "docvortex.analyzers.native._shared.markup.styles")
|
|
325
|
+
preserve_type_module(_SelectorCascade, "docvortex.analyzers.native._shared.markup.styles")
|
|
326
|
+
preserve_type_module(_ParsedDeclarations, "docvortex.analyzers.native._shared.markup.styles")
|
|
327
|
+
preserve_type_module(MarkupStylesheet, "docvortex.analyzers.native._shared.markup.styles")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""把常用 Presentation MathML 结构转换为 LaTeX。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
8
|
+
|
|
9
|
+
from docvortex.foundation.xml_names import local_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_OPERATOR_MAP = {
|
|
13
|
+
"−": "-",
|
|
14
|
+
"×": r"\times ",
|
|
15
|
+
"÷": r"\div ",
|
|
16
|
+
"·": r"\cdot ",
|
|
17
|
+
"±": r"\pm ",
|
|
18
|
+
"∓": r"\mp ",
|
|
19
|
+
"∞": r"\infty ",
|
|
20
|
+
"≠": r"\ne ",
|
|
21
|
+
"≤": r"\le ",
|
|
22
|
+
"≥": r"\ge ",
|
|
23
|
+
"≈": r"\approx ",
|
|
24
|
+
"≡": r"\equiv ",
|
|
25
|
+
"∈": r"\in ",
|
|
26
|
+
"∉": r"\notin ",
|
|
27
|
+
"⊂": r"\subset ",
|
|
28
|
+
"⊆": r"\subseteq ",
|
|
29
|
+
"∪": r"\cup ",
|
|
30
|
+
"∩": r"\cap ",
|
|
31
|
+
"∑": r"\sum ",
|
|
32
|
+
"∏": r"\prod ",
|
|
33
|
+
"∫": r"\int ",
|
|
34
|
+
"∂": r"\partial ",
|
|
35
|
+
"√": r"\sqrt{}",
|
|
36
|
+
"→": r"\to ",
|
|
37
|
+
"←": r"\leftarrow ",
|
|
38
|
+
"↔": r"\leftrightarrow ",
|
|
39
|
+
}
|
|
40
|
+
_GREEK_MAP = {
|
|
41
|
+
"α": r"\alpha ",
|
|
42
|
+
"β": r"\beta ",
|
|
43
|
+
"γ": r"\gamma ",
|
|
44
|
+
"δ": r"\delta ",
|
|
45
|
+
"ε": r"\epsilon ",
|
|
46
|
+
"θ": r"\theta ",
|
|
47
|
+
"λ": r"\lambda ",
|
|
48
|
+
"μ": r"\mu ",
|
|
49
|
+
"π": r"\pi ",
|
|
50
|
+
"σ": r"\sigma ",
|
|
51
|
+
"φ": r"\phi ",
|
|
52
|
+
"ω": r"\omega ",
|
|
53
|
+
"Γ": r"\Gamma ",
|
|
54
|
+
"Δ": r"\Delta ",
|
|
55
|
+
"Θ": r"\Theta ",
|
|
56
|
+
"Λ": r"\Lambda ",
|
|
57
|
+
"Π": r"\Pi ",
|
|
58
|
+
"Σ": r"\Sigma ",
|
|
59
|
+
"Φ": r"\Phi ",
|
|
60
|
+
"Ω": r"\Omega ",
|
|
61
|
+
}
|
|
62
|
+
_LATEX_ESCAPE_RE = re.compile(r"([#$%&_{}])")
|
|
63
|
+
_LATEX_MATH_TOKEN_ESCAPES = {
|
|
64
|
+
"\\": r"\backslash{}",
|
|
65
|
+
"#": r"\#",
|
|
66
|
+
"$": r"\$",
|
|
67
|
+
"%": r"\%",
|
|
68
|
+
"&": r"\&",
|
|
69
|
+
"_": r"\_",
|
|
70
|
+
"^": r"\^{}",
|
|
71
|
+
"{": r"\{",
|
|
72
|
+
"}": r"\}",
|
|
73
|
+
"~": r"\~{}",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _escape_text(value: str) -> str:
|
|
78
|
+
"""转义进入 LaTeX 文本命令的保留字符。"""
|
|
79
|
+
return _LATEX_ESCAPE_RE.sub(r"\\\1", value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _escape_math_token(value: str) -> str:
|
|
83
|
+
"""转义 MathML 标识符中的 TeX 控制字符,避免字面文本改变公式结构。"""
|
|
84
|
+
return "".join(_LATEX_MATH_TOKEN_ESCAPES.get(char, char) for char in value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _children(element: etree._Element) -> list[etree._Element]:
|
|
88
|
+
"""返回当前元素的全部普通 XML 子元素。"""
|
|
89
|
+
return [child for child in element if isinstance(child.tag, str)]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _join_children(element: etree._Element) -> str:
|
|
93
|
+
"""按文档顺序拼接所有子 MathML 节点。"""
|
|
94
|
+
return "".join(_convert(child) for child in _children(element))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _convert(element: etree._Element) -> str:
|
|
98
|
+
"""递归转换一个常用 MathML 节点,未知容器保留其可解析子项。"""
|
|
99
|
+
name = local_name(element)
|
|
100
|
+
children = _children(element)
|
|
101
|
+
text = (element.text or "").strip()
|
|
102
|
+
if name == "semantics":
|
|
103
|
+
if not children or local_name(children[0]) in {"annotation", "annotation-xml"}:
|
|
104
|
+
return ""
|
|
105
|
+
return _convert(children[0])
|
|
106
|
+
if name in {"math", "mrow", "mstyle", "mpadded", "mphantom"}:
|
|
107
|
+
return _join_children(element)
|
|
108
|
+
if name in {"mi", "mn"}:
|
|
109
|
+
return _GREEK_MAP.get(text, _escape_math_token(text))
|
|
110
|
+
if name == "mo":
|
|
111
|
+
return _OPERATOR_MAP.get(text, text)
|
|
112
|
+
if name == "mtext":
|
|
113
|
+
return rf"\text{{{_escape_text(text)}}}"
|
|
114
|
+
if name == "mspace":
|
|
115
|
+
return r"\,"
|
|
116
|
+
if name == "mfrac" and len(children) >= 2:
|
|
117
|
+
return rf"\frac{{{_convert(children[0])}}}{{{_convert(children[1])}}}"
|
|
118
|
+
if name == "msqrt":
|
|
119
|
+
return rf"\sqrt{{{_join_children(element)}}}"
|
|
120
|
+
if name == "mroot" and len(children) >= 2:
|
|
121
|
+
return rf"\sqrt[{_convert(children[1])}]{{{_convert(children[0])}}}"
|
|
122
|
+
if name == "msup" and len(children) >= 2:
|
|
123
|
+
return rf"{{{_convert(children[0])}}}^{{{_convert(children[1])}}}"
|
|
124
|
+
if name == "msub" and len(children) >= 2:
|
|
125
|
+
return rf"{{{_convert(children[0])}}}_{{{_convert(children[1])}}}"
|
|
126
|
+
if name == "msubsup" and len(children) >= 3:
|
|
127
|
+
return rf"{{{_convert(children[0])}}}_{{{_convert(children[1])}}}^{{{_convert(children[2])}}}"
|
|
128
|
+
if name == "mover" and len(children) >= 2:
|
|
129
|
+
return rf"\overset{{{_convert(children[1])}}}{{{_convert(children[0])}}}"
|
|
130
|
+
if name == "munder" and len(children) >= 2:
|
|
131
|
+
return rf"\underset{{{_convert(children[1])}}}{{{_convert(children[0])}}}"
|
|
132
|
+
if name == "munderover" and len(children) >= 3:
|
|
133
|
+
base = _convert(children[0])
|
|
134
|
+
return rf"\underset{{{_convert(children[1])}}}{{\overset{{{_convert(children[2])}}}{{{base}}}}}"
|
|
135
|
+
if name == "mfenced":
|
|
136
|
+
opening = element.get("open", "(")
|
|
137
|
+
closing = element.get("close", ")")
|
|
138
|
+
separators = element.get("separators", ",") or ","
|
|
139
|
+
values = [_convert(child) for child in children]
|
|
140
|
+
return rf"\left{opening}{separators[0].join(values)}\right{closing}"
|
|
141
|
+
if name == "mtable":
|
|
142
|
+
rows = [_convert(child) for child in children if local_name(child) in {"mtr", "mlabeledtr"}]
|
|
143
|
+
return r"\begin{matrix}" + r" \\ ".join(rows) + r"\end{matrix}"
|
|
144
|
+
if name in {"mtr", "mlabeledtr"}:
|
|
145
|
+
return " & ".join(_convert(child) for child in children)
|
|
146
|
+
if name == "mtd":
|
|
147
|
+
return _join_children(element)
|
|
148
|
+
if name == "annotation" and "tex" in (element.get("encoding", "").casefold()):
|
|
149
|
+
return text
|
|
150
|
+
return _join_children(element) or text
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def mathml_to_latex(math_element: etree._Element) -> str | None:
|
|
154
|
+
"""转换 MathML 根节点,并优先采用生产者保留的 TeX annotation。"""
|
|
155
|
+
for annotation in math_element.iter():
|
|
156
|
+
if not isinstance(annotation.tag, str):
|
|
157
|
+
continue
|
|
158
|
+
if local_name(annotation) != "annotation":
|
|
159
|
+
continue
|
|
160
|
+
encoding = (annotation.get("encoding") or "").casefold()
|
|
161
|
+
if "tex" in encoding and (annotation.text or "").strip():
|
|
162
|
+
return (annotation.text or "").strip()
|
|
163
|
+
latex = _convert(math_element).strip()
|
|
164
|
+
return latex or None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["mathml_to_latex"]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""PDF 模型输出的可见文字清洗;原始字符、布局证据及其它输入格式不在此处理。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..foundation.text import full_to_half_exclude_marks
|
|
10
|
+
from ..schema import BlockType, RAW_CAPTION, RAW_FOOTNOTE, RAW_PHONETIC
|
|
11
|
+
|
|
12
|
+
_FULLWIDTH_MODEL_TEXT = re.compile("[A-Za-z0-9:./\-_%+=@#&*]")
|
|
13
|
+
_PDF_SYMBOL_TRANSLATION = str.maketrans(
|
|
14
|
+
{
|
|
15
|
+
":": ":",
|
|
16
|
+
".": ".",
|
|
17
|
+
"/": "/",
|
|
18
|
+
"\": "\\",
|
|
19
|
+
"-": "-",
|
|
20
|
+
"_": "_",
|
|
21
|
+
"%": "%",
|
|
22
|
+
"+": "+",
|
|
23
|
+
"=": "=",
|
|
24
|
+
"@": "@",
|
|
25
|
+
"#": "#",
|
|
26
|
+
"&": "&",
|
|
27
|
+
"*": "*",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
_FORMULA_OPENING = re.compile(r"\\[\(\[]")
|
|
31
|
+
_NATURAL_LANGUAGE_TYPES = frozenset(
|
|
32
|
+
{
|
|
33
|
+
BlockType.TEXT,
|
|
34
|
+
BlockType.DOC_TITLE,
|
|
35
|
+
BlockType.PARAGRAPH_TITLE,
|
|
36
|
+
BlockType.ASIDE_TEXT,
|
|
37
|
+
BlockType.HEADER,
|
|
38
|
+
BlockType.FOOTER,
|
|
39
|
+
BlockType.PAGE_NUMBER,
|
|
40
|
+
BlockType.PAGE_FOOTNOTE,
|
|
41
|
+
BlockType.REF_TEXT,
|
|
42
|
+
BlockType.LIST,
|
|
43
|
+
BlockType.INDEX,
|
|
44
|
+
BlockType.IMAGE_CAPTION,
|
|
45
|
+
BlockType.IMAGE_FOOTNOTE,
|
|
46
|
+
BlockType.TABLE_CAPTION,
|
|
47
|
+
BlockType.TABLE_FOOTNOTE,
|
|
48
|
+
BlockType.CHART_CAPTION,
|
|
49
|
+
BlockType.CHART_FOOTNOTE,
|
|
50
|
+
BlockType.CODE_CAPTION,
|
|
51
|
+
BlockType.CODE_FOOTNOTE,
|
|
52
|
+
RAW_CAPTION,
|
|
53
|
+
RAW_FOOTNOTE,
|
|
54
|
+
RAW_PHONETIC,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
_OPAQUE_HTML_TAGS = frozenset(
|
|
58
|
+
{"eq", "math", "pre", "code", "script", "style", "svg", "template", "textarea", "object", "embed", "canvas", "iframe"}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalize_plain_text(content: str) -> str:
|
|
63
|
+
"""按一对一码点映射转换英数和 PDF 符号白名单,不改变通用英数工具的契约。"""
|
|
64
|
+
return full_to_half_exclude_marks(content).translate(_PDF_SYMBOL_TRANSLATION)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_text(content: str) -> str:
|
|
68
|
+
"""转换公式范围之外的英数及白名单符号;未闭合公式保护到逻辑文字段末尾。"""
|
|
69
|
+
if not _FULLWIDTH_MODEL_TEXT.search(content):
|
|
70
|
+
return content
|
|
71
|
+
parts: list[str] = []
|
|
72
|
+
cursor = 0
|
|
73
|
+
while match := _FORMULA_OPENING.search(content, cursor):
|
|
74
|
+
parts.append(_normalize_plain_text(content[cursor : match.start()]))
|
|
75
|
+
closing = r"\)" if match.group() == r"\(" else r"\]"
|
|
76
|
+
end = content.find(closing, match.end())
|
|
77
|
+
if end < 0:
|
|
78
|
+
parts.append(content[match.start() :])
|
|
79
|
+
return "".join(parts)
|
|
80
|
+
cursor = end + len(closing)
|
|
81
|
+
parts.append(content[match.start() : cursor])
|
|
82
|
+
parts.append(_normalize_plain_text(content[cursor:]))
|
|
83
|
+
return "".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_parts(parts: Sequence[str]) -> list[str]:
|
|
87
|
+
"""先识别跨节点公式,再按原长度分回节点,保持样式与链接的边界。"""
|
|
88
|
+
source = "".join(parts)
|
|
89
|
+
normalized = _normalize_text(source)
|
|
90
|
+
if source == normalized:
|
|
91
|
+
return list(parts)
|
|
92
|
+
# 英数及符号映射始终一对一;不要使用可能扩展字符的整体 Unicode 规范化。
|
|
93
|
+
result: list[str] = []
|
|
94
|
+
offset = 0
|
|
95
|
+
for part in parts:
|
|
96
|
+
result.append(normalized[offset : offset + len(part)])
|
|
97
|
+
offset += len(part)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _span_parts(spans: list[Any]) -> Iterator[tuple[dict[str, Any] | None, str]]:
|
|
102
|
+
"""递归遍历可见 TextSpan;不读取 URL,并用不可见屏障隔离公式和代码载荷。"""
|
|
103
|
+
for span in spans:
|
|
104
|
+
if not isinstance(span, dict):
|
|
105
|
+
yield None, "\0"
|
|
106
|
+
continue
|
|
107
|
+
content = span.get("content")
|
|
108
|
+
if span.get("type") == "text" and isinstance(content, str):
|
|
109
|
+
yield span, content
|
|
110
|
+
elif span.get("type") == "hyperlink" and isinstance(content, list):
|
|
111
|
+
yield from _span_parts(content)
|
|
112
|
+
else:
|
|
113
|
+
yield None, "\0"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _normalize_spans(spans: list[Any]) -> None:
|
|
117
|
+
"""只更新文字叶子的内容,既不重建 Span,也不合并相邻等样式片段。"""
|
|
118
|
+
entries = list(_span_parts(spans))
|
|
119
|
+
normalized = _normalize_parts([text for _span, text in entries])
|
|
120
|
+
for (span, original), replacement in zip(entries, normalized):
|
|
121
|
+
if span is not None and replacement != original:
|
|
122
|
+
span["content"] = replacement
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_opaque_html_node(node: Any) -> bool:
|
|
126
|
+
"""识别现有 HTML 公式、代码及非文本载体,保持它们的内容与属性原样。"""
|
|
127
|
+
name = str(node.name).split(":")[-1].lower()
|
|
128
|
+
return (
|
|
129
|
+
name in _OPAQUE_HTML_TAGS
|
|
130
|
+
or node.get("data-block-type") in {"equation", "code", "code_body", "algorithm", "algorithm_body"}
|
|
131
|
+
or node.has_attr("data-docvortex-latex")
|
|
132
|
+
or node.has_attr("data-formula-display")
|
|
133
|
+
or "docvortex-math" in (node.get("class") or [])
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _normalize_table(markup: str) -> str:
|
|
138
|
+
"""只修改单元格的可见文本节点;没有实际变化时保留原 HTML 字节表示。"""
|
|
139
|
+
if not _FULLWIDTH_MODEL_TEXT.search(markup) and "&#" not in markup:
|
|
140
|
+
return markup
|
|
141
|
+
# 与现有表格处理保持同一 HTML 解析器,公开模块导入不触发 HTML 依赖。
|
|
142
|
+
from bs4 import BeautifulSoup, NavigableString, Tag
|
|
143
|
+
|
|
144
|
+
soup = BeautifulSoup(markup, "html.parser")
|
|
145
|
+
changed = False
|
|
146
|
+
|
|
147
|
+
def cell_parts(node: Any) -> Iterator[tuple[NavigableString | None, str]]:
|
|
148
|
+
"""样式标签保持透明,嵌套表格由其自身单元格处理,换行和载荷不能拼出定界符。"""
|
|
149
|
+
if type(node) is NavigableString:
|
|
150
|
+
yield node, str(node)
|
|
151
|
+
elif isinstance(node, Tag):
|
|
152
|
+
if _is_opaque_html_node(node) or node.name in {"table", "br", "hr", "img"}:
|
|
153
|
+
yield None, "\0"
|
|
154
|
+
else:
|
|
155
|
+
for child in node.children:
|
|
156
|
+
yield from cell_parts(child)
|
|
157
|
+
|
|
158
|
+
for cell in soup.find_all(["td", "th"]):
|
|
159
|
+
if any(isinstance(parent, Tag) and _is_opaque_html_node(parent) for parent in (cell, *cell.parents)):
|
|
160
|
+
continue
|
|
161
|
+
entries = [part for child in cell.children for part in cell_parts(child)]
|
|
162
|
+
normalized = _normalize_parts([text for _node, text in entries])
|
|
163
|
+
for (node, original), replacement in zip(entries, normalized):
|
|
164
|
+
if node is not None and replacement != original:
|
|
165
|
+
node.replace_with(NavigableString(replacement))
|
|
166
|
+
changed = True
|
|
167
|
+
return str(soup) if changed else markup
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def normalize_pdf_model_text(model_list: list[list[dict[str, Any]]]) -> None:
|
|
171
|
+
"""统一 PDF 自然语言及表格可见英数与白名单符号,保留公式、代码、URL 和结构。
|
|
172
|
+
|
|
173
|
+
应在样式、上下标和链接匹配结束后、ModelJson 构造前调用;函数幂等,不修改
|
|
174
|
+
原始字符证据,也不会将字符串转换为 Span 或清理其它模型元数据。
|
|
175
|
+
"""
|
|
176
|
+
for page in model_list:
|
|
177
|
+
for block in page:
|
|
178
|
+
kind, content = block.get("type"), block.get("content")
|
|
179
|
+
if kind in _NATURAL_LANGUAGE_TYPES:
|
|
180
|
+
if isinstance(content, str):
|
|
181
|
+
block["content"] = _normalize_text(content)
|
|
182
|
+
elif isinstance(content, list):
|
|
183
|
+
_normalize_spans(content)
|
|
184
|
+
elif kind in {BlockType.TABLE, BlockType.TABLE_BODY} and isinstance(content, str):
|
|
185
|
+
block["content"] = _normalize_table(content)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = ["normalize_pdf_model_text"]
|