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,635 @@
|
|
|
1
|
+
"""Markdown 与 Content List 共用的 block 级序列化。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from ....options import LatexDelimitersConfig
|
|
9
|
+
from ..common.index import strip_index_page_tail
|
|
10
|
+
from ..common.list_items import has_markdown_unordered_marker, reference_list_needs_bullets
|
|
11
|
+
from ..common.planner import PlannedBlock
|
|
12
|
+
from .assets import build_markdown_image, resolve_image_source
|
|
13
|
+
from .escaping import escape_standalone_marker_rule, escape_text_block_markdown_prefix
|
|
14
|
+
from .inline import (
|
|
15
|
+
render_inline_content,
|
|
16
|
+
render_inline_spans,
|
|
17
|
+
render_inline_spans_in_html_context,
|
|
18
|
+
render_internal_link,
|
|
19
|
+
render_joined_inline_contents,
|
|
20
|
+
)
|
|
21
|
+
from .table import _strip_embedded_images, format_embedded_html, render_html_table
|
|
22
|
+
from ...contracts import ImageRenderer
|
|
23
|
+
from ....schema import (
|
|
24
|
+
RAW_ALGORITHM,
|
|
25
|
+
AlgorithmBodyBlock,
|
|
26
|
+
BlockType,
|
|
27
|
+
ChartAnnotationBlock,
|
|
28
|
+
ChartBlock,
|
|
29
|
+
ChartBodyBlock,
|
|
30
|
+
CodeAnnotationBlock,
|
|
31
|
+
CodeBlock,
|
|
32
|
+
CodeBodyBlock,
|
|
33
|
+
DocTitleBlock,
|
|
34
|
+
EquationBlock,
|
|
35
|
+
ImageAnnotationBlock,
|
|
36
|
+
ImageBlock,
|
|
37
|
+
ImageBodyBlock,
|
|
38
|
+
InlineSpan,
|
|
39
|
+
IndexBlock,
|
|
40
|
+
ListBlock,
|
|
41
|
+
PageAuxTextBlock,
|
|
42
|
+
PageFootnoteBlock,
|
|
43
|
+
PageBlock,
|
|
44
|
+
ParagraphTitleBlock,
|
|
45
|
+
RefTextBlock,
|
|
46
|
+
TableAnnotationBlock,
|
|
47
|
+
TableBlock,
|
|
48
|
+
TableBodyBlock,
|
|
49
|
+
TextBlock,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_VALID_CODE_LANGUAGE_RE = re.compile(r"[A-Za-z0-9_.+#-]+")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def render_planned_block(
|
|
56
|
+
planned: PlannedBlock,
|
|
57
|
+
*,
|
|
58
|
+
delimiters: LatexDelimitersConfig,
|
|
59
|
+
asset_base_url: str,
|
|
60
|
+
image_renderer: ImageRenderer | None = None,
|
|
61
|
+
anchor_targets: set[str] | None = None,
|
|
62
|
+
emitted_anchors: set[str] | None = None,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""按具体 Pydantic block 类型分发 Markdown 渲染。"""
|
|
65
|
+
block = planned.block
|
|
66
|
+
if isinstance(block, TextBlock):
|
|
67
|
+
content = render_joined_inline_contents(planned.text_contents or [block.content], delimiters)
|
|
68
|
+
rendered = escape_standalone_marker_rule(escape_text_block_markdown_prefix(content))
|
|
69
|
+
return _prepend_markdown_anchor(rendered, block.anchor, emitted_anchors)
|
|
70
|
+
if isinstance(block, RefTextBlock):
|
|
71
|
+
content = render_joined_inline_contents(planned.text_contents or [block.content], delimiters)
|
|
72
|
+
return escape_standalone_marker_rule(content)
|
|
73
|
+
if isinstance(block, (DocTitleBlock, ParagraphTitleBlock)):
|
|
74
|
+
return _render_title(block, delimiters, emitted_anchors)
|
|
75
|
+
if isinstance(block, PageFootnoteBlock):
|
|
76
|
+
return _render_page_footnote(block, delimiters, emitted_anchors)
|
|
77
|
+
if isinstance(block, PageAuxTextBlock):
|
|
78
|
+
content = escape_text_block_markdown_prefix(render_inline_content(block.content, delimiters))
|
|
79
|
+
return escape_standalone_marker_rule(content)
|
|
80
|
+
if isinstance(block, EquationBlock):
|
|
81
|
+
return _render_equation(block, delimiters, asset_base_url, image_renderer)
|
|
82
|
+
if isinstance(block, ListBlock):
|
|
83
|
+
return _render_list(block, delimiters)
|
|
84
|
+
if isinstance(block, IndexBlock):
|
|
85
|
+
return _render_index(block, delimiters, anchor_targets=anchor_targets)
|
|
86
|
+
if isinstance(block, ImageBlock):
|
|
87
|
+
return _render_image_block(block, delimiters, asset_base_url, image_renderer)
|
|
88
|
+
if isinstance(block, TableBlock):
|
|
89
|
+
return _render_table_block(block, delimiters, asset_base_url, image_renderer)
|
|
90
|
+
if isinstance(block, ChartBlock):
|
|
91
|
+
return _render_chart_block(block, delimiters, asset_base_url, image_renderer)
|
|
92
|
+
if isinstance(block, CodeBlock):
|
|
93
|
+
return _render_code_block(block, delimiters)
|
|
94
|
+
raise TypeError(f"Unsupported PageBlock type: {type(block).__name__}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _claim_markdown_anchor(anchor: str | None, emitted_anchors: set[str] | None) -> str:
|
|
98
|
+
"""规范化 Markdown 目标,并在文档级集合中只登记首次出现的 anchor。"""
|
|
99
|
+
normalized = (anchor or "").strip()
|
|
100
|
+
if not normalized:
|
|
101
|
+
return ""
|
|
102
|
+
if emitted_anchors is not None:
|
|
103
|
+
if normalized in emitted_anchors:
|
|
104
|
+
return ""
|
|
105
|
+
emitted_anchors.add(normalized)
|
|
106
|
+
return normalized
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _prepend_markdown_anchor(content: str, anchor: str | None, emitted_anchors: set[str] | None) -> str:
|
|
110
|
+
"""给非空 Markdown 正文添加独立 HTML anchor,避免污染正文文本。"""
|
|
111
|
+
if not content.strip():
|
|
112
|
+
return content
|
|
113
|
+
normalized = _claim_markdown_anchor(anchor, emitted_anchors)
|
|
114
|
+
if not normalized:
|
|
115
|
+
return content
|
|
116
|
+
return f'<a id="{html.escape(normalized, quote=True)}"></a>\n{content}'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _render_page_footnote(
|
|
120
|
+
block: PageFootnoteBlock,
|
|
121
|
+
delimiters: LatexDelimitersConfig,
|
|
122
|
+
emitted_anchors: set[str] | None = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""用非折叠的小字号浅色 HTML 标识输出页面脚注。"""
|
|
125
|
+
content = escape_text_block_markdown_prefix(render_inline_content(block.content, delimiters))
|
|
126
|
+
rendered = escape_standalone_marker_rule(content)
|
|
127
|
+
if not rendered.strip():
|
|
128
|
+
return ""
|
|
129
|
+
rendered = rendered.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "<br>")
|
|
130
|
+
attrs = [
|
|
131
|
+
'class="docvortex-page-footnote"',
|
|
132
|
+
'data-block-type="page_footnote"',
|
|
133
|
+
'style="color:#6b7280"',
|
|
134
|
+
]
|
|
135
|
+
anchor = _claim_markdown_anchor(block.anchor, emitted_anchors)
|
|
136
|
+
if anchor:
|
|
137
|
+
attrs.insert(0, f'id="{html.escape(anchor, quote=True)}"')
|
|
138
|
+
return f"<small><span {' '.join(attrs)}>{rendered}</span></small>"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def render_single_block(
|
|
142
|
+
block: PageBlock,
|
|
143
|
+
*,
|
|
144
|
+
delimiters: LatexDelimitersConfig,
|
|
145
|
+
asset_base_url: str,
|
|
146
|
+
image_renderer: ImageRenderer | None = None,
|
|
147
|
+
) -> str:
|
|
148
|
+
"""不执行续段合并或页面过滤,直接渲染一个顶层 block。"""
|
|
149
|
+
text_contents = [block.content] if isinstance(block, (TextBlock, RefTextBlock)) else []
|
|
150
|
+
planned = PlannedBlock(page_idx=0, block=block, text_contents=text_contents)
|
|
151
|
+
return render_planned_block(
|
|
152
|
+
planned,
|
|
153
|
+
delimiters=delimiters,
|
|
154
|
+
asset_base_url=asset_base_url,
|
|
155
|
+
image_renderer=image_renderer,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _render_title(
|
|
160
|
+
block: DocTitleBlock | ParagraphTitleBlock,
|
|
161
|
+
delimiters: LatexDelimitersConfig,
|
|
162
|
+
emitted_anchors: set[str] | None = None,
|
|
163
|
+
) -> str:
|
|
164
|
+
"""渲染带可选 HTML anchor 的 Markdown 标题。"""
|
|
165
|
+
level = min(max(block.level, 1), 6)
|
|
166
|
+
title = f"{'#' * level} {render_title_inline_content(block, delimiters)}"
|
|
167
|
+
anchor = _claim_markdown_anchor(block.anchor, emitted_anchors)
|
|
168
|
+
if not anchor:
|
|
169
|
+
return title
|
|
170
|
+
return f'<a id="{html.escape(anchor, quote=True)}"></a>\n{title}'
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def render_title_inline_content(
|
|
174
|
+
block: DocTitleBlock | ParagraphTitleBlock,
|
|
175
|
+
delimiters: LatexDelimitersConfig,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""只渲染标题的行内语义,不添加 heading 标记或 HTML anchor。"""
|
|
178
|
+
return render_inline_content(block.content, delimiters)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _render_equation(
|
|
182
|
+
block: EquationBlock,
|
|
183
|
+
delimiters: LatexDelimitersConfig,
|
|
184
|
+
asset_base_url: str,
|
|
185
|
+
image_renderer: ImageRenderer | None,
|
|
186
|
+
) -> str:
|
|
187
|
+
"""优先渲染行间 LaTeX,空公式内容回退到 image_renderer 或公式图片。"""
|
|
188
|
+
latex = block.content.strip()
|
|
189
|
+
if latex:
|
|
190
|
+
return f"{delimiters.display.left}\n{latex}\n{delimiters.display.right}"
|
|
191
|
+
if image_renderer is not None:
|
|
192
|
+
rendered = image_renderer(block)
|
|
193
|
+
if rendered:
|
|
194
|
+
return rendered
|
|
195
|
+
source = resolve_image_source(block, asset_base_url)
|
|
196
|
+
return build_markdown_image(source) if source else ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _render_list(block: ListBlock, delimiters: LatexDelimitersConfig, depth: int = 0) -> str:
|
|
200
|
+
"""递归渲染列表,并给多数条目无数字前缀的参考文献补无序标记。"""
|
|
201
|
+
add_ref_bullets = reference_list_needs_bullets(block)
|
|
202
|
+
lines: list[str] = []
|
|
203
|
+
for child in block.content:
|
|
204
|
+
if isinstance(child, ListBlock):
|
|
205
|
+
nested = _render_list(child, delimiters, depth + 1)
|
|
206
|
+
if nested:
|
|
207
|
+
lines.extend(nested.splitlines())
|
|
208
|
+
continue
|
|
209
|
+
item = render_inline_content(child.content, delimiters)
|
|
210
|
+
if not item:
|
|
211
|
+
continue
|
|
212
|
+
item = escape_standalone_marker_rule(item)
|
|
213
|
+
indent = " " * depth
|
|
214
|
+
item_lines = item.splitlines() or [item]
|
|
215
|
+
if add_ref_bullets and not has_markdown_unordered_marker(child.content):
|
|
216
|
+
lines.append(f"{indent}- {item_lines[0]}")
|
|
217
|
+
lines.extend(f"{indent} {line}" for line in item_lines[1:])
|
|
218
|
+
else:
|
|
219
|
+
lines.extend(f"{indent}{line}" for line in item_lines)
|
|
220
|
+
return "\n".join(lines)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _render_index(
|
|
224
|
+
block: IndexBlock,
|
|
225
|
+
delimiters: LatexDelimitersConfig,
|
|
226
|
+
depth: int = 0,
|
|
227
|
+
*,
|
|
228
|
+
anchor_targets: set[str] | None = None,
|
|
229
|
+
) -> str:
|
|
230
|
+
"""递归渲染目录列表,并给标题叶子添加内部 anchor 链接。"""
|
|
231
|
+
lines: list[str] = []
|
|
232
|
+
for child in block.content:
|
|
233
|
+
if isinstance(child, IndexBlock):
|
|
234
|
+
nested = _render_index(child, delimiters, depth + 1, anchor_targets=anchor_targets)
|
|
235
|
+
if nested:
|
|
236
|
+
lines.extend(nested.splitlines())
|
|
237
|
+
continue
|
|
238
|
+
content = strip_index_page_tail(child.content)
|
|
239
|
+
label = render_inline_content(content, delimiters).strip()
|
|
240
|
+
if not label:
|
|
241
|
+
continue
|
|
242
|
+
anchor = (child.anchor or "").strip()
|
|
243
|
+
if anchor and (anchor_targets is None or anchor in anchor_targets):
|
|
244
|
+
label = render_internal_link(label, anchor)
|
|
245
|
+
lines.append(f"{' ' * depth}- {label}")
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _render_image_block(
|
|
250
|
+
block: ImageBlock,
|
|
251
|
+
delimiters: LatexDelimitersConfig,
|
|
252
|
+
asset_base_url: str,
|
|
253
|
+
image_renderer: ImageRenderer | None,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""按原始子块顺序渲染图片主体及说明文本。"""
|
|
256
|
+
parts: list[str] = []
|
|
257
|
+
for child in block.content:
|
|
258
|
+
if isinstance(child, ImageBodyBlock):
|
|
259
|
+
body = _render_visual_body_child(
|
|
260
|
+
block,
|
|
261
|
+
child,
|
|
262
|
+
delimiters=delimiters,
|
|
263
|
+
asset_base_url=asset_base_url,
|
|
264
|
+
image_renderer=image_renderer,
|
|
265
|
+
)
|
|
266
|
+
elif isinstance(child, ImageAnnotationBlock):
|
|
267
|
+
body = render_visual_annotation(child, delimiters)
|
|
268
|
+
else:
|
|
269
|
+
raise TypeError(f"Unsupported image child: {type(child).__name__}")
|
|
270
|
+
if body:
|
|
271
|
+
parts.append(body)
|
|
272
|
+
return _join_visual_parts(parts)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _render_chart_block(
|
|
276
|
+
block: ChartBlock,
|
|
277
|
+
delimiters: LatexDelimitersConfig,
|
|
278
|
+
asset_base_url: str,
|
|
279
|
+
image_renderer: ImageRenderer | None,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""按原始子块顺序渲染图表图片、结构内容和说明文本。"""
|
|
282
|
+
parts: list[str] = []
|
|
283
|
+
for child in block.content:
|
|
284
|
+
if isinstance(child, ChartBodyBlock):
|
|
285
|
+
body = _render_visual_body_child(
|
|
286
|
+
block,
|
|
287
|
+
child,
|
|
288
|
+
delimiters=delimiters,
|
|
289
|
+
asset_base_url=asset_base_url,
|
|
290
|
+
image_renderer=image_renderer,
|
|
291
|
+
)
|
|
292
|
+
elif isinstance(child, ChartAnnotationBlock):
|
|
293
|
+
body = render_visual_annotation(child, delimiters)
|
|
294
|
+
else:
|
|
295
|
+
raise TypeError(f"Unsupported chart child: {type(child).__name__}")
|
|
296
|
+
if body:
|
|
297
|
+
parts.append(body)
|
|
298
|
+
return _join_visual_parts(parts)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _render_media_body(
|
|
302
|
+
block: ImageBodyBlock,
|
|
303
|
+
*,
|
|
304
|
+
summary: str,
|
|
305
|
+
delimiters: LatexDelimitersConfig,
|
|
306
|
+
asset_base_url: str,
|
|
307
|
+
image_renderer: ImageRenderer | None,
|
|
308
|
+
parent_block: ImageBlock,
|
|
309
|
+
) -> str:
|
|
310
|
+
"""渲染图片载荷,并将识别内容放入折叠详情。image_renderer 优先于 image_path。"""
|
|
311
|
+
rendered_content = _render_media_content(block, delimiters, asset_base_url)
|
|
312
|
+
if image_renderer is not None:
|
|
313
|
+
image_ref = image_renderer(parent_block)
|
|
314
|
+
parts = [image_ref] if image_ref else []
|
|
315
|
+
else:
|
|
316
|
+
source = resolve_image_source(block, asset_base_url)
|
|
317
|
+
parts = [build_markdown_image(source)] if source else []
|
|
318
|
+
if rendered_content:
|
|
319
|
+
parts.append(_render_details(rendered_content, summary))
|
|
320
|
+
return "\n\n".join(part for part in parts if part)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _render_media_content(
|
|
324
|
+
block: ImageBodyBlock,
|
|
325
|
+
delimiters: LatexDelimitersConfig,
|
|
326
|
+
asset_base_url: str,
|
|
327
|
+
) -> str:
|
|
328
|
+
"""渲染图片识别内容,不添加图片语法或折叠详情包装。"""
|
|
329
|
+
content = block.content.strip()
|
|
330
|
+
if not content:
|
|
331
|
+
return ""
|
|
332
|
+
return format_embedded_html(content, asset_base_url=asset_base_url, delimiters=delimiters)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _render_chart_body(
|
|
336
|
+
block: ChartBodyBlock,
|
|
337
|
+
*,
|
|
338
|
+
summary: str,
|
|
339
|
+
delimiters: LatexDelimitersConfig,
|
|
340
|
+
asset_base_url: str,
|
|
341
|
+
image_renderer: ImageRenderer | None,
|
|
342
|
+
parent_block: ChartBlock,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""渲染 chart 图片,并统一转换其 HTML 表格内容。内容为空时回退到 image_renderer 或图片。"""
|
|
345
|
+
rendered_content = _render_chart_content(block.content, delimiters, asset_base_url)
|
|
346
|
+
if rendered_content:
|
|
347
|
+
return _render_chart_with_details(rendered_content, block, asset_base_url, summary)
|
|
348
|
+
if image_renderer is not None:
|
|
349
|
+
rendered = image_renderer(parent_block)
|
|
350
|
+
if rendered:
|
|
351
|
+
return rendered
|
|
352
|
+
source = resolve_image_source(block, asset_base_url)
|
|
353
|
+
return build_markdown_image(source) if source else ""
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _render_chart_with_details(
|
|
357
|
+
rendered_content: str,
|
|
358
|
+
block: ChartBodyBlock,
|
|
359
|
+
asset_base_url: str,
|
|
360
|
+
summary: str,
|
|
361
|
+
) -> str:
|
|
362
|
+
"""chart 有结构化内容时,附上图片与折叠详情。"""
|
|
363
|
+
source = resolve_image_source(block, asset_base_url)
|
|
364
|
+
if source:
|
|
365
|
+
parts = [build_markdown_image(source), _render_details(rendered_content, summary)]
|
|
366
|
+
return "\n\n".join(part for part in parts if part)
|
|
367
|
+
return rendered_content
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _render_chart_content(
|
|
371
|
+
content: str,
|
|
372
|
+
delimiters: LatexDelimitersConfig,
|
|
373
|
+
asset_base_url: str,
|
|
374
|
+
) -> str:
|
|
375
|
+
"""把 chart 内容中的简单 HTML 表格转为 GFM,其他内容保持原表示。"""
|
|
376
|
+
normalized = content.strip()
|
|
377
|
+
if not normalized:
|
|
378
|
+
return ""
|
|
379
|
+
html_table = render_html_table(
|
|
380
|
+
normalized,
|
|
381
|
+
asset_base_url=asset_base_url,
|
|
382
|
+
delimiters=delimiters,
|
|
383
|
+
)
|
|
384
|
+
if html_table is not None:
|
|
385
|
+
return html_table
|
|
386
|
+
return format_embedded_html(normalized, asset_base_url=asset_base_url, delimiters=delimiters)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _render_details(content: str, summary: str) -> str:
|
|
390
|
+
"""构造保留已渲染视觉内容的折叠 HTML 详情块。"""
|
|
391
|
+
safe_summary = html.escape(summary, quote=False)
|
|
392
|
+
return f"<details>\n<summary>{safe_summary}</summary>\n\n{content.strip()}\n</details>"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _render_table_block(
|
|
396
|
+
block: TableBlock,
|
|
397
|
+
delimiters: LatexDelimitersConfig,
|
|
398
|
+
asset_base_url: str,
|
|
399
|
+
image_renderer: ImageRenderer | None,
|
|
400
|
+
) -> str:
|
|
401
|
+
"""按原始子块顺序渲染表格主体及说明文本。"""
|
|
402
|
+
parts: list[str] = []
|
|
403
|
+
for child in block.content:
|
|
404
|
+
if isinstance(child, TableBodyBlock):
|
|
405
|
+
body = _render_visual_body_child(
|
|
406
|
+
block,
|
|
407
|
+
child,
|
|
408
|
+
delimiters=delimiters,
|
|
409
|
+
asset_base_url=asset_base_url,
|
|
410
|
+
image_renderer=image_renderer,
|
|
411
|
+
)
|
|
412
|
+
elif isinstance(child, TableAnnotationBlock):
|
|
413
|
+
body = render_visual_annotation(child, delimiters)
|
|
414
|
+
else:
|
|
415
|
+
raise TypeError(f"Unsupported table child: {type(child).__name__}")
|
|
416
|
+
if body:
|
|
417
|
+
parts.append(body)
|
|
418
|
+
return _join_visual_parts(parts)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _render_table_body(
|
|
422
|
+
block: TableBodyBlock,
|
|
423
|
+
delimiters: LatexDelimitersConfig,
|
|
424
|
+
asset_base_url: str,
|
|
425
|
+
image_renderer: ImageRenderer | None,
|
|
426
|
+
parent_block: TableBlock,
|
|
427
|
+
) -> str:
|
|
428
|
+
"""按 HTML、空间投影文本、image_renderer、图片的优先级渲染表格主体。"""
|
|
429
|
+
rendered_content = _render_table_content(
|
|
430
|
+
block,
|
|
431
|
+
delimiters,
|
|
432
|
+
asset_base_url,
|
|
433
|
+
strip_embedded_images=image_renderer is not None,
|
|
434
|
+
)
|
|
435
|
+
if rendered_content:
|
|
436
|
+
return rendered_content
|
|
437
|
+
if image_renderer is not None:
|
|
438
|
+
rendered = image_renderer(parent_block)
|
|
439
|
+
if rendered:
|
|
440
|
+
return rendered
|
|
441
|
+
source = resolve_image_source(block, asset_base_url)
|
|
442
|
+
return build_markdown_image(source) if source else ""
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _render_table_content(
|
|
446
|
+
block: TableBodyBlock,
|
|
447
|
+
delimiters: LatexDelimitersConfig,
|
|
448
|
+
asset_base_url: str,
|
|
449
|
+
*,
|
|
450
|
+
strip_embedded_images: bool = False,
|
|
451
|
+
) -> str:
|
|
452
|
+
"""渲染表格结构内容,不执行空内容时的图片回退。"""
|
|
453
|
+
content = _strip_embedded_images(block.content) if strip_embedded_images else block.content
|
|
454
|
+
if not content:
|
|
455
|
+
return ""
|
|
456
|
+
html_table = render_html_table(
|
|
457
|
+
content,
|
|
458
|
+
asset_base_url=asset_base_url,
|
|
459
|
+
delimiters=delimiters,
|
|
460
|
+
)
|
|
461
|
+
if html_table is not None:
|
|
462
|
+
return html_table
|
|
463
|
+
return _render_fenced_content(content)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _render_code_block(block: CodeBlock, delimiters: LatexDelimitersConfig) -> str:
|
|
467
|
+
"""按父块 subtype 渲染普通代码或支持公式的算法。"""
|
|
468
|
+
parts: list[str] = []
|
|
469
|
+
for child in block.content:
|
|
470
|
+
if isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
|
|
471
|
+
body = _render_visual_body_child(
|
|
472
|
+
block,
|
|
473
|
+
child,
|
|
474
|
+
delimiters=delimiters,
|
|
475
|
+
asset_base_url="",
|
|
476
|
+
image_renderer=None,
|
|
477
|
+
)
|
|
478
|
+
elif isinstance(child, CodeAnnotationBlock):
|
|
479
|
+
body = render_visual_annotation(child, delimiters)
|
|
480
|
+
else:
|
|
481
|
+
raise TypeError(f"Unsupported code child: {type(child).__name__}")
|
|
482
|
+
if body:
|
|
483
|
+
parts.append(body)
|
|
484
|
+
return _join_visual_parts(parts)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def render_visual_body_content(
|
|
488
|
+
block: ImageBlock | TableBlock | ChartBlock | CodeBlock,
|
|
489
|
+
*,
|
|
490
|
+
delimiters: LatexDelimitersConfig,
|
|
491
|
+
asset_base_url: str,
|
|
492
|
+
) -> str:
|
|
493
|
+
"""查找视觉父块唯一 body,并只渲染可结构化消费的语义内容。"""
|
|
494
|
+
for child in block.content:
|
|
495
|
+
if isinstance(block, ImageBlock) and isinstance(child, ImageBodyBlock):
|
|
496
|
+
return _render_media_content(child, delimiters, asset_base_url)
|
|
497
|
+
if isinstance(block, TableBlock) and isinstance(child, TableBodyBlock):
|
|
498
|
+
return _render_table_content(child, delimiters, asset_base_url)
|
|
499
|
+
if isinstance(block, ChartBlock) and isinstance(child, ChartBodyBlock):
|
|
500
|
+
return _render_chart_content(child.content, delimiters, asset_base_url)
|
|
501
|
+
if isinstance(block, CodeBlock) and isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
|
|
502
|
+
return _render_code_semantic_content(block, child, delimiters)
|
|
503
|
+
raise ValueError(f"Missing visual body: {block.type}")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _render_visual_body_child(
|
|
507
|
+
block: ImageBlock | TableBlock | ChartBlock | CodeBlock,
|
|
508
|
+
child: ImageBodyBlock | TableBodyBlock | ChartBodyBlock | CodeBodyBlock | AlgorithmBodyBlock,
|
|
509
|
+
*,
|
|
510
|
+
delimiters: LatexDelimitersConfig,
|
|
511
|
+
asset_base_url: str,
|
|
512
|
+
image_renderer: ImageRenderer | None = None,
|
|
513
|
+
) -> str:
|
|
514
|
+
"""按视觉父子类型组合渲染一个 body 子块。"""
|
|
515
|
+
if isinstance(block, ImageBlock) and isinstance(child, ImageBodyBlock):
|
|
516
|
+
return _render_media_body(
|
|
517
|
+
child,
|
|
518
|
+
summary=block.sub_type or "image content",
|
|
519
|
+
delimiters=delimiters,
|
|
520
|
+
asset_base_url=asset_base_url,
|
|
521
|
+
image_renderer=image_renderer,
|
|
522
|
+
parent_block=block,
|
|
523
|
+
)
|
|
524
|
+
if isinstance(block, TableBlock) and isinstance(child, TableBodyBlock):
|
|
525
|
+
return _render_table_body(child, delimiters, asset_base_url, image_renderer, block)
|
|
526
|
+
if isinstance(block, ChartBlock) and isinstance(child, ChartBodyBlock):
|
|
527
|
+
return _render_chart_body(
|
|
528
|
+
child,
|
|
529
|
+
summary=block.sub_type or "chart content",
|
|
530
|
+
delimiters=delimiters,
|
|
531
|
+
asset_base_url=asset_base_url,
|
|
532
|
+
image_renderer=image_renderer,
|
|
533
|
+
parent_block=block,
|
|
534
|
+
)
|
|
535
|
+
if isinstance(block, CodeBlock) and isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
|
|
536
|
+
return _render_code_body(block, child, delimiters)
|
|
537
|
+
raise TypeError(f"Unsupported visual body pair: {block.type}/{child.type}")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def render_visual_annotation(
|
|
541
|
+
block: ImageAnnotationBlock | TableAnnotationBlock | ChartAnnotationBlock | CodeAnnotationBlock,
|
|
542
|
+
delimiters: LatexDelimitersConfig,
|
|
543
|
+
) -> str:
|
|
544
|
+
"""把一个视觉说明子块渲染为独立 Markdown 字符串。"""
|
|
545
|
+
return escape_standalone_marker_rule(render_inline_content(block.content, delimiters))
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _render_code_body(
|
|
549
|
+
block: CodeBlock,
|
|
550
|
+
child: CodeBodyBlock | AlgorithmBodyBlock,
|
|
551
|
+
delimiters: LatexDelimitersConfig,
|
|
552
|
+
) -> str:
|
|
553
|
+
"""依据父块 subtype 渲染代码或算法 body。"""
|
|
554
|
+
if block.sub_type == RAW_ALGORITHM:
|
|
555
|
+
if not isinstance(child, AlgorithmBodyBlock):
|
|
556
|
+
raise TypeError("algorithm subtype requires AlgorithmBodyBlock")
|
|
557
|
+
return _render_algorithm_html(child.content, delimiters)
|
|
558
|
+
return _render_code_semantic_content(block, child, delimiters)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _render_code_semantic_content(
|
|
562
|
+
block: CodeBlock,
|
|
563
|
+
child: CodeBodyBlock | AlgorithmBodyBlock,
|
|
564
|
+
delimiters: LatexDelimitersConfig,
|
|
565
|
+
) -> str:
|
|
566
|
+
"""把代码或算法 body 渲染为 Structured Content 可消费的类 Markdown 字符串。"""
|
|
567
|
+
if block.sub_type == BlockType.CODE:
|
|
568
|
+
if not isinstance(child, CodeBodyBlock):
|
|
569
|
+
raise TypeError("code subtype requires CodeBodyBlock")
|
|
570
|
+
return _render_fenced_content(child.content, _normalize_code_language(block.guess_lang))
|
|
571
|
+
if block.sub_type == RAW_ALGORITHM:
|
|
572
|
+
if not isinstance(child, AlgorithmBodyBlock):
|
|
573
|
+
raise TypeError("algorithm subtype requires AlgorithmBodyBlock")
|
|
574
|
+
return _render_algorithm_markdown(child.content, delimiters)
|
|
575
|
+
raise ValueError(f"Unsupported code subtype: {block.sub_type}")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _normalize_code_language(language: str | None) -> str:
|
|
579
|
+
"""校验 fenced code info string,非法值统一回退 txt。"""
|
|
580
|
+
normalized = (language or "").strip()
|
|
581
|
+
if not normalized or _VALID_CODE_LANGUAGE_RE.fullmatch(normalized) is None:
|
|
582
|
+
return "txt"
|
|
583
|
+
return normalized
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _render_fenced_content(content: str, language: str | None = None) -> str:
|
|
587
|
+
"""使用长于正文反引号游程的围栏包裹原始内容。"""
|
|
588
|
+
longest = max((len(match.group(0)) for match in re.finditer(r"`+", content)), default=0)
|
|
589
|
+
fence = "`" * max(3, longest + 1)
|
|
590
|
+
opening = f"{fence}{language or ''}"
|
|
591
|
+
closing_prefix = "" if content.endswith("\n") else "\n"
|
|
592
|
+
return f"{opening}\n{content}{closing_prefix}{fence}"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _render_algorithm_html(content: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
|
|
596
|
+
"""参考 dev 实现渲染保留空白、上下标和行内公式的算法 HTML。"""
|
|
597
|
+
parts: list[str] = []
|
|
598
|
+
previous_equation = False
|
|
599
|
+
for span in content:
|
|
600
|
+
current_equation = str(span.type) == "equation_inline"
|
|
601
|
+
if previous_equation and current_equation:
|
|
602
|
+
parts.append(" ")
|
|
603
|
+
parts.append(render_inline_spans_in_html_context([span], delimiters))
|
|
604
|
+
previous_equation = current_equation
|
|
605
|
+
body = "".join(parts)
|
|
606
|
+
if not body.strip():
|
|
607
|
+
return ""
|
|
608
|
+
return f'<div class="docvortex-algorithm" style="white-space: pre-wrap; font-family:monospace;">\n{body}\n</div>'
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _render_algorithm_markdown(content: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
|
|
612
|
+
"""把算法 Span 渲染为类 Markdown 内容,并分隔相邻行内公式。"""
|
|
613
|
+
parts: list[str] = []
|
|
614
|
+
previous_equation = False
|
|
615
|
+
for span in content:
|
|
616
|
+
current_equation = str(span.type) == "equation_inline"
|
|
617
|
+
if previous_equation and current_equation:
|
|
618
|
+
parts.append(" ")
|
|
619
|
+
parts.append(render_inline_spans([span], delimiters))
|
|
620
|
+
previous_equation = current_equation
|
|
621
|
+
return "".join(parts)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _join_visual_parts(parts: list[str]) -> str:
|
|
625
|
+
"""使用安全空行连接同一视觉父块中的有序子块。"""
|
|
626
|
+
return "\n\n".join(part.strip("\n") for part in parts if part and part.strip())
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
__all__ = [
|
|
630
|
+
"render_planned_block",
|
|
631
|
+
"render_single_block",
|
|
632
|
+
"render_title_inline_content",
|
|
633
|
+
"render_visual_annotation",
|
|
634
|
+
"render_visual_body_content",
|
|
635
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Markdown 文本转义与 block 前缀保护。"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
CONSERVATIVE_MARKDOWN_SPECIAL_CHARS = ("*", "_", "`", "~", "$")
|
|
6
|
+
TEXT_BLOCK_MARKDOWN_PREFIX_RE = re.compile(r"^(?P<indent>[ \t]{0,3})(?P<marker>#{1,6}|[+-])(?=[ \t])")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# DONE in render
|
|
10
|
+
def escape_conservative_markdown_text(content: str) -> str:
|
|
11
|
+
"""Escape plain-text characters that carry inline Markdown semantics."""
|
|
12
|
+
if not content:
|
|
13
|
+
return content
|
|
14
|
+
|
|
15
|
+
escaped_chars = []
|
|
16
|
+
preceding_backslashes = 0
|
|
17
|
+
|
|
18
|
+
for char in content:
|
|
19
|
+
if char == "\\":
|
|
20
|
+
escaped_chars.append(char)
|
|
21
|
+
preceding_backslashes += 1
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
if char in CONSERVATIVE_MARKDOWN_SPECIAL_CHARS and preceding_backslashes % 2 == 0:
|
|
25
|
+
escaped_chars.append("\\")
|
|
26
|
+
|
|
27
|
+
escaped_chars.append(char)
|
|
28
|
+
preceding_backslashes = 0
|
|
29
|
+
|
|
30
|
+
return "".join(escaped_chars)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# DONE in render
|
|
34
|
+
def escape_text_block_markdown_prefix(content: str) -> str:
|
|
35
|
+
"""Escape a leading Markdown block marker in an assembled text block."""
|
|
36
|
+
if not content:
|
|
37
|
+
return content
|
|
38
|
+
|
|
39
|
+
match = TEXT_BLOCK_MARKDOWN_PREFIX_RE.match(content)
|
|
40
|
+
if not match:
|
|
41
|
+
return content
|
|
42
|
+
|
|
43
|
+
marker_start = match.start("marker")
|
|
44
|
+
return f"{content[:marker_start]}\\{content[marker_start:]}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def escape_standalone_marker_rule(content: str) -> str:
|
|
48
|
+
"""转义独立的下划线或短横线序列,避免被 Markdown 识别为分割线。"""
|
|
49
|
+
if content and (set(content) == {"_"} or set(content) == {"-"}):
|
|
50
|
+
return f"\\{content}"
|
|
51
|
+
return content
|