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,117 @@
|
|
|
1
|
+
"""Standalone HTML 到单页 DocVortex raw model-list 的原生 converter。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, BinaryIO
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from ....schema import BlockType
|
|
12
|
+
from docvortex.content.markup import MarkupProjector, MarkupStylesheet
|
|
13
|
+
from ....content.spans import text_spans
|
|
14
|
+
from .anchors import HtmlAnchorRegistry, append_referenced_notes
|
|
15
|
+
from .constants import MAX_HTML_BYTES, MAX_HTML_RENDERED_BYTES
|
|
16
|
+
from docvortex.document.contracts import HtmlSourceContext
|
|
17
|
+
from .document import HtmlDocument, parse_html_document
|
|
18
|
+
from .errors import HtmlResourceLimitError
|
|
19
|
+
from .resources import HtmlResourceContext
|
|
20
|
+
from .selector import select_auto_content
|
|
21
|
+
from ....codecs.html import decode_docvortex_html_wire
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HtmlConverter:
|
|
25
|
+
"""把静态 HTML 转换为一个无 bbox 的逻辑页。"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
"""初始化空页面结果。"""
|
|
29
|
+
self.pages: list[list[dict[str, Any]]] = []
|
|
30
|
+
|
|
31
|
+
def convert(
|
|
32
|
+
self,
|
|
33
|
+
file_binary: BinaryIO,
|
|
34
|
+
*,
|
|
35
|
+
source_context: HtmlSourceContext | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""读取调用方 HTML 流,自动选择正文并生成单页 raw blocks。"""
|
|
38
|
+
file_bytes = file_binary.read(MAX_HTML_BYTES + 1)
|
|
39
|
+
if len(file_bytes) > MAX_HTML_BYTES:
|
|
40
|
+
raise HtmlResourceLimitError(f"HTML resource limit exceeded: max_html_bytes={MAX_HTML_BYTES}")
|
|
41
|
+
document = parse_html_document(file_bytes, source_context)
|
|
42
|
+
resources = HtmlResourceContext(document.source_context, base_href=document.base_href)
|
|
43
|
+
wire_result = decode_docvortex_html_wire(document.body, resources)
|
|
44
|
+
if wire_result.blocks is not None:
|
|
45
|
+
blocks = wire_result.blocks
|
|
46
|
+
log_values = ("docvortex_exact", 1.0, 1.0, "version_1")
|
|
47
|
+
else:
|
|
48
|
+
if wire_result.fallback_reason is not None:
|
|
49
|
+
logger.warning("DocVortex HTML marker fallback reason={}", wire_result.fallback_reason)
|
|
50
|
+
stylesheet = _load_stylesheet(document, resources)
|
|
51
|
+
selection = select_auto_content(document.body, stylesheet)
|
|
52
|
+
selected_root = append_referenced_notes(
|
|
53
|
+
selection.root,
|
|
54
|
+
document.body,
|
|
55
|
+
stylesheet=stylesheet,
|
|
56
|
+
resolve_same_document_fragment=resources.same_document_fragment,
|
|
57
|
+
)
|
|
58
|
+
source_key = document.source_context.source_uri or "html"
|
|
59
|
+
anchors = HtmlAnchorRegistry(selected_root, stylesheet, source_key=source_key)
|
|
60
|
+
resources.bind_anchors(anchors)
|
|
61
|
+
blocks = MarkupProjector(
|
|
62
|
+
selected_root,
|
|
63
|
+
resources,
|
|
64
|
+
stylesheet,
|
|
65
|
+
single_document_title=True,
|
|
66
|
+
).convert()
|
|
67
|
+
if not any(block.get("type") == BlockType.DOC_TITLE for block in blocks):
|
|
68
|
+
if title := _document_title(document):
|
|
69
|
+
blocks.insert(0, {"type": BlockType.DOC_TITLE, "level": 1, "content": text_spans(title)})
|
|
70
|
+
log_values = (
|
|
71
|
+
selection.mode_used,
|
|
72
|
+
selection.confidence,
|
|
73
|
+
selection.retained_text_ratio,
|
|
74
|
+
selection.reason,
|
|
75
|
+
)
|
|
76
|
+
rendered_bytes = len(json.dumps(blocks, ensure_ascii=False, separators=(",", ":")).encode())
|
|
77
|
+
if rendered_bytes > MAX_HTML_RENDERED_BYTES:
|
|
78
|
+
raise HtmlResourceLimitError(f"HTML projection exceeds max_html_rendered_bytes={MAX_HTML_RENDERED_BYTES}")
|
|
79
|
+
logger.debug(
|
|
80
|
+
"HTML content selection finished mode={} confidence={:.3f} retained_text_ratio={:.3f} reason={}",
|
|
81
|
+
*log_values,
|
|
82
|
+
)
|
|
83
|
+
self.pages = [blocks]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_stylesheet(document: HtmlDocument, resources: HtmlResourceContext) -> MarkupStylesheet:
|
|
87
|
+
"""按 head 文档顺序加载本地 stylesheet 与内联 style 的受支持子集。"""
|
|
88
|
+
stylesheet = MarkupStylesheet()
|
|
89
|
+
for source in document.stylesheets:
|
|
90
|
+
if source.kind == "inline":
|
|
91
|
+
resources.charge_inline_stylesheet(source.value)
|
|
92
|
+
stylesheet.add(source.value)
|
|
93
|
+
elif css := resources.load_stylesheet(source.value):
|
|
94
|
+
stylesheet.add(css)
|
|
95
|
+
return stylesheet
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _document_title(document: HtmlDocument) -> str | None:
|
|
99
|
+
"""按 OpenGraph/title 优先级返回去重且保守去站点后缀的标题。"""
|
|
100
|
+
title = (document.open_graph_title or document.title or "").strip()
|
|
101
|
+
if not title:
|
|
102
|
+
return None
|
|
103
|
+
site_name = (document.site_name or "").strip()
|
|
104
|
+
if site_name:
|
|
105
|
+
for separator in (" - ", " | ", " · ", " — ", " _ "):
|
|
106
|
+
suffix = f"{separator}{site_name}"
|
|
107
|
+
prefix = f"{site_name}{separator}"
|
|
108
|
+
if title.casefold().endswith(suffix.casefold()):
|
|
109
|
+
title = title[: -len(suffix)].strip()
|
|
110
|
+
break
|
|
111
|
+
if title.casefold().startswith(prefix.casefold()):
|
|
112
|
+
title = title[len(prefix) :].strip()
|
|
113
|
+
break
|
|
114
|
+
return re.sub(r"\s+", " ", title) or None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["HtmlConverter"]
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""安全加载、规范化并描述一个 standalone HTML 文档。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import codecs
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import re
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from lxml import etree, html as lxml_html # type: ignore[reportMissingImports]
|
|
12
|
+
|
|
13
|
+
from docvortex.content.markup.formula import FormulaExtraction, extract_formula, is_tex_script
|
|
14
|
+
from docvortex.content.markup.projector import local_name
|
|
15
|
+
from .constants import MAX_HTML_BYTES, MAX_HTML_DEPTH, MAX_HTML_NODES
|
|
16
|
+
from docvortex.document.contracts import HtmlSourceContext
|
|
17
|
+
from .errors import HtmlParseError, HtmlResourceLimitError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_ACTIVE_TAGS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"applet",
|
|
23
|
+
"audio",
|
|
24
|
+
"button",
|
|
25
|
+
"canvas",
|
|
26
|
+
"embed",
|
|
27
|
+
"form",
|
|
28
|
+
"iframe",
|
|
29
|
+
"input",
|
|
30
|
+
"object",
|
|
31
|
+
"script",
|
|
32
|
+
"select",
|
|
33
|
+
"style",
|
|
34
|
+
"template",
|
|
35
|
+
"textarea",
|
|
36
|
+
"video",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
_FORMULA_GENERATOR_CLASS_TOKENS = frozenset({"katex", "mathjax", "docvortex-math"})
|
|
40
|
+
_GENERIC_FORMULA_CLASS_TOKENS = frozenset({"formula", "math", "tex"})
|
|
41
|
+
_FORMULA_VISIBILITY_ATTRIBUTES = ("hidden", "aria-hidden", "style", "class")
|
|
42
|
+
_MEANINGFUL_FORMULA_SIBLING_TAGS = frozenset(
|
|
43
|
+
{"audio", "br", "canvas", "figure", "hr", "iframe", "image", "img", "object", "svg", "table", "video"}
|
|
44
|
+
)
|
|
45
|
+
_ASCIIMATH_SCRIPT_TYPE_RE = re.compile(r"^math/asciimath(?:\s*;.*)?$", re.IGNORECASE)
|
|
46
|
+
_HTML_ENCODING_DECLARATION_CANDIDATE_RE = re.compile(rb"(?is)<(?:meta\b[^>]*\bcharset\s*=|\?xml\b[^>]*\bencoding\s*=)")
|
|
47
|
+
_XML_ENCODING_DECLARATION_RE = re.compile(rb"(?is)^<\?xml\b[^>]*\bencoding\s*=")
|
|
48
|
+
_META_CONTENT_CHARSET_RE = re.compile(r"\bcharset\s*=", re.IGNORECASE)
|
|
49
|
+
_UNICODE_BOMS = (b"\x00\x00\xfe\xff", b"\xff\xfe\x00\x00", b"\xef\xbb\xbf", b"\xfe\xff", b"\xff\xfe")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class HtmlStylesheetSource:
|
|
54
|
+
"""保存一个按 head 源顺序出现的内联或外链 stylesheet。"""
|
|
55
|
+
|
|
56
|
+
kind: Literal["inline", "link"]
|
|
57
|
+
value: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class HtmlDocument:
|
|
62
|
+
"""保存已规范化 DOM、标题、样式引用与来源上下文。"""
|
|
63
|
+
|
|
64
|
+
root: etree._Element
|
|
65
|
+
body: etree._Element
|
|
66
|
+
stylesheets: tuple[HtmlStylesheetSource, ...]
|
|
67
|
+
base_href: str | None
|
|
68
|
+
title: str | None
|
|
69
|
+
open_graph_title: str | None
|
|
70
|
+
site_name: str | None
|
|
71
|
+
source_context: HtmlSourceContext
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_html_document(file_bytes: bytes, source_context: HtmlSourceContext | None = None) -> HtmlDocument:
|
|
75
|
+
"""从受限字节输入构造不执行脚本且资源引用尚未加载的 HTML DOM。"""
|
|
76
|
+
if len(file_bytes) > MAX_HTML_BYTES:
|
|
77
|
+
raise HtmlResourceLimitError(f"HTML resource limit exceeded: max_html_bytes={MAX_HTML_BYTES}")
|
|
78
|
+
context = source_context or HtmlSourceContext()
|
|
79
|
+
if not file_bytes.strip():
|
|
80
|
+
root = etree.Element("html")
|
|
81
|
+
body = etree.SubElement(root, "body")
|
|
82
|
+
return HtmlDocument(root, body, (), None, None, None, None, context)
|
|
83
|
+
|
|
84
|
+
transport_encoding = _normalize_transport_encoding(context.transport_encoding)
|
|
85
|
+
parser = lxml_html.HTMLParser(
|
|
86
|
+
recover=True,
|
|
87
|
+
no_network=True,
|
|
88
|
+
remove_comments=False,
|
|
89
|
+
huge_tree=False,
|
|
90
|
+
encoding=transport_encoding,
|
|
91
|
+
)
|
|
92
|
+
try:
|
|
93
|
+
root = lxml_html.document_fromstring(
|
|
94
|
+
_html_parser_input(file_bytes, transport_encoding=transport_encoding),
|
|
95
|
+
parser=parser,
|
|
96
|
+
)
|
|
97
|
+
except (etree.ParserError, etree.XMLSyntaxError, UnicodeError, ValueError) as exc:
|
|
98
|
+
raise HtmlParseError(f"Malformed HTML document: {exc}") from exc
|
|
99
|
+
_validate_dom_shape(root)
|
|
100
|
+
|
|
101
|
+
stylesheets: list[HtmlStylesheetSource] = []
|
|
102
|
+
for element in root.iter():
|
|
103
|
+
if not isinstance(element.tag, str):
|
|
104
|
+
continue
|
|
105
|
+
name = local_name(element)
|
|
106
|
+
if name == "style":
|
|
107
|
+
if _has_discarded_active_ancestor(element):
|
|
108
|
+
continue
|
|
109
|
+
stylesheets.append(HtmlStylesheetSource("inline", "".join(element.itertext())))
|
|
110
|
+
elif name == "link" and "stylesheet" in (element.get("rel") or "").casefold().split():
|
|
111
|
+
if _has_discarded_active_ancestor(element):
|
|
112
|
+
continue
|
|
113
|
+
if href := (element.get("href") or "").strip():
|
|
114
|
+
stylesheets.append(HtmlStylesheetSource("link", href))
|
|
115
|
+
base_href = next(
|
|
116
|
+
(
|
|
117
|
+
value
|
|
118
|
+
for element in root.iter()
|
|
119
|
+
if isinstance(element.tag, str)
|
|
120
|
+
and local_name(element) == "base"
|
|
121
|
+
and not _has_discarded_active_ancestor(element)
|
|
122
|
+
and (value := (element.get("href") or "").strip())
|
|
123
|
+
),
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
title = next(
|
|
127
|
+
(
|
|
128
|
+
_collapsed_text(element)
|
|
129
|
+
for element in root.iter()
|
|
130
|
+
if isinstance(element.tag, str) and local_name(element) == "title" and _collapsed_text(element)
|
|
131
|
+
),
|
|
132
|
+
None,
|
|
133
|
+
)
|
|
134
|
+
open_graph_title = _meta_content(root, property_name="og:title")
|
|
135
|
+
site_name = _meta_content(root, property_name="og:site_name")
|
|
136
|
+
|
|
137
|
+
_normalize_formula_sources(root)
|
|
138
|
+
_remove_active_content(root)
|
|
139
|
+
body = next(
|
|
140
|
+
(element for element in root.iter() if isinstance(element.tag, str) and local_name(element) == "body"),
|
|
141
|
+
None,
|
|
142
|
+
)
|
|
143
|
+
if body is None:
|
|
144
|
+
body = etree.Element("body")
|
|
145
|
+
body.append(deepcopy(root))
|
|
146
|
+
root = etree.Element("html")
|
|
147
|
+
root.append(body)
|
|
148
|
+
return HtmlDocument(
|
|
149
|
+
root=root,
|
|
150
|
+
body=body,
|
|
151
|
+
stylesheets=tuple(stylesheets),
|
|
152
|
+
base_href=base_href,
|
|
153
|
+
title=title,
|
|
154
|
+
open_graph_title=open_graph_title,
|
|
155
|
+
site_name=site_name,
|
|
156
|
+
source_context=context,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _normalize_transport_encoding(value: str | None) -> str | None:
|
|
161
|
+
"""把 HTTP 声明编码规范化为 lxml 可用名称,未知标签继续走文档内探测。"""
|
|
162
|
+
if not value:
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return codecs.lookup(value).name
|
|
166
|
+
except LookupError:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _has_html_encoding_declaration(file_bytes: bytes) -> bool:
|
|
171
|
+
"""只承认首个 4 KiB 内由 HTML 语法解析出的真实编码声明。"""
|
|
172
|
+
prefix = file_bytes[:4096]
|
|
173
|
+
if not _HTML_ENCODING_DECLARATION_CANDIDATE_RE.search(prefix):
|
|
174
|
+
return False
|
|
175
|
+
if _XML_ENCODING_DECLARATION_RE.match(prefix):
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
declaration_parser = lxml_html.HTMLParser(
|
|
179
|
+
recover=True,
|
|
180
|
+
no_network=True,
|
|
181
|
+
remove_comments=False,
|
|
182
|
+
huge_tree=False,
|
|
183
|
+
encoding="iso-8859-1",
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
root = lxml_html.document_fromstring(prefix, parser=declaration_parser)
|
|
187
|
+
except (etree.ParserError, etree.XMLSyntaxError, UnicodeError, ValueError):
|
|
188
|
+
return False
|
|
189
|
+
for element in root.iter():
|
|
190
|
+
if not isinstance(element.tag, str) or local_name(element) != "meta":
|
|
191
|
+
continue
|
|
192
|
+
if (element.get("charset") or "").strip():
|
|
193
|
+
return True
|
|
194
|
+
http_equiv = (element.get("http-equiv") or "").strip().casefold()
|
|
195
|
+
if http_equiv == "content-type" and _META_CONTENT_CHARSET_RE.search(element.get("content") or ""):
|
|
196
|
+
return True
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _html_parser_input(file_bytes: bytes, *, transport_encoding: str | None = None) -> bytes | str:
|
|
201
|
+
"""无显式编码且符合 UTF-8 时先解码,避免 lxml 按单字节旧编码解释正文。"""
|
|
202
|
+
if transport_encoding is not None:
|
|
203
|
+
return file_bytes
|
|
204
|
+
if file_bytes.startswith(_UNICODE_BOMS) or _has_html_encoding_declaration(file_bytes):
|
|
205
|
+
return file_bytes
|
|
206
|
+
try:
|
|
207
|
+
return file_bytes.decode("utf-8")
|
|
208
|
+
except UnicodeDecodeError:
|
|
209
|
+
return file_bytes
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _validate_dom_shape(root: etree._Element) -> None:
|
|
213
|
+
"""迭代校验 DOM 节点数和最大深度,避免深层递归继续传播。"""
|
|
214
|
+
node_count = 0
|
|
215
|
+
stack: list[tuple[etree._Element, int]] = [(root, 1)]
|
|
216
|
+
while stack:
|
|
217
|
+
node, depth = stack.pop()
|
|
218
|
+
node_count += 1
|
|
219
|
+
if node_count > MAX_HTML_NODES:
|
|
220
|
+
raise HtmlResourceLimitError(f"HTML resource limit exceeded: max_html_nodes={MAX_HTML_NODES}")
|
|
221
|
+
if not isinstance(node.tag, str):
|
|
222
|
+
continue
|
|
223
|
+
if depth > MAX_HTML_DEPTH:
|
|
224
|
+
raise HtmlResourceLimitError(f"HTML resource limit exceeded: max_html_depth={MAX_HTML_DEPTH}")
|
|
225
|
+
stack.extend((child, depth + 1) for child in node)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _meta_content(root: etree._Element, *, property_name: str) -> str | None:
|
|
229
|
+
"""返回首个匹配 property/name 的非空 meta content。"""
|
|
230
|
+
target = property_name.casefold()
|
|
231
|
+
for element in root.iter():
|
|
232
|
+
if not isinstance(element.tag, str) or local_name(element) != "meta":
|
|
233
|
+
continue
|
|
234
|
+
name = (element.get("property") or element.get("name") or "").strip().casefold()
|
|
235
|
+
content = (element.get("content") or "").strip()
|
|
236
|
+
if name == target and content:
|
|
237
|
+
return content
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _collapsed_text(element: etree._Element) -> str:
|
|
242
|
+
"""折叠元素纯文本中的 HTML 排版空白。"""
|
|
243
|
+
return re.sub(r"\s+", " ", "".join(element.itertext())).strip()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _has_discarded_active_ancestor(element: etree._Element) -> bool:
|
|
247
|
+
"""判断元素是否位于稍后会整棵删除的活动内容祖先中。"""
|
|
248
|
+
return any(isinstance(ancestor.tag, str) and local_name(ancestor) in _ACTIVE_TAGS for ancestor in element.iterancestors())
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _normalize_formula_sources(root: etree._Element) -> None:
|
|
252
|
+
"""按共享优先级把成功来源收敛为携带裸 LaTeX 的静态 math 元素。"""
|
|
253
|
+
_preserve_asciimath_text(root)
|
|
254
|
+
for element in list(root.iter()):
|
|
255
|
+
if not isinstance(element.tag, str) or not _is_attached(root, element):
|
|
256
|
+
continue
|
|
257
|
+
classes = frozenset((element.get("class") or "").casefold().split())
|
|
258
|
+
is_candidate = _is_formula_carrier(element) or (
|
|
259
|
+
bool(classes & _GENERIC_FORMULA_CLASS_TOKENS) and _formula_wrapper_contains_only_carrier(element)
|
|
260
|
+
)
|
|
261
|
+
if not is_candidate:
|
|
262
|
+
continue
|
|
263
|
+
if formula := extract_formula(element):
|
|
264
|
+
_replace_with_formula(element, formula)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _is_formula_carrier(element: etree._Element) -> bool:
|
|
268
|
+
"""判断元素自身是否携带公式来源,而不是仅从任意后代继承。"""
|
|
269
|
+
if local_name(element) == "math" or is_tex_script(element):
|
|
270
|
+
return True
|
|
271
|
+
if any((element.get(attribute) or "").strip() for attribute in ("data-docvortex-latex", "data-tex", "data-expr")):
|
|
272
|
+
return True
|
|
273
|
+
classes = frozenset((element.get("class") or "").casefold().split())
|
|
274
|
+
return bool(classes & _FORMULA_GENERATOR_CLASS_TOKENS)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _formula_wrapper_contains_only_carrier(element: etree._Element) -> bool:
|
|
278
|
+
"""仅允许恰好一个 carrier 且其外没有可见文本或媒体的通用 wrapper 整体折叠。"""
|
|
279
|
+
carrier: etree._Element | None = None
|
|
280
|
+
for candidate in element.iterdescendants():
|
|
281
|
+
if not isinstance(candidate.tag, str) or not _is_formula_carrier(candidate):
|
|
282
|
+
continue
|
|
283
|
+
if carrier is None:
|
|
284
|
+
carrier = candidate
|
|
285
|
+
continue
|
|
286
|
+
if any(ancestor is carrier for ancestor in candidate.iterancestors()):
|
|
287
|
+
continue
|
|
288
|
+
return False
|
|
289
|
+
if carrier is None:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
def inside_carrier(candidate: etree._Element | None) -> bool:
|
|
293
|
+
"""判断节点正文是否位于唯一 carrier 子树内。"""
|
|
294
|
+
return candidate is not None and (
|
|
295
|
+
candidate is carrier or any(ancestor is carrier for ancestor in candidate.iterancestors())
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
outside_text = [element.text or ""]
|
|
299
|
+
for candidate in element.iterdescendants():
|
|
300
|
+
if not isinstance(candidate.tag, str):
|
|
301
|
+
if not inside_carrier(candidate.getparent()):
|
|
302
|
+
outside_text.append(candidate.tail or "")
|
|
303
|
+
continue
|
|
304
|
+
if not inside_carrier(candidate):
|
|
305
|
+
outside_text.append(candidate.text or "")
|
|
306
|
+
if local_name(candidate) in _MEANINGFUL_FORMULA_SIBLING_TAGS:
|
|
307
|
+
return False
|
|
308
|
+
if not inside_carrier(candidate.getparent()):
|
|
309
|
+
outside_text.append(candidate.tail or "")
|
|
310
|
+
return not any(value.strip() for value in outside_text)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _preserve_asciimath_text(root: etree._Element) -> None:
|
|
314
|
+
"""把暂不支持的 AsciiMath script 转为可见静态文本,避免活动内容清理时丢失。"""
|
|
315
|
+
for element in list(root.iter()):
|
|
316
|
+
if not isinstance(element.tag, str) or local_name(element) != "script":
|
|
317
|
+
continue
|
|
318
|
+
script_type = (element.get("type") or "").strip()
|
|
319
|
+
if _ASCIIMATH_SCRIPT_TYPE_RE.fullmatch(script_type) is None:
|
|
320
|
+
continue
|
|
321
|
+
value = "".join(element.itertext()).strip()
|
|
322
|
+
if not value:
|
|
323
|
+
continue
|
|
324
|
+
parent = element.getparent()
|
|
325
|
+
if parent is None:
|
|
326
|
+
continue
|
|
327
|
+
replacement = etree.Element("span")
|
|
328
|
+
replacement.set("class", "docvortex-formula-fallback")
|
|
329
|
+
replacement.text = value
|
|
330
|
+
replacement.tail = element.tail
|
|
331
|
+
parent.replace(element, replacement)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _replace_with_formula(element: etree._Element, formula: FormulaExtraction) -> None:
|
|
335
|
+
"""用携带规范 LaTeX 的安全 math 占位替换一个网页公式节点。"""
|
|
336
|
+
parent = element.getparent()
|
|
337
|
+
if parent is None:
|
|
338
|
+
return
|
|
339
|
+
replacement = etree.Element("math")
|
|
340
|
+
replacement.set("data-docvortex-latex", formula.latex)
|
|
341
|
+
replacement.set("data-formula-display", formula.display)
|
|
342
|
+
for attribute in _FORMULA_VISIBILITY_ATTRIBUTES:
|
|
343
|
+
if (value := element.get(attribute)) is not None:
|
|
344
|
+
replacement.set(attribute, value)
|
|
345
|
+
if formula.display == "block":
|
|
346
|
+
replacement.set("display", "block")
|
|
347
|
+
if (element.get("data-block-type") or "").strip() == "equation":
|
|
348
|
+
replacement.set("data-block-type", "equation")
|
|
349
|
+
replacement.tail = element.tail
|
|
350
|
+
parent.replace(element, replacement)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _is_attached(root: etree._Element, element: etree._Element) -> bool:
|
|
354
|
+
"""判断预扫描元素是否仍属于当前 DOM,跳过已被外层公式替换的旧后代。"""
|
|
355
|
+
return element is root or any(ancestor is root for ancestor in element.iterancestors())
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _remove_active_content(root: etree._Element) -> None:
|
|
359
|
+
"""删除活动内容并把 noscript 静态回退转换为普通容器。"""
|
|
360
|
+
for element in list(root.iter()):
|
|
361
|
+
if isinstance(element, etree._Comment):
|
|
362
|
+
_drop_tree_preserve_tail(element)
|
|
363
|
+
continue
|
|
364
|
+
if not isinstance(element.tag, str):
|
|
365
|
+
continue
|
|
366
|
+
name = local_name(element)
|
|
367
|
+
if name == "noscript":
|
|
368
|
+
element.tag = "div"
|
|
369
|
+
continue
|
|
370
|
+
if name in _ACTIVE_TAGS:
|
|
371
|
+
_drop_tree_preserve_tail(element)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _drop_tree_preserve_tail(element: etree._Element) -> None:
|
|
375
|
+
"""删除节点整棵子树,同时把 tail 归还给相邻文本位置。"""
|
|
376
|
+
parent = element.getparent()
|
|
377
|
+
if parent is None:
|
|
378
|
+
return
|
|
379
|
+
tail = element.tail or ""
|
|
380
|
+
previous = element.getprevious()
|
|
381
|
+
if tail:
|
|
382
|
+
if previous is not None:
|
|
383
|
+
previous.tail = (previous.tail or "") + tail
|
|
384
|
+
else:
|
|
385
|
+
parent.text = (parent.text or "") + tail
|
|
386
|
+
parent.remove(element)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
__all__ = ["HtmlDocument", "HtmlStylesheetSource", "parse_html_document"]
|