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,365 @@
|
|
|
1
|
+
"""Standalone HTML 链接、图片与本地 stylesheet 的安全解析。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from urllib.parse import SplitResult, unquote, urljoin, urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
11
|
+
|
|
12
|
+
from ....foundation.image_payload import parse_image_data_uri_strict, validate_remote_image_url
|
|
13
|
+
from .._shared.hyperlink import sanitize_hyperlink_target
|
|
14
|
+
from docvortex.content.markup import ResolvedMarkupImage
|
|
15
|
+
from .constants import (
|
|
16
|
+
MAX_HTML_IMAGE_BYTES,
|
|
17
|
+
MAX_HTML_IMAGE_TOTAL_BYTES,
|
|
18
|
+
MAX_HTML_STYLESHEET_BYTES,
|
|
19
|
+
MAX_HTML_STYLESHEET_TOTAL_BYTES,
|
|
20
|
+
)
|
|
21
|
+
from docvortex.document.contracts import HtmlSourceContext
|
|
22
|
+
from .errors import HtmlResourceLimitError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_IMAGE_MIME_SIGNATURES: tuple[tuple[str, tuple[bytes, ...]], ...] = (
|
|
26
|
+
("image/jpeg", (b"\xff\xd8\xff",)),
|
|
27
|
+
("image/png", (b"\x89PNG\r\n\x1a\n",)),
|
|
28
|
+
("image/gif", (b"GIF87a", b"GIF89a")),
|
|
29
|
+
("image/webp", (b"RIFF",)),
|
|
30
|
+
("image/bmp", (b"BM",)),
|
|
31
|
+
("image/tiff", (b"II*\x00", b"MM\x00*")),
|
|
32
|
+
)
|
|
33
|
+
_SAME_DOCUMENT_SCHEMES = frozenset({"file", "http", "https"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HtmlAnchorResolver(Protocol):
|
|
37
|
+
"""定义资源解析与共享 projector 所需的文档内 anchor 查询。"""
|
|
38
|
+
|
|
39
|
+
def resolve_fragment(self, fragment: str) -> str | None:
|
|
40
|
+
"""把源 fragment 转为统一内部链接。"""
|
|
41
|
+
|
|
42
|
+
def heading_anchor(self, heading: etree._Element) -> str | None:
|
|
43
|
+
"""返回标题节点的统一 anchor。"""
|
|
44
|
+
|
|
45
|
+
def heading_label(self, anchor: str) -> str | None:
|
|
46
|
+
"""返回统一 anchor 对应的标题文本。"""
|
|
47
|
+
|
|
48
|
+
def note_anchor(self, note: etree._Element) -> str | None:
|
|
49
|
+
"""返回脚注节点的统一 anchor。"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HtmlResourceContext:
|
|
53
|
+
"""实现共享 projector 所需的 HTML 来源、资源与 anchor 适配。"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
source_context: HtmlSourceContext,
|
|
58
|
+
*,
|
|
59
|
+
base_href: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""绑定来源上下文并计算不会逃逸本地根目录的 base。"""
|
|
62
|
+
self.source_context = source_context
|
|
63
|
+
self.base_href = base_href
|
|
64
|
+
self.anchors: HtmlAnchorResolver | None = None
|
|
65
|
+
self._image_bytes = 0
|
|
66
|
+
self._stylesheet_bytes = 0
|
|
67
|
+
self._image_cache: dict[Path, str] = {}
|
|
68
|
+
self._data_image_cache: dict[str, ResolvedMarkupImage] = {}
|
|
69
|
+
self._stylesheet_cache: dict[Path, str] = {}
|
|
70
|
+
self._local_root = source_context.local_resource_root.resolve() if source_context.local_resource_root else None
|
|
71
|
+
self._local_base = self._resolve_local_base()
|
|
72
|
+
self._remote_base = self._resolve_remote_base()
|
|
73
|
+
|
|
74
|
+
def bind_anchors(self, anchors: HtmlAnchorResolver) -> None:
|
|
75
|
+
"""在正文选择完成后绑定仅包含实际输出目标的 anchor 表。"""
|
|
76
|
+
self.anchors = anchors
|
|
77
|
+
|
|
78
|
+
def same_document_fragment(self, href: str) -> str | None:
|
|
79
|
+
"""解析纯 fragment 或与来源文档身份相同的相对、绝对 URL fragment。"""
|
|
80
|
+
normalized = sanitize_hyperlink_target(
|
|
81
|
+
href,
|
|
82
|
+
allowed_schemes=_SAME_DOCUMENT_SCHEMES,
|
|
83
|
+
allow_relative=True,
|
|
84
|
+
allow_fragment=True,
|
|
85
|
+
allow_root_relative=True,
|
|
86
|
+
)
|
|
87
|
+
if normalized is None:
|
|
88
|
+
return None
|
|
89
|
+
base_href = (self.base_href or "").strip()
|
|
90
|
+
if normalized.startswith("#") and (not base_href or base_href.startswith("#")):
|
|
91
|
+
fragment = unquote(normalized[1:]).strip()
|
|
92
|
+
return fragment or None
|
|
93
|
+
source_uri = (self.source_context.source_uri or "").strip()
|
|
94
|
+
if not source_uri:
|
|
95
|
+
return None
|
|
96
|
+
try:
|
|
97
|
+
source_parts = urlsplit(source_uri)
|
|
98
|
+
base_uri = urljoin(source_uri, base_href)
|
|
99
|
+
target_parts = urlsplit(urljoin(base_uri, normalized))
|
|
100
|
+
except ValueError:
|
|
101
|
+
return None
|
|
102
|
+
fragment = unquote(target_parts.fragment).strip()
|
|
103
|
+
if not fragment or _document_url_identity(target_parts) != _document_url_identity(source_parts):
|
|
104
|
+
return None
|
|
105
|
+
return fragment
|
|
106
|
+
|
|
107
|
+
def resolve_link(self, href: str) -> str | None:
|
|
108
|
+
"""解析安全外链、相对链接或实际存在的文档内 fragment。"""
|
|
109
|
+
candidate = (href or "").strip()
|
|
110
|
+
if self._remote_base and candidate.startswith("//"):
|
|
111
|
+
try:
|
|
112
|
+
candidate = urljoin(self._remote_base, candidate)
|
|
113
|
+
except ValueError:
|
|
114
|
+
return None
|
|
115
|
+
if self.anchors is not None and (fragment := self.same_document_fragment(candidate)) is not None:
|
|
116
|
+
if internal := self.anchors.resolve_fragment(fragment):
|
|
117
|
+
return internal
|
|
118
|
+
normalized = sanitize_hyperlink_target(
|
|
119
|
+
candidate,
|
|
120
|
+
allow_relative=True,
|
|
121
|
+
allow_fragment=True,
|
|
122
|
+
allow_root_relative=True,
|
|
123
|
+
)
|
|
124
|
+
if normalized is None:
|
|
125
|
+
return None
|
|
126
|
+
if normalized.startswith("#"):
|
|
127
|
+
base_href = (self.base_href or "").strip()
|
|
128
|
+
if base_href:
|
|
129
|
+
source_uri = (self.source_context.source_uri or "").strip()
|
|
130
|
+
try:
|
|
131
|
+
target = urljoin(urljoin(source_uri, base_href), normalized)
|
|
132
|
+
except ValueError:
|
|
133
|
+
return None
|
|
134
|
+
return sanitize_hyperlink_target(
|
|
135
|
+
target,
|
|
136
|
+
allow_relative=True,
|
|
137
|
+
allow_fragment=True,
|
|
138
|
+
allow_root_relative=True,
|
|
139
|
+
)
|
|
140
|
+
return self.anchors.resolve_fragment(normalized) if self.anchors else None
|
|
141
|
+
parsed = urlsplit(normalized)
|
|
142
|
+
if parsed.scheme:
|
|
143
|
+
return normalized
|
|
144
|
+
if self._remote_base:
|
|
145
|
+
resolved = urljoin(self._remote_base, normalized)
|
|
146
|
+
return sanitize_hyperlink_target(resolved)
|
|
147
|
+
if self._local_root is not None and self._local_base is not None and parsed.path and not parsed.path.startswith("/"):
|
|
148
|
+
local_path = self._resolve_local_path(normalized)
|
|
149
|
+
if local_path is None:
|
|
150
|
+
return None
|
|
151
|
+
relative_path = local_path.relative_to(self._local_root).as_posix()
|
|
152
|
+
return sanitize_hyperlink_target(
|
|
153
|
+
urlunsplit(("", "", relative_path, parsed.query, parsed.fragment)),
|
|
154
|
+
allow_relative=True,
|
|
155
|
+
allow_fragment=True,
|
|
156
|
+
)
|
|
157
|
+
return sanitize_hyperlink_target(normalized, allow_relative=True, allow_fragment=True)
|
|
158
|
+
|
|
159
|
+
def resolve_image(self, source: str, *, alt: str = "") -> ResolvedMarkupImage | None:
|
|
160
|
+
"""按 data URI、本地安全文件、远程 URL 的顺序解析图片。"""
|
|
161
|
+
normalized = (source or "").strip()
|
|
162
|
+
if not normalized:
|
|
163
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
164
|
+
if normalized.casefold().startswith("data:"):
|
|
165
|
+
return self._resolve_data_image(normalized, alt=alt)
|
|
166
|
+
if remote_url := self._resolve_remote_image_url(normalized):
|
|
167
|
+
return ResolvedMarkupImage(image_url=remote_url, alt=alt)
|
|
168
|
+
local_path = self._resolve_local_path(normalized)
|
|
169
|
+
if local_path is None or not local_path.is_file():
|
|
170
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
171
|
+
cached = self._image_cache.get(local_path)
|
|
172
|
+
if cached is not None:
|
|
173
|
+
return ResolvedMarkupImage(image_base64=cached, alt=alt)
|
|
174
|
+
with local_path.open("rb") as source_file:
|
|
175
|
+
payload = source_file.read(MAX_HTML_IMAGE_BYTES + 1)
|
|
176
|
+
if len(payload) > MAX_HTML_IMAGE_BYTES:
|
|
177
|
+
raise HtmlResourceLimitError(f"HTML image exceeds max_html_image_bytes={MAX_HTML_IMAGE_BYTES}")
|
|
178
|
+
data_uri = _image_data_uri(payload)
|
|
179
|
+
if data_uri is None:
|
|
180
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
181
|
+
self._charge_image_bytes(len(payload))
|
|
182
|
+
self._image_cache[local_path] = data_uri
|
|
183
|
+
return ResolvedMarkupImage(image_base64=data_uri, alt=alt)
|
|
184
|
+
|
|
185
|
+
def load_stylesheet(self, href: str) -> str | None:
|
|
186
|
+
"""只读取安全本地根目录内的 stylesheet,远程 CSS 始终忽略。"""
|
|
187
|
+
path = self._resolve_local_path(href)
|
|
188
|
+
if path is None or not path.is_file():
|
|
189
|
+
return None
|
|
190
|
+
cached = self._stylesheet_cache.get(path)
|
|
191
|
+
if cached is not None:
|
|
192
|
+
return cached
|
|
193
|
+
with path.open("rb") as source_file:
|
|
194
|
+
payload = source_file.read(MAX_HTML_STYLESHEET_BYTES + 1)
|
|
195
|
+
self._charge_stylesheet_bytes(len(payload))
|
|
196
|
+
stylesheet = payload.decode("utf-8-sig", errors="replace")
|
|
197
|
+
self._stylesheet_cache[path] = stylesheet
|
|
198
|
+
return stylesheet
|
|
199
|
+
|
|
200
|
+
def charge_inline_stylesheet(self, stylesheet: str) -> None:
|
|
201
|
+
"""把一段内联 CSS 的 UTF-8 字节数计入统一 stylesheet 预算。"""
|
|
202
|
+
self._charge_stylesheet_bytes(len(stylesheet.encode("utf-8")))
|
|
203
|
+
|
|
204
|
+
def _charge_stylesheet_bytes(self, byte_count: int) -> None:
|
|
205
|
+
"""执行单份和整文档 stylesheet 字节限制,并只在校验通过后累计。"""
|
|
206
|
+
if byte_count > MAX_HTML_STYLESHEET_BYTES:
|
|
207
|
+
raise HtmlResourceLimitError(f"HTML stylesheet exceeds max_html_stylesheet_bytes={MAX_HTML_STYLESHEET_BYTES}")
|
|
208
|
+
total_bytes = self._stylesheet_bytes + byte_count
|
|
209
|
+
if total_bytes > MAX_HTML_STYLESHEET_TOTAL_BYTES:
|
|
210
|
+
raise HtmlResourceLimitError(
|
|
211
|
+
f"HTML stylesheets exceed max_html_stylesheet_total_bytes={MAX_HTML_STYLESHEET_TOTAL_BYTES}"
|
|
212
|
+
)
|
|
213
|
+
self._stylesheet_bytes = total_bytes
|
|
214
|
+
|
|
215
|
+
def heading_anchor(self, heading: etree._Element) -> str | None:
|
|
216
|
+
"""把标题 anchor 查询委托给已绑定的注册表。"""
|
|
217
|
+
return self.anchors.heading_anchor(heading) if self.anchors else None
|
|
218
|
+
|
|
219
|
+
def heading_label(self, anchor: str) -> str | None:
|
|
220
|
+
"""把标题标签查询委托给已绑定的注册表。"""
|
|
221
|
+
return self.anchors.heading_label(anchor) if self.anchors else None
|
|
222
|
+
|
|
223
|
+
def note_anchor(self, note: etree._Element) -> str | None:
|
|
224
|
+
"""把脚注 anchor 查询委托给已绑定的注册表。"""
|
|
225
|
+
return self.anchors.note_anchor(note) if self.anchors else None
|
|
226
|
+
|
|
227
|
+
def _resolve_data_image(self, data_uri: str, *, alt: str) -> ResolvedMarkupImage | None:
|
|
228
|
+
"""严格解析 data URI,并执行单图与累计图片预算。"""
|
|
229
|
+
if cached := self._data_image_cache.get(data_uri):
|
|
230
|
+
return ResolvedMarkupImage(image_base64=cached.image_base64, alt=alt)
|
|
231
|
+
if len(data_uri) > MAX_HTML_IMAGE_BYTES * 2:
|
|
232
|
+
raise HtmlResourceLimitError(f"HTML image exceeds max_html_image_bytes={MAX_HTML_IMAGE_BYTES}")
|
|
233
|
+
try:
|
|
234
|
+
payload, extension = parse_image_data_uri_strict(data_uri)
|
|
235
|
+
except ValueError:
|
|
236
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
237
|
+
if extension == "svg":
|
|
238
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
239
|
+
if len(payload) > MAX_HTML_IMAGE_BYTES:
|
|
240
|
+
raise HtmlResourceLimitError(f"HTML image exceeds max_html_image_bytes={MAX_HTML_IMAGE_BYTES}")
|
|
241
|
+
self._charge_image_bytes(len(payload))
|
|
242
|
+
resolved = ResolvedMarkupImage(image_base64=data_uri, alt=alt)
|
|
243
|
+
self._data_image_cache[data_uri] = resolved
|
|
244
|
+
return resolved
|
|
245
|
+
|
|
246
|
+
def _charge_image_bytes(self, byte_count: int) -> None:
|
|
247
|
+
"""累计实际保留的图片字节,并在超限时终止整份文档。"""
|
|
248
|
+
self._image_bytes += byte_count
|
|
249
|
+
if self._image_bytes > MAX_HTML_IMAGE_TOTAL_BYTES:
|
|
250
|
+
raise HtmlResourceLimitError(f"HTML images exceed max_html_image_total_bytes={MAX_HTML_IMAGE_TOTAL_BYTES}")
|
|
251
|
+
|
|
252
|
+
def _resolve_remote_image_url(self, source: str) -> str | None:
|
|
253
|
+
"""把远程或远程来源相对图片解析为受限 HTTP(S) 绝对 URL。"""
|
|
254
|
+
try:
|
|
255
|
+
parsed = urlsplit(source)
|
|
256
|
+
except ValueError:
|
|
257
|
+
return None
|
|
258
|
+
if parsed.scheme:
|
|
259
|
+
try:
|
|
260
|
+
return validate_remote_image_url(source)
|
|
261
|
+
except ValueError:
|
|
262
|
+
return None
|
|
263
|
+
if not self._remote_base:
|
|
264
|
+
return None
|
|
265
|
+
try:
|
|
266
|
+
return validate_remote_image_url(urljoin(self._remote_base, source))
|
|
267
|
+
except ValueError:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _resolve_remote_base(self) -> str | None:
|
|
271
|
+
"""返回来源 URI 与 base href 合成后的 HTTP(S) 资源基址。"""
|
|
272
|
+
source_uri = (self.source_context.source_uri or "").strip()
|
|
273
|
+
base_href = (self.base_href or "").strip()
|
|
274
|
+
try:
|
|
275
|
+
base_scheme = urlsplit(base_href).scheme.casefold() if base_href else ""
|
|
276
|
+
source_scheme = urlsplit(source_uri).scheme.casefold() if source_uri else ""
|
|
277
|
+
except ValueError:
|
|
278
|
+
return None
|
|
279
|
+
if base_href and base_scheme in {"http", "https"}:
|
|
280
|
+
base = base_href
|
|
281
|
+
elif source_uri and source_scheme in {"http", "https"}:
|
|
282
|
+
base = urljoin(source_uri, base_href) if base_href else source_uri
|
|
283
|
+
else:
|
|
284
|
+
return None
|
|
285
|
+
try:
|
|
286
|
+
parsed = urlsplit(base)
|
|
287
|
+
except ValueError:
|
|
288
|
+
return None
|
|
289
|
+
return base if parsed.scheme.casefold() in {"http", "https"} and parsed.hostname else None
|
|
290
|
+
|
|
291
|
+
def _resolve_local_base(self) -> Path | None:
|
|
292
|
+
"""按本地安全根与相对 base href 计算资源起始目录。"""
|
|
293
|
+
root = self._local_root
|
|
294
|
+
if root is None:
|
|
295
|
+
return None
|
|
296
|
+
raw_base = (self.base_href or "").strip()
|
|
297
|
+
if not raw_base:
|
|
298
|
+
return root
|
|
299
|
+
try:
|
|
300
|
+
parsed = urlsplit(raw_base)
|
|
301
|
+
except ValueError:
|
|
302
|
+
return root
|
|
303
|
+
if parsed.scheme or parsed.netloc or parsed.path.startswith(("/", "\\")) or "\\" in parsed.path:
|
|
304
|
+
return root
|
|
305
|
+
decoded = unquote(parsed.path)
|
|
306
|
+
if not decoded:
|
|
307
|
+
return root
|
|
308
|
+
candidate = (root / decoded).resolve()
|
|
309
|
+
base = candidate if raw_base.endswith("/") else candidate.parent
|
|
310
|
+
return base if _is_within(base, root) else root
|
|
311
|
+
|
|
312
|
+
def _resolve_local_path(self, source: str) -> Path | None:
|
|
313
|
+
"""解析本地相对资源,并在 resolve 后再次校验根目录边界。"""
|
|
314
|
+
root = self._local_root
|
|
315
|
+
base = self._local_base
|
|
316
|
+
if root is None or base is None:
|
|
317
|
+
return None
|
|
318
|
+
try:
|
|
319
|
+
parsed = urlsplit(source)
|
|
320
|
+
except ValueError:
|
|
321
|
+
return None
|
|
322
|
+
if parsed.scheme or parsed.netloc or parsed.path.startswith(("/", "\\")) or "\\" in parsed.path:
|
|
323
|
+
return None
|
|
324
|
+
decoded = unquote(parsed.path)
|
|
325
|
+
if not decoded or "\x00" in decoded or ".." in Path(decoded).parts:
|
|
326
|
+
return None
|
|
327
|
+
candidate = (base / decoded).resolve()
|
|
328
|
+
return candidate if _is_within(candidate, root) else None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _image_data_uri(payload: bytes) -> str | None:
|
|
332
|
+
"""按文件签名构造并复核受支持栅格图片的 data URI。"""
|
|
333
|
+
mime = next(
|
|
334
|
+
(
|
|
335
|
+
media_type
|
|
336
|
+
for media_type, signatures in _IMAGE_MIME_SIGNATURES
|
|
337
|
+
if any(payload.startswith(signature) for signature in signatures)
|
|
338
|
+
),
|
|
339
|
+
None,
|
|
340
|
+
)
|
|
341
|
+
if mime == "image/webp" and (len(payload) < 12 or payload[8:12] != b"WEBP"):
|
|
342
|
+
mime = None
|
|
343
|
+
if mime is None:
|
|
344
|
+
return None
|
|
345
|
+
data_uri = f"data:{mime};base64,{base64.b64encode(payload).decode('ascii')}"
|
|
346
|
+
try:
|
|
347
|
+
parse_image_data_uri_strict(data_uri)
|
|
348
|
+
except ValueError:
|
|
349
|
+
return None
|
|
350
|
+
return data_uri
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _document_url_identity(parts: SplitResult) -> tuple[str, str, str, str]:
|
|
354
|
+
"""返回忽略 fragment 的确定文档身份,并规范层级 URL 的空路径。"""
|
|
355
|
+
scheme = parts.scheme.casefold()
|
|
356
|
+
path = parts.path or ("/" if scheme in {"http", "https"} else "")
|
|
357
|
+
return scheme, parts.netloc.casefold(), unquote(path), parts.query
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _is_within(path: Path, root: Path) -> bool:
|
|
361
|
+
"""判断已解析路径是否等于安全根或位于其内部。"""
|
|
362
|
+
return path == root or root in path.parents
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
__all__ = ["HtmlAnchorResolver", "HtmlResourceContext"]
|