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,306 @@
|
|
|
1
|
+
"""把 EPUB XHTML/SVG 内容文档转换为 DocVortex raw blocks。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
from ....foundation.image_payload import parse_image_data_uri_strict
|
|
11
|
+
from .._shared.hyperlink import sanitize_hyperlink_target
|
|
12
|
+
from docvortex.content.markup import (
|
|
13
|
+
MarkupAnchorDocument,
|
|
14
|
+
MarkupAnchorRegistry,
|
|
15
|
+
MarkupProjector,
|
|
16
|
+
MarkupStylesheet,
|
|
17
|
+
ResolvedMarkupImage,
|
|
18
|
+
element_id,
|
|
19
|
+
visible_element_text,
|
|
20
|
+
)
|
|
21
|
+
from docvortex.content.markup.projector import (
|
|
22
|
+
BLOCK_TAGS as _BLOCK_TAGS,
|
|
23
|
+
SKIPPED_TAGS as _SKIPPED_TAGS,
|
|
24
|
+
clean_text_node as _clean_text_node,
|
|
25
|
+
entity_text as _entity_text,
|
|
26
|
+
local_name as _local_name,
|
|
27
|
+
)
|
|
28
|
+
from .constants import IMAGE_MEDIA_BY_EXTENSION, SVG_MEDIA_TYPE
|
|
29
|
+
from .package import EpubPackage
|
|
30
|
+
|
|
31
|
+
_INDIVIDUAL_NOTE_TYPE_ORDER = ("footnote", "endnote", "rearnote")
|
|
32
|
+
_INDIVIDUAL_NOTE_ROLE_ORDER = ("doc-footnote", "doc-endnote")
|
|
33
|
+
_INDIVIDUAL_NOTE_TYPES = frozenset(_INDIVIDUAL_NOTE_TYPE_ORDER)
|
|
34
|
+
_INDIVIDUAL_NOTE_ROLES = frozenset(_INDIVIDUAL_NOTE_ROLE_ORDER)
|
|
35
|
+
_NOTE_BLOCK_TAGS = _BLOCK_TAGS | {"li"}
|
|
36
|
+
_NOTE_NON_TEXT_SUBTREES = frozenset(
|
|
37
|
+
{
|
|
38
|
+
"figure",
|
|
39
|
+
"h1",
|
|
40
|
+
"h2",
|
|
41
|
+
"h3",
|
|
42
|
+
"h4",
|
|
43
|
+
"h5",
|
|
44
|
+
"h6",
|
|
45
|
+
"math",
|
|
46
|
+
"ol",
|
|
47
|
+
"pre",
|
|
48
|
+
"svg",
|
|
49
|
+
"table",
|
|
50
|
+
"ul",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _epub_types(element: etree._Element) -> frozenset[str]:
|
|
56
|
+
"""读取 EPUB 命名空间或未命名 type 属性中的结构语义 token。"""
|
|
57
|
+
values: list[str] = []
|
|
58
|
+
for name, value in element.attrib.items():
|
|
59
|
+
local_name = etree.QName(name).localname if name.startswith("{") else name.split(":", 1)[-1]
|
|
60
|
+
if local_name == "type":
|
|
61
|
+
values.extend(value.casefold().split())
|
|
62
|
+
return frozenset(values)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _roles(element: etree._Element) -> frozenset[str]:
|
|
66
|
+
"""读取 ARIA role 属性中的小写语义 token。"""
|
|
67
|
+
return frozenset((element.get("role") or "").casefold().split())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_individual_note(element: etree._Element) -> bool:
|
|
71
|
+
"""判断块级元素是否表示单条 EPUB Footnote/Endnote。"""
|
|
72
|
+
if _local_name(element) not in _NOTE_BLOCK_TAGS:
|
|
73
|
+
return False
|
|
74
|
+
return bool(_epub_types(element) & _INDIVIDUAL_NOTE_TYPES or _roles(element) & _INDIVIDUAL_NOTE_ROLES)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _note_semantic(element: etree._Element) -> str:
|
|
78
|
+
"""按固定优先级返回 note 的 EPUB type 或 ARIA role。"""
|
|
79
|
+
epub_types = _epub_types(element)
|
|
80
|
+
for note_type in _INDIVIDUAL_NOTE_TYPE_ORDER:
|
|
81
|
+
if note_type in epub_types:
|
|
82
|
+
return note_type
|
|
83
|
+
roles = _roles(element)
|
|
84
|
+
for role in _INDIVIDUAL_NOTE_ROLE_ORDER:
|
|
85
|
+
if role in roles:
|
|
86
|
+
return role
|
|
87
|
+
return "note"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _note_has_text_block(element: etree._Element) -> bool:
|
|
91
|
+
"""判断 note 是否能产生非空 text,从而避免注册没有正文目标的 anchor。"""
|
|
92
|
+
if _clean_text_node(element.text).strip():
|
|
93
|
+
return True
|
|
94
|
+
for child in element:
|
|
95
|
+
if child.tail and _clean_text_node(child.tail).strip():
|
|
96
|
+
return True
|
|
97
|
+
if not isinstance(child.tag, str):
|
|
98
|
+
if _entity_text(child):
|
|
99
|
+
return True
|
|
100
|
+
continue
|
|
101
|
+
name = _local_name(child)
|
|
102
|
+
if name in _SKIPPED_TAGS or name in _NOTE_NON_TEXT_SUBTREES or name in {"img", "image"}:
|
|
103
|
+
continue
|
|
104
|
+
if _note_has_text_block(child):
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_chapter_stylesheet(package: EpubPackage, chapter_path: str, root: etree._Element) -> MarkupStylesheet:
|
|
110
|
+
"""按章节 head 顺序加载包内 CSS 与内联 style。"""
|
|
111
|
+
stylesheet = MarkupStylesheet()
|
|
112
|
+
for element in root.iter():
|
|
113
|
+
if not isinstance(element.tag, str):
|
|
114
|
+
continue
|
|
115
|
+
name = _local_name(element)
|
|
116
|
+
if name == "link" and "stylesheet" in (element.get("rel") or "").casefold().split():
|
|
117
|
+
target = package.resolve_reference(element.get("href") or "", base_part=chapter_path)
|
|
118
|
+
if target is None:
|
|
119
|
+
continue
|
|
120
|
+
data = package.read_part(target.path)
|
|
121
|
+
if data is not None:
|
|
122
|
+
stylesheet.add(data.decode("utf-8-sig", errors="replace"))
|
|
123
|
+
elif name == "style":
|
|
124
|
+
stylesheet.add("".join(element.itertext()))
|
|
125
|
+
return stylesheet
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _EpubAnchorPolicy:
|
|
129
|
+
"""保持 EPUB 标题、脚注 identity 与可落地性判定的既有契约。"""
|
|
130
|
+
|
|
131
|
+
anchor_prefix = "epub"
|
|
132
|
+
register_document_start = True
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def heading_identity(element: etree._Element, ordinal: int) -> str:
|
|
136
|
+
"""按源 ID 或匿名标题序号生成 EPUB 标题 identity。"""
|
|
137
|
+
return f"{element_id(element) or 'heading'}-{ordinal}"
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def is_materializable_note(element: etree._Element, document: MarkupAnchorDocument) -> bool:
|
|
141
|
+
"""沿用 EPUB note 语义、文本块能力和最终可见性判断。"""
|
|
142
|
+
return _is_individual_note(element) and _note_has_text_block(element) and bool(visible_element_text(element, document))
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def note_identity(element: etree._Element, ordinal: int) -> str:
|
|
146
|
+
"""按 note 类型、源 ID 与章节内序号生成 EPUB 脚注 identity。"""
|
|
147
|
+
source_id = element_id(element)
|
|
148
|
+
return f"note-{_note_semantic(element)}-{source_id or 'anonymous'}-{ordinal}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class EpubAnchorRegistry:
|
|
152
|
+
"""建立章节路径、标题与 note fragment 到实际 canonical anchor 的别名表。"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, chapters: list[tuple[str, etree._Element]], package: EpubPackage) -> None:
|
|
155
|
+
"""预扫描全部选中 XHTML 章节,建立标题、note 与章节起点映射。"""
|
|
156
|
+
self._package = package
|
|
157
|
+
documents = [
|
|
158
|
+
MarkupAnchorDocument(
|
|
159
|
+
key=chapter_path,
|
|
160
|
+
root=root,
|
|
161
|
+
stylesheet=_load_chapter_stylesheet(package, chapter_path, root),
|
|
162
|
+
visibility_scope="nearest_body",
|
|
163
|
+
text_normalization="xhtml_whitespace",
|
|
164
|
+
)
|
|
165
|
+
for chapter_path, root in chapters
|
|
166
|
+
]
|
|
167
|
+
self._registry = MarkupAnchorRegistry(documents, _EpubAnchorPolicy())
|
|
168
|
+
|
|
169
|
+
def heading_anchor(self, heading: etree._Element) -> str | None:
|
|
170
|
+
"""返回一个已预扫描 EPUB 标题的规范 anchor。"""
|
|
171
|
+
return self._registry.heading_anchor(heading)
|
|
172
|
+
|
|
173
|
+
def heading_label(self, anchor: str) -> str | None:
|
|
174
|
+
"""返回规范 EPUB 标题 anchor 对应的可见标签。"""
|
|
175
|
+
return self._registry.heading_label(anchor)
|
|
176
|
+
|
|
177
|
+
def note_anchor(self, note: etree._Element) -> str | None:
|
|
178
|
+
"""返回一个已预扫描 EPUB Footnote/Endnote anchor。"""
|
|
179
|
+
return self._registry.note_anchor(note)
|
|
180
|
+
|
|
181
|
+
def resolve_anchor(self, href: str, *, base_part: str) -> str | None:
|
|
182
|
+
"""解析指向正文标题或 note 的 EPUB 包内链接,并返回不带井号的 anchor。"""
|
|
183
|
+
normalized = sanitize_hyperlink_target(
|
|
184
|
+
href,
|
|
185
|
+
allowed_schemes=(),
|
|
186
|
+
allow_relative=True,
|
|
187
|
+
allow_fragment=True,
|
|
188
|
+
)
|
|
189
|
+
if normalized is None:
|
|
190
|
+
return None
|
|
191
|
+
target = self._package.resolve_reference(normalized, base_part=base_part)
|
|
192
|
+
if target is None:
|
|
193
|
+
return None
|
|
194
|
+
return self._registry.resolve_target(target.path, target.fragment)
|
|
195
|
+
|
|
196
|
+
def resolve_link(self, href: str, *, base_part: str) -> str | None:
|
|
197
|
+
"""解析安全外部链接或指向已输出标题/note 的 EPUB 内部链接。"""
|
|
198
|
+
external = sanitize_hyperlink_target(href)
|
|
199
|
+
if external is not None:
|
|
200
|
+
return external
|
|
201
|
+
anchor = self.resolve_anchor(href, base_part=base_part)
|
|
202
|
+
return f"#{anchor}" if anchor else None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_anchor_registry(chapters: list[tuple[str, etree._Element]], package: EpubPackage) -> EpubAnchorRegistry:
|
|
206
|
+
"""从已解析章节元组建立跨章节锚点注册表。"""
|
|
207
|
+
return EpubAnchorRegistry(chapters, package)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True, slots=True)
|
|
211
|
+
class _EpubMarkupContext:
|
|
212
|
+
"""把 EPUB 包资源与文档级 anchor 适配到共享 markup projector。"""
|
|
213
|
+
|
|
214
|
+
package: EpubPackage
|
|
215
|
+
chapter_path: str
|
|
216
|
+
anchors: EpubAnchorRegistry
|
|
217
|
+
|
|
218
|
+
def resolve_link(self, href: str) -> str | None:
|
|
219
|
+
"""解析安全外部链接或实际存在的 EPUB 包内 anchor。"""
|
|
220
|
+
return self.anchors.resolve_link(href, base_part=self.chapter_path)
|
|
221
|
+
|
|
222
|
+
def resolve_image(self, source: str, *, alt: str = "") -> ResolvedMarkupImage | None:
|
|
223
|
+
"""读取并严格校验一个 EPUB 包内栅格图片引用。"""
|
|
224
|
+
target = self.package.resolve_reference(source, base_part=self.chapter_path)
|
|
225
|
+
if target is None:
|
|
226
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
227
|
+
media_type = (self.package.content_type_for(target.path) or "").casefold()
|
|
228
|
+
extension = target.path.rsplit(".", 1)[-1].casefold() if "." in target.path else ""
|
|
229
|
+
media_type = media_type or IMAGE_MEDIA_BY_EXTENSION.get(extension, "")
|
|
230
|
+
if not media_type.startswith("image/") or media_type == SVG_MEDIA_TYPE:
|
|
231
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
232
|
+
payload = self.package.read_part(target.path, asset=True)
|
|
233
|
+
if payload is None:
|
|
234
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
235
|
+
data_uri = f"data:{media_type};base64,{base64.b64encode(payload).decode('ascii')}"
|
|
236
|
+
try:
|
|
237
|
+
parse_image_data_uri_strict(data_uri)
|
|
238
|
+
except ValueError:
|
|
239
|
+
return ResolvedMarkupImage(alt=alt) if alt else None
|
|
240
|
+
return ResolvedMarkupImage(image_base64=data_uri, alt=alt)
|
|
241
|
+
|
|
242
|
+
def heading_anchor(self, heading: etree._Element) -> str | None:
|
|
243
|
+
"""返回一个已预扫描 EPUB 标题的规范 anchor。"""
|
|
244
|
+
return self.anchors.heading_anchor(heading)
|
|
245
|
+
|
|
246
|
+
def heading_label(self, anchor: str) -> str | None:
|
|
247
|
+
"""返回规范 EPUB 标题 anchor 对应的可见标签。"""
|
|
248
|
+
return self.anchors.heading_label(anchor)
|
|
249
|
+
|
|
250
|
+
def note_anchor(self, note: etree._Element) -> str | None:
|
|
251
|
+
"""返回一个已预扫描 EPUB Footnote/Endnote anchor。"""
|
|
252
|
+
return self.anchors.note_anchor(note)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class EpubChapterConverter:
|
|
256
|
+
"""把一个 XHTML spine item 通过共享 projector 投影为 raw blocks。"""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
package: EpubPackage,
|
|
261
|
+
chapter_path: str,
|
|
262
|
+
root: etree._Element,
|
|
263
|
+
anchors: EpubAnchorRegistry,
|
|
264
|
+
) -> None:
|
|
265
|
+
"""绑定单个章节的包、路径、DOM 与文档级 anchor 注册表。"""
|
|
266
|
+
self.package = package
|
|
267
|
+
self.chapter_path = chapter_path
|
|
268
|
+
self.root = root
|
|
269
|
+
self.anchors = anchors
|
|
270
|
+
self.stylesheet = _load_chapter_stylesheet(package, chapter_path, root)
|
|
271
|
+
|
|
272
|
+
def convert(self) -> list[dict[str, object]]:
|
|
273
|
+
"""解析 XHTML body,并保持既有 EPUB 标题与脚注语义。"""
|
|
274
|
+
body = next(
|
|
275
|
+
(element for element in self.root.iter() if isinstance(element.tag, str) and _local_name(element) == "body"),
|
|
276
|
+
None,
|
|
277
|
+
)
|
|
278
|
+
if body is None:
|
|
279
|
+
return []
|
|
280
|
+
context = _EpubMarkupContext(self.package, self.chapter_path, self.anchors)
|
|
281
|
+
return MarkupProjector(
|
|
282
|
+
body,
|
|
283
|
+
context,
|
|
284
|
+
self.stylesheet,
|
|
285
|
+
single_document_title=False,
|
|
286
|
+
).convert()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def convert_svg_spine(
|
|
290
|
+
package: EpubPackage,
|
|
291
|
+
chapter_path: str,
|
|
292
|
+
root: etree._Element,
|
|
293
|
+
) -> list[dict[str, object]]:
|
|
294
|
+
"""把 standalone SVG spine item 尽力转换为文本和包内栅格图片。"""
|
|
295
|
+
empty_registry = EpubAnchorRegistry([], package)
|
|
296
|
+
context = _EpubMarkupContext(package, chapter_path, empty_registry)
|
|
297
|
+
stylesheet = _load_chapter_stylesheet(package, chapter_path, root)
|
|
298
|
+
return MarkupProjector(root, context, stylesheet).convert_svg()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
__all__ = [
|
|
302
|
+
"EpubAnchorRegistry",
|
|
303
|
+
"EpubChapterConverter",
|
|
304
|
+
"build_anchor_registry",
|
|
305
|
+
"convert_svg_spine",
|
|
306
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Standalone HTML 标题、脚注与 fragment anchor 规范化。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
from docvortex.content.markup import MarkupAnchorDocument, MarkupAnchorRegistry, MarkupStylesheet, TextStyle, element_id
|
|
11
|
+
from docvortex.content.markup.projector import BLOCK_TAGS, SKIPPED_TAGS, local_name, visible_raw_text_with_style
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_NOTE_TYPES = frozenset({"footnote", "endnote", "rearnote"})
|
|
15
|
+
_NOTE_ROLES = frozenset({"doc-footnote", "doc-endnote"})
|
|
16
|
+
_NON_TEXT_BLOCK_TAGS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"figure",
|
|
19
|
+
"h1",
|
|
20
|
+
"h2",
|
|
21
|
+
"h3",
|
|
22
|
+
"h4",
|
|
23
|
+
"h5",
|
|
24
|
+
"h6",
|
|
25
|
+
"hr",
|
|
26
|
+
"math",
|
|
27
|
+
"ol",
|
|
28
|
+
"pre",
|
|
29
|
+
"svg",
|
|
30
|
+
"table",
|
|
31
|
+
"ul",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_note_element(element: etree._Element) -> bool:
|
|
37
|
+
"""判断元素是否表示一条可独立投影的 Footnote/Endnote。"""
|
|
38
|
+
roles = frozenset((element.get("role") or "").casefold().split())
|
|
39
|
+
classes = frozenset((element.get("class") or "").casefold().split())
|
|
40
|
+
if (element.get("data-block-type") or "").casefold() == "page_footnote" or "docvortex-page-footnote" in classes:
|
|
41
|
+
return True
|
|
42
|
+
types: set[str] = set()
|
|
43
|
+
for name, value in element.attrib.items():
|
|
44
|
+
attribute = etree.QName(name).localname if name.startswith("{") else name.split(":", 1)[-1]
|
|
45
|
+
if attribute == "type":
|
|
46
|
+
types.update(value.casefold().split())
|
|
47
|
+
return bool(roles & _NOTE_ROLES or types & _NOTE_TYPES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def append_referenced_notes(
|
|
51
|
+
selected_root: etree._Element,
|
|
52
|
+
original_body: etree._Element,
|
|
53
|
+
*,
|
|
54
|
+
stylesheet: MarkupStylesheet,
|
|
55
|
+
resolve_same_document_fragment: Callable[[str], str | None],
|
|
56
|
+
) -> etree._Element:
|
|
57
|
+
"""把正文候选引用但位于候选外的脚注副本追加到内容根末尾。"""
|
|
58
|
+
selected_ids = {
|
|
59
|
+
identity
|
|
60
|
+
for element in selected_root.iter()
|
|
61
|
+
if isinstance(element.tag, str) and (identity := element_id(element)) is not None
|
|
62
|
+
}
|
|
63
|
+
targets = {
|
|
64
|
+
identity: element
|
|
65
|
+
for element in original_body.iter()
|
|
66
|
+
if isinstance(element.tag, str) and (identity := element_id(element)) is not None and is_note_element(element)
|
|
67
|
+
}
|
|
68
|
+
referenced_ids = dict.fromkeys(
|
|
69
|
+
fragment
|
|
70
|
+
for element in selected_root.iter()
|
|
71
|
+
if isinstance(element.tag, str)
|
|
72
|
+
and local_name(element) == "a"
|
|
73
|
+
and (fragment := resolve_same_document_fragment(element.get("href") or "")) is not None
|
|
74
|
+
)
|
|
75
|
+
companions = [
|
|
76
|
+
element for identity, element in targets.items() if identity in referenced_ids and identity not in selected_ids
|
|
77
|
+
]
|
|
78
|
+
companion_copies = [
|
|
79
|
+
copy for companion in companions if (copy := _copy_note_with_source_visibility(companion, stylesheet)) is not None
|
|
80
|
+
]
|
|
81
|
+
if not companion_copies:
|
|
82
|
+
return selected_root
|
|
83
|
+
wrapper = etree.Element("div")
|
|
84
|
+
wrapper.append(selected_root)
|
|
85
|
+
for companion in companion_copies:
|
|
86
|
+
wrapper.append(companion)
|
|
87
|
+
return wrapper
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _copy_note_with_source_visibility(
|
|
91
|
+
note: etree._Element,
|
|
92
|
+
stylesheet: MarkupStylesheet,
|
|
93
|
+
) -> etree._Element | None:
|
|
94
|
+
"""按原始祖先链复制 note;整树隐藏时丢弃,并保留继承文字样式与 visibility。"""
|
|
95
|
+
inherited = TextStyle()
|
|
96
|
+
visibility_hidden = False
|
|
97
|
+
chain = [ancestor for ancestor in reversed(list(note.iterancestors())) if isinstance(ancestor.tag, str)]
|
|
98
|
+
for current in chain:
|
|
99
|
+
resolved = stylesheet.resolve(current, inherited, visibility_hidden)
|
|
100
|
+
if resolved.subtree_hidden:
|
|
101
|
+
return None
|
|
102
|
+
inherited = resolved.text
|
|
103
|
+
visibility_hidden = resolved.visibility_hidden
|
|
104
|
+
if stylesheet.resolve(note, inherited, visibility_hidden).subtree_hidden:
|
|
105
|
+
return None
|
|
106
|
+
copied = deepcopy(note)
|
|
107
|
+
declarations: list[str] = []
|
|
108
|
+
if inherited.bold:
|
|
109
|
+
declarations.append("font-weight:bold")
|
|
110
|
+
if inherited.italic:
|
|
111
|
+
declarations.append("font-style:italic")
|
|
112
|
+
decorations = [
|
|
113
|
+
decoration
|
|
114
|
+
for enabled, decoration in (
|
|
115
|
+
(inherited.underline, "underline"),
|
|
116
|
+
(inherited.strikethrough, "line-through"),
|
|
117
|
+
)
|
|
118
|
+
if enabled
|
|
119
|
+
]
|
|
120
|
+
if decorations:
|
|
121
|
+
declarations.append(f"text-decoration:{' '.join(decorations)}")
|
|
122
|
+
if inherited.superscript:
|
|
123
|
+
declarations.append("vertical-align:super")
|
|
124
|
+
elif inherited.subscript:
|
|
125
|
+
declarations.append("vertical-align:sub")
|
|
126
|
+
if visibility_hidden:
|
|
127
|
+
declarations.append("visibility:hidden")
|
|
128
|
+
if not declarations:
|
|
129
|
+
return copied
|
|
130
|
+
wrapper = etree.Element("div")
|
|
131
|
+
wrapper.set("style", ";".join(declarations))
|
|
132
|
+
if inherited.superscript and inherited.subscript:
|
|
133
|
+
subscript_wrapper = etree.SubElement(wrapper, "sub")
|
|
134
|
+
subscript_wrapper.append(copied)
|
|
135
|
+
else:
|
|
136
|
+
wrapper.append(copied)
|
|
137
|
+
return wrapper
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class _HtmlAnchorPolicy:
|
|
141
|
+
"""保持 standalone HTML 标题与脚注 identity 的既有生成规则。"""
|
|
142
|
+
|
|
143
|
+
anchor_prefix = "html"
|
|
144
|
+
register_document_start = False
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def heading_identity(element: etree._Element, ordinal: int) -> str:
|
|
148
|
+
"""按源 ID 或匿名标题序号生成 HTML 标题 identity。"""
|
|
149
|
+
identity = element_id(element) or f"heading-{ordinal}"
|
|
150
|
+
return f"heading-{identity}-{ordinal}"
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def is_materializable_note(element: etree._Element, document: MarkupAnchorDocument) -> bool:
|
|
154
|
+
"""沿用 HTML note marker 与顶层文本可落地性判断。"""
|
|
155
|
+
return is_note_element(element) and _note_has_materializable_text_target(element, document.stylesheet)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def note_identity(element: etree._Element, ordinal: int) -> str:
|
|
159
|
+
"""按源 ID 或匿名脚注序号生成 HTML 脚注 identity。"""
|
|
160
|
+
identity = element_id(element) or f"note-{ordinal}"
|
|
161
|
+
return f"note-{identity}-{ordinal}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class HtmlAnchorRegistry:
|
|
165
|
+
"""把选中 DOM 的标题、note 和源 fragment 映射到稳定 anchor。"""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
root: etree._Element,
|
|
170
|
+
stylesheet: MarkupStylesheet,
|
|
171
|
+
*,
|
|
172
|
+
source_key: str = "html",
|
|
173
|
+
) -> None:
|
|
174
|
+
"""预扫描选中内容,建立 document-wide 唯一 anchor 映射。"""
|
|
175
|
+
self._source_key = source_key
|
|
176
|
+
document = MarkupAnchorDocument(
|
|
177
|
+
key=source_key,
|
|
178
|
+
root=root,
|
|
179
|
+
stylesheet=stylesheet,
|
|
180
|
+
visibility_scope="all_ancestors",
|
|
181
|
+
text_normalization="unicode_whitespace",
|
|
182
|
+
)
|
|
183
|
+
self._registry = MarkupAnchorRegistry([document], _HtmlAnchorPolicy())
|
|
184
|
+
|
|
185
|
+
def heading_anchor(self, heading: etree._Element) -> str | None:
|
|
186
|
+
"""返回标题的规范 anchor。"""
|
|
187
|
+
return self._registry.heading_anchor(heading)
|
|
188
|
+
|
|
189
|
+
def heading_label(self, anchor: str) -> str | None:
|
|
190
|
+
"""返回规范标题 anchor 对应的可见标签。"""
|
|
191
|
+
return self._registry.heading_label(anchor)
|
|
192
|
+
|
|
193
|
+
def note_anchor(self, note: etree._Element) -> str | None:
|
|
194
|
+
"""返回单条 Footnote/Endnote 的规范 anchor。"""
|
|
195
|
+
return self._registry.note_anchor(note)
|
|
196
|
+
|
|
197
|
+
def resolve_fragment(self, fragment: str) -> str | None:
|
|
198
|
+
"""把源文档 fragment 转换为实际可输出的内部链接。"""
|
|
199
|
+
normalized = fragment.removeprefix("#").strip()
|
|
200
|
+
anchor = self._registry.resolve_target(self._source_key, normalized)
|
|
201
|
+
return f"#{anchor}" if anchor else None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _note_has_materializable_text_target(element: etree._Element, stylesheet: MarkupStylesheet) -> bool:
|
|
205
|
+
"""判断 note 是否会投影出可挂载 anchor 的顶层文本 block。"""
|
|
206
|
+
inherited = TextStyle()
|
|
207
|
+
visibility_hidden = False
|
|
208
|
+
chain = [ancestor for ancestor in reversed(list(element.iterancestors())) if isinstance(ancestor.tag, str)]
|
|
209
|
+
for ancestor in chain:
|
|
210
|
+
resolved = stylesheet.resolve(ancestor, inherited, visibility_hidden)
|
|
211
|
+
if resolved.subtree_hidden:
|
|
212
|
+
return False
|
|
213
|
+
inherited = resolved.text
|
|
214
|
+
visibility_hidden = resolved.visibility_hidden
|
|
215
|
+
resolved = stylesheet.resolve(element, inherited, visibility_hidden)
|
|
216
|
+
if resolved.subtree_hidden:
|
|
217
|
+
return False
|
|
218
|
+
return _container_materializes_text_block(
|
|
219
|
+
element,
|
|
220
|
+
stylesheet,
|
|
221
|
+
resolved.text,
|
|
222
|
+
resolved.visibility_hidden,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _container_materializes_text_block(
|
|
227
|
+
element: etree._Element,
|
|
228
|
+
stylesheet: MarkupStylesheet,
|
|
229
|
+
style: TextStyle,
|
|
230
|
+
visibility_hidden: bool,
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""按共享 projector 的容器分块规则判断是否会产生顶层文本。"""
|
|
233
|
+
if not visibility_hidden and (element.text or "").strip():
|
|
234
|
+
return True
|
|
235
|
+
for child in element:
|
|
236
|
+
if isinstance(child.tag, str):
|
|
237
|
+
resolved = stylesheet.resolve(child, style, visibility_hidden)
|
|
238
|
+
if not resolved.subtree_hidden:
|
|
239
|
+
name = local_name(child)
|
|
240
|
+
if name == "p":
|
|
241
|
+
value = visible_raw_text_with_style(
|
|
242
|
+
child,
|
|
243
|
+
stylesheet,
|
|
244
|
+
resolved.text,
|
|
245
|
+
resolved.visibility_hidden,
|
|
246
|
+
)
|
|
247
|
+
if value.strip():
|
|
248
|
+
return True
|
|
249
|
+
elif name in BLOCK_TAGS:
|
|
250
|
+
if name not in _NON_TEXT_BLOCK_TAGS and _container_materializes_text_block(
|
|
251
|
+
child,
|
|
252
|
+
stylesheet,
|
|
253
|
+
resolved.text,
|
|
254
|
+
resolved.visibility_hidden,
|
|
255
|
+
):
|
|
256
|
+
return True
|
|
257
|
+
elif name not in SKIPPED_TAGS:
|
|
258
|
+
value = visible_raw_text_with_style(
|
|
259
|
+
child,
|
|
260
|
+
stylesheet,
|
|
261
|
+
resolved.text,
|
|
262
|
+
resolved.visibility_hidden,
|
|
263
|
+
)
|
|
264
|
+
if value.strip():
|
|
265
|
+
return True
|
|
266
|
+
if not visibility_hidden and (child.tail or "").strip():
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
__all__ = ["HtmlAnchorRegistry", "append_referenced_notes", "is_note_element"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""HTML 静态解析使用的固定资源限制。"""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MAX_HTML_BYTES: Final = 128 * 1024 * 1024
|
|
7
|
+
MAX_HTML_NODES: Final = 2_000_000
|
|
8
|
+
MAX_HTML_DEPTH: Final = 256
|
|
9
|
+
MAX_HTML_IMAGE_BYTES: Final = 20 * 1024 * 1024
|
|
10
|
+
MAX_HTML_IMAGE_TOTAL_BYTES: Final = 128 * 1024 * 1024
|
|
11
|
+
MAX_HTML_STYLESHEET_BYTES: Final = 5 * 1024 * 1024
|
|
12
|
+
MAX_HTML_STYLESHEET_TOTAL_BYTES: Final = 20 * 1024 * 1024
|
|
13
|
+
MAX_HTML_RENDERED_BYTES: Final = 256 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MAX_HTML_BYTES",
|
|
18
|
+
"MAX_HTML_DEPTH",
|
|
19
|
+
"MAX_HTML_IMAGE_BYTES",
|
|
20
|
+
"MAX_HTML_IMAGE_TOTAL_BYTES",
|
|
21
|
+
"MAX_HTML_NODES",
|
|
22
|
+
"MAX_HTML_RENDERED_BYTES",
|
|
23
|
+
"MAX_HTML_STYLESHEET_BYTES",
|
|
24
|
+
"MAX_HTML_STYLESHEET_TOTAL_BYTES",
|
|
25
|
+
]
|