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,347 @@
|
|
|
1
|
+
"""HTML table 到 TeX Live longtable/tabular 源码的安全物化。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeAlias
|
|
6
|
+
|
|
7
|
+
from bs4 import NavigableString, Tag
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from ....schema import CodeInlineSpan, EquationInlineSpan, HyperlinkSpan, InlineSpan, InlineStyle, TextSpan, parse_inline_spans
|
|
11
|
+
from ....foundation.hyperlink import OFFICE_EXTERNAL_HYPERLINK_SCHEMES, sanitize_hyperlink_target
|
|
12
|
+
from ..common.html_table import (
|
|
13
|
+
HtmlTableCell,
|
|
14
|
+
HtmlTableError,
|
|
15
|
+
HtmlTableGrid,
|
|
16
|
+
HtmlTableSource,
|
|
17
|
+
MAX_NESTED_TABLE_DEPTH,
|
|
18
|
+
parse_html_tables,
|
|
19
|
+
)
|
|
20
|
+
from .assets import resolve_html_image_path, tex_image_path
|
|
21
|
+
from .inline import LatexAnchorRegistry, escape_latex_text, escape_latex_url, render_inline_spans
|
|
22
|
+
|
|
23
|
+
_BLOCK_TAGS = {"address", "article", "blockquote", "div", "figcaption", "footer", "header", "li", "p", "section"}
|
|
24
|
+
_SKIPPED_TAGS = {"script", "style", "template", "noscript"}
|
|
25
|
+
_HtmlCellToken: TypeAlias = InlineSpan | Tag
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LatexTableError(HtmlTableError):
|
|
29
|
+
"""表示 HTML table 无法安全物化为 LaTeX 表格。"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LatexTableRenderer:
|
|
33
|
+
"""持有图片路径和 anchor 上下文的递归 LaTeX 表格 renderer。"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, *, asset_base_path: str, anchors: LatexAnchorRegistry) -> None:
|
|
36
|
+
"""保存表格单元格渲染所需的文档级上下文。"""
|
|
37
|
+
self.asset_base_path = asset_base_path
|
|
38
|
+
self.anchors = anchors
|
|
39
|
+
|
|
40
|
+
def render(self, source: HtmlTableSource) -> str:
|
|
41
|
+
"""把一个或多个顶层 HTML table 转换为可分页 LaTeX 表格。"""
|
|
42
|
+
return self._render_source(source, depth=1)
|
|
43
|
+
|
|
44
|
+
def _render_source(self, source: HtmlTableSource, *, depth: int) -> str:
|
|
45
|
+
"""递归解析当前层级的 table,并执行深度上限校验。"""
|
|
46
|
+
if depth > MAX_NESTED_TABLE_DEPTH:
|
|
47
|
+
raise LatexTableError(f"Nested table depth exceeds {MAX_NESTED_TABLE_DEPTH}")
|
|
48
|
+
try:
|
|
49
|
+
grids = parse_html_tables(source)
|
|
50
|
+
except HtmlTableError as exc:
|
|
51
|
+
raise LatexTableError(str(exc)) from exc
|
|
52
|
+
return "\n\n".join(self._render_grid(grid, depth=depth) for grid in grids)
|
|
53
|
+
|
|
54
|
+
def _render_grid(self, grid: HtmlTableGrid, *, depth: int) -> str:
|
|
55
|
+
"""把严格占位网格转换为 longtable 或嵌套 tabular。"""
|
|
56
|
+
environment = "longtable" if depth == 1 else "tabular"
|
|
57
|
+
column_spec = self._column_spec(grid.column_count)
|
|
58
|
+
occupied = {
|
|
59
|
+
(row, column): placement
|
|
60
|
+
for placement in grid.cells
|
|
61
|
+
for row in range(placement.row, placement.end_row + 1)
|
|
62
|
+
for column in range(placement.column, placement.end_column + 1)
|
|
63
|
+
}
|
|
64
|
+
lines = [rf"\begin{{{environment}}}{{{column_spec}}}", r"\hline"]
|
|
65
|
+
for row_index in range(grid.row_count):
|
|
66
|
+
segments: list[str] = []
|
|
67
|
+
column_index = 0
|
|
68
|
+
while column_index < grid.column_count:
|
|
69
|
+
placement = occupied[(row_index, column_index)]
|
|
70
|
+
if placement.column != column_index:
|
|
71
|
+
column_index += 1
|
|
72
|
+
continue
|
|
73
|
+
if placement.row == row_index:
|
|
74
|
+
segment = self._render_cell(
|
|
75
|
+
placement.tag,
|
|
76
|
+
is_header=placement.is_header,
|
|
77
|
+
rowspan=placement.rowspan,
|
|
78
|
+
colspan=placement.colspan,
|
|
79
|
+
column_count=grid.column_count,
|
|
80
|
+
depth=depth,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
segment = self._continued_rowspan_placeholder(placement.colspan)
|
|
84
|
+
segments.append(segment)
|
|
85
|
+
column_index += placement.colspan
|
|
86
|
+
lines.append(" & ".join(segments) + r" \\")
|
|
87
|
+
lines.extend(_row_rules(occupied, row_index=row_index, column_count=grid.column_count))
|
|
88
|
+
lines.append(rf"\end{{{environment}}}")
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
def _render_cell(
|
|
92
|
+
self,
|
|
93
|
+
cell: Tag,
|
|
94
|
+
*,
|
|
95
|
+
is_header: bool,
|
|
96
|
+
rowspan: int,
|
|
97
|
+
colspan: int,
|
|
98
|
+
column_count: int,
|
|
99
|
+
depth: int,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""渲染一个 origin cell,并组合 rowspan/colspan 声明。"""
|
|
102
|
+
parts = self._render_cell_parts(cell, depth=depth)
|
|
103
|
+
content = r"\par ".join(part for part in parts if part) or "~"
|
|
104
|
+
if is_header:
|
|
105
|
+
content = rf"\cellcolor{{DocVortexTableHeader}}\textbf{{{content}}}"
|
|
106
|
+
width = _cell_width(colspan, column_count)
|
|
107
|
+
if rowspan > 1:
|
|
108
|
+
content = rf"\multirow{{{rowspan}}}{{*}}{{\parbox[t]{{{width}}}{{{content}}}}}"
|
|
109
|
+
if colspan > 1:
|
|
110
|
+
content = rf"\multicolumn{{{colspan}}}{{|p{{{width}}}|}}{{{content}}}"
|
|
111
|
+
return content
|
|
112
|
+
|
|
113
|
+
def _render_cell_parts(self, cell: Tag, *, depth: int) -> list[str]:
|
|
114
|
+
"""按 HTML 来源顺序交替渲染行内内容、图片与嵌套表格。"""
|
|
115
|
+
|
|
116
|
+
parts: list[str] = []
|
|
117
|
+
inline_buffer: list[InlineSpan] = []
|
|
118
|
+
|
|
119
|
+
def flush_inline_buffer() -> None:
|
|
120
|
+
"""把当前连续行内 token 校验并渲染为一个 LaTeX 片段。"""
|
|
121
|
+
|
|
122
|
+
if not inline_buffer:
|
|
123
|
+
return
|
|
124
|
+
spans = _parse_cell_inline_spans(inline_buffer)
|
|
125
|
+
rendered = render_inline_spans(spans, self.anchors)
|
|
126
|
+
if rendered:
|
|
127
|
+
parts.append(rendered)
|
|
128
|
+
inline_buffer.clear()
|
|
129
|
+
|
|
130
|
+
for token in _html_cell_tokens(cell):
|
|
131
|
+
if not isinstance(token, Tag):
|
|
132
|
+
inline_buffer.append(token)
|
|
133
|
+
continue
|
|
134
|
+
flush_inline_buffer()
|
|
135
|
+
if token.name == "img":
|
|
136
|
+
parts.append(self._render_image_tag(token))
|
|
137
|
+
elif token.name == "table":
|
|
138
|
+
parts.append(self._render_source(token, depth=depth + 1))
|
|
139
|
+
flush_inline_buffer()
|
|
140
|
+
return parts
|
|
141
|
+
|
|
142
|
+
def _render_image_tag(self, image: Tag) -> str:
|
|
143
|
+
"""读取一个单元格图片标签并渲染为 LaTeX 图片或可见回退。"""
|
|
144
|
+
|
|
145
|
+
source = image.get("src", "")
|
|
146
|
+
if isinstance(source, list):
|
|
147
|
+
source = ""
|
|
148
|
+
alt = image.get("alt", "image")
|
|
149
|
+
if isinstance(alt, list):
|
|
150
|
+
alt = " ".join(str(item) for item in alt)
|
|
151
|
+
return self._render_image(str(source), str(alt))
|
|
152
|
+
|
|
153
|
+
def _render_image(self, source: str, alt_text: str) -> str:
|
|
154
|
+
"""渲染单元格图片;非本地 sidecar 退化为可见链接或占位。"""
|
|
155
|
+
path = resolve_html_image_path(source, self.asset_base_path)
|
|
156
|
+
if path is not None:
|
|
157
|
+
return rf"\includegraphics[width=.85\linewidth,height=.25\textheight,keepaspectratio]{{{tex_image_path(path)}}}"
|
|
158
|
+
label = rf"\textit{{{escape_latex_text(f'image unavailable: {alt_text}')}}}"
|
|
159
|
+
normalized = source.strip()
|
|
160
|
+
if normalized.startswith(("http://", "https://")):
|
|
161
|
+
return rf"\href{{{escape_latex_url(normalized)}}}{{{label}}}"
|
|
162
|
+
return label
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _column_spec(column_count: int) -> str:
|
|
166
|
+
"""按逻辑列数生成确定性的等宽段落列声明。"""
|
|
167
|
+
if column_count <= 0:
|
|
168
|
+
raise LatexTableError("Table must contain at least one column")
|
|
169
|
+
width = _cell_width(1, column_count)
|
|
170
|
+
column = rf">{{\raggedright\arraybackslash}}p{{{width}}}"
|
|
171
|
+
return "|" + "|".join(column for _ in range(column_count)) + "|"
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _continued_rowspan_placeholder(colspan: int) -> str:
|
|
175
|
+
"""为前序行延续下来的 rowspan 写入等列宽空占位。"""
|
|
176
|
+
return rf"\multicolumn{{{colspan}}}{{|l|}}{{}}" if colspan > 1 else ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cell_width(colspan: int, column_count: int) -> str:
|
|
180
|
+
"""按表格总列数计算当前单元格占用的 linewidth 比例。"""
|
|
181
|
+
ratio = 0.94 * colspan / column_count
|
|
182
|
+
return f"{ratio:.6f}\\linewidth"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _row_rules(
|
|
186
|
+
occupied: dict[tuple[int, int], HtmlTableCell],
|
|
187
|
+
*,
|
|
188
|
+
row_index: int,
|
|
189
|
+
column_count: int,
|
|
190
|
+
) -> list[str]:
|
|
191
|
+
"""只在当前行结束的单元格列下画线,避免横线切穿 rowspan。"""
|
|
192
|
+
ending_columns = [column for column in range(column_count) if occupied[(row_index, column)].end_row == row_index]
|
|
193
|
+
if len(ending_columns) == column_count:
|
|
194
|
+
return [r"\hline"]
|
|
195
|
+
rules: list[str] = []
|
|
196
|
+
range_start: int | None = None
|
|
197
|
+
previous: int | None = None
|
|
198
|
+
for column in ending_columns:
|
|
199
|
+
if range_start is None:
|
|
200
|
+
range_start = column
|
|
201
|
+
elif previous is not None and column != previous + 1:
|
|
202
|
+
rules.append(rf"\cline{{{range_start + 1}-{previous + 1}}}")
|
|
203
|
+
range_start = column
|
|
204
|
+
previous = column
|
|
205
|
+
if range_start is not None and previous is not None:
|
|
206
|
+
rules.append(rf"\cline{{{range_start + 1}-{previous + 1}}}")
|
|
207
|
+
return rules
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_cell_inline_spans(spans: list[InlineSpan]) -> list[InlineSpan]:
|
|
211
|
+
"""校验一个不跨图片或嵌套表格的连续单元格行内片段。"""
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
return parse_inline_spans(spans)
|
|
215
|
+
except (TypeError, ValidationError, ValueError) as exc:
|
|
216
|
+
raise LatexTableError("HTML table cell inline content is invalid") from exc
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _html_cell_tokens(cell: Tag) -> list[_HtmlCellToken]:
|
|
220
|
+
"""按来源顺序返回单元格中的行内、图片与嵌套表格 token。"""
|
|
221
|
+
|
|
222
|
+
return [
|
|
223
|
+
token
|
|
224
|
+
for child in cell.children
|
|
225
|
+
for token in _html_node_tokens(
|
|
226
|
+
child,
|
|
227
|
+
styles=(),
|
|
228
|
+
allow_links=True,
|
|
229
|
+
)
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _html_node_tokens(
|
|
234
|
+
node: object,
|
|
235
|
+
*,
|
|
236
|
+
styles: tuple[InlineStyle, ...],
|
|
237
|
+
allow_links: bool,
|
|
238
|
+
) -> list[_HtmlCellToken]:
|
|
239
|
+
"""递归解析 HTML 节点,同时把图片和嵌套表格保留为顺序屏障。"""
|
|
240
|
+
|
|
241
|
+
if isinstance(node, NavigableString):
|
|
242
|
+
text = str(node)
|
|
243
|
+
return [TextSpan(type="text", content=text, styles=list(styles))] if text else []
|
|
244
|
+
if not isinstance(node, Tag):
|
|
245
|
+
return []
|
|
246
|
+
name = (node.name or "").lower()
|
|
247
|
+
if name in _SKIPPED_TAGS:
|
|
248
|
+
return []
|
|
249
|
+
if name in {"img", "table"}:
|
|
250
|
+
return [node]
|
|
251
|
+
if name == "br":
|
|
252
|
+
return [TextSpan(type="text", content="\n", styles=list(styles))]
|
|
253
|
+
if name == "eq":
|
|
254
|
+
content = node.get_text()
|
|
255
|
+
return [EquationInlineSpan(type="equation_inline", content=content)] if content.strip() else []
|
|
256
|
+
if name == "code":
|
|
257
|
+
content = node.get_text()
|
|
258
|
+
return [CodeInlineSpan(type="code_inline", content=content)] if content else []
|
|
259
|
+
style_name: InlineStyle | None = {
|
|
260
|
+
"b": "bold",
|
|
261
|
+
"strong": "bold",
|
|
262
|
+
"i": "italic",
|
|
263
|
+
"em": "italic",
|
|
264
|
+
"u": "underline",
|
|
265
|
+
"s": "strikethrough",
|
|
266
|
+
"del": "strikethrough",
|
|
267
|
+
"sup": "superscript",
|
|
268
|
+
"sub": "subscript",
|
|
269
|
+
}.get(name)
|
|
270
|
+
child_styles = tuple(dict.fromkeys((*styles, style_name))) if style_name is not None else styles
|
|
271
|
+
if name == "a" and allow_links:
|
|
272
|
+
children = [
|
|
273
|
+
token
|
|
274
|
+
for child in node.children
|
|
275
|
+
for token in _html_node_tokens(
|
|
276
|
+
child,
|
|
277
|
+
styles=child_styles,
|
|
278
|
+
allow_links=False,
|
|
279
|
+
)
|
|
280
|
+
if not isinstance(token, HyperlinkSpan)
|
|
281
|
+
]
|
|
282
|
+
target = sanitize_hyperlink_target(
|
|
283
|
+
node.get("href"),
|
|
284
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
285
|
+
allow_relative=True,
|
|
286
|
+
allow_fragment=True,
|
|
287
|
+
)
|
|
288
|
+
return _wrap_hyperlink_tokens(children, target)
|
|
289
|
+
tokens = [
|
|
290
|
+
token
|
|
291
|
+
for child in node.children
|
|
292
|
+
for token in _html_node_tokens(
|
|
293
|
+
child,
|
|
294
|
+
styles=child_styles,
|
|
295
|
+
allow_links=allow_links,
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
if name in _BLOCK_TAGS and tokens and not _tokens_end_with_newline(tokens):
|
|
299
|
+
tokens.append(TextSpan(type="text", content="\n"))
|
|
300
|
+
return tokens
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _wrap_hyperlink_tokens(
|
|
304
|
+
tokens: list[_HtmlCellToken],
|
|
305
|
+
target: str | None,
|
|
306
|
+
) -> list[_HtmlCellToken]:
|
|
307
|
+
"""只包装链接内连续的 InlineSpan,让图片和嵌套表格保持原位。"""
|
|
308
|
+
|
|
309
|
+
if target is None:
|
|
310
|
+
return tokens
|
|
311
|
+
output: list[_HtmlCellToken] = []
|
|
312
|
+
inline_buffer: list[InlineSpan] = []
|
|
313
|
+
|
|
314
|
+
def flush_inline_buffer() -> None:
|
|
315
|
+
"""把当前链接行内片段包装成一个 HyperlinkSpan。"""
|
|
316
|
+
|
|
317
|
+
if not inline_buffer:
|
|
318
|
+
return
|
|
319
|
+
try:
|
|
320
|
+
output.append(
|
|
321
|
+
HyperlinkSpan(
|
|
322
|
+
type="hyperlink",
|
|
323
|
+
url=target,
|
|
324
|
+
content=list(inline_buffer),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
except ValidationError:
|
|
328
|
+
output.extend(inline_buffer)
|
|
329
|
+
inline_buffer.clear()
|
|
330
|
+
|
|
331
|
+
for token in tokens:
|
|
332
|
+
if isinstance(token, Tag):
|
|
333
|
+
flush_inline_buffer()
|
|
334
|
+
output.append(token)
|
|
335
|
+
else:
|
|
336
|
+
inline_buffer.append(token)
|
|
337
|
+
flush_inline_buffer()
|
|
338
|
+
return output
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _tokens_end_with_newline(tokens: list[_HtmlCellToken]) -> bool:
|
|
342
|
+
"""判断当前 token 序列是否已经以普通文本换行结束。"""
|
|
343
|
+
|
|
344
|
+
return bool(tokens and isinstance(tokens[-1], TextSpan) and tokens[-1].content.endswith("\n"))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
__all__ = ["LatexTableError", "LatexTableRenderer"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Markdown 与 Content List 共用的图片资源解析。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from ....schema import ImagePayloadBlock
|
|
9
|
+
|
|
10
|
+
_HTML_IMAGE_SRC_RE = re.compile(
|
|
11
|
+
r"(?P<prefix>\bsrc\s*=\s*)(?P<quote>[\"'])(?P<src>.*?)(?P=quote)",
|
|
12
|
+
re.IGNORECASE | re.DOTALL,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_image_source(block: ImagePayloadBlock, asset_base_url: str = "") -> str | None:
|
|
17
|
+
"""按 sidecar、data URI、远程 URL 的稳定优先级解析图片来源。"""
|
|
18
|
+
if block.image_path:
|
|
19
|
+
return join_asset_base_url(asset_base_url, block.image_path)
|
|
20
|
+
if block.image_base64:
|
|
21
|
+
return block.image_base64
|
|
22
|
+
if block.image_url:
|
|
23
|
+
return block.image_url
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def join_asset_base_url(asset_base_url: str, relative_path: str) -> str:
|
|
28
|
+
"""使用 POSIX 语义拼接资源根地址与安全相对路径。"""
|
|
29
|
+
if not asset_base_url:
|
|
30
|
+
return relative_path
|
|
31
|
+
return f"{asset_base_url.rstrip('/')}/{relative_path.lstrip('/')}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_markdown_image(source: str, alt: str = "") -> str:
|
|
35
|
+
"""构造不会被空格或括号截断的 Markdown 图片语法。"""
|
|
36
|
+
if not source:
|
|
37
|
+
return ""
|
|
38
|
+
safe_alt = alt.replace("[", r"\[").replace("]", r"\]")
|
|
39
|
+
destination = normalize_image_source(source)
|
|
40
|
+
return f""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_image_source(source: str) -> str:
|
|
44
|
+
"""把图片来源规范化为 Markdown 与 structured_content 共用的安全地址。"""
|
|
45
|
+
if source.startswith("data:"):
|
|
46
|
+
return source
|
|
47
|
+
return quote(source, safe="/:#?&=%@+~,;!$'*-._")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def prefix_html_image_sources(markup: str, asset_base_url: str = "") -> str:
|
|
51
|
+
"""给 HTML 中的相对图片地址添加资源根地址。"""
|
|
52
|
+
if not markup or not asset_base_url:
|
|
53
|
+
return markup
|
|
54
|
+
|
|
55
|
+
def _replace(match: re.Match[str]) -> str:
|
|
56
|
+
"""只重写相对 src,保留 data URI、绝对 URL 与根路径。"""
|
|
57
|
+
source = match.group("src")
|
|
58
|
+
if _is_absolute_image_source(source):
|
|
59
|
+
return match.group(0)
|
|
60
|
+
resolved = join_asset_base_url(asset_base_url, source)
|
|
61
|
+
return f"{match.group('prefix')}{match.group('quote')}{resolved}{match.group('quote')}"
|
|
62
|
+
|
|
63
|
+
return _HTML_IMAGE_SRC_RE.sub(_replace, markup)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_absolute_image_source(source: str) -> bool:
|
|
67
|
+
"""判断 HTML 图片地址是否已经是绝对或不可前缀化的来源。"""
|
|
68
|
+
normalized = source.strip().lower()
|
|
69
|
+
return normalized.startswith(("data:", "http://", "https://", "//", "/", "#"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"build_markdown_image",
|
|
74
|
+
"join_asset_base_url",
|
|
75
|
+
"normalize_image_source",
|
|
76
|
+
"prefix_html_image_sources",
|
|
77
|
+
"resolve_image_source",
|
|
78
|
+
]
|