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,178 @@
|
|
|
1
|
+
"""PPTX 幻灯片标题判定,复用当前转换器的单文档状态。"""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from .....schema import BlockType
|
|
6
|
+
|
|
7
|
+
from .context import (
|
|
8
|
+
_EFFECTIVE_FONT_SIZE_KEY,
|
|
9
|
+
_EFFECTIVE_ALL_BOLD_KEY,
|
|
10
|
+
_PPTX_TITLE_CANDIDATE_KEY,
|
|
11
|
+
_PPTX_TITLE_ROLE_KEY,
|
|
12
|
+
_PPTX_TITLE_ROLE_CENTER,
|
|
13
|
+
_PPTX_TITLE_ROLE_SUBTITLE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _PptxTitles:
|
|
18
|
+
"""集中维护幻灯片标题判定,不改变文档生命周期和公开入口。"""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _most_common_size(font_sizes: list[float]) -> Optional[float]:
|
|
22
|
+
"""按原有幻灯片标题判定规则执行 _most_common_size,保持输入顺序与降级行为。"""
|
|
23
|
+
if not font_sizes:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
counts = Counter(font_sizes)
|
|
27
|
+
return min(
|
|
28
|
+
counts.items(),
|
|
29
|
+
key=lambda item: (-item[1], item[0]),
|
|
30
|
+
)[0]
|
|
31
|
+
|
|
32
|
+
def _promote_slide_text_blocks_to_titles(self, slide_blocks: list[dict]) -> None:
|
|
33
|
+
"""按原有幻灯片标题判定规则执行 _promote_slide_text_blocks_to_titles,保持输入顺序与降级行为。"""
|
|
34
|
+
body_font_size_pt = self._most_common_size(
|
|
35
|
+
[
|
|
36
|
+
block[_EFFECTIVE_FONT_SIZE_KEY]
|
|
37
|
+
for block in slide_blocks
|
|
38
|
+
if (
|
|
39
|
+
block.get("type") == BlockType.TEXT
|
|
40
|
+
and block.get(_PPTX_TITLE_CANDIDATE_KEY) is not True
|
|
41
|
+
and block.get(_EFFECTIVE_FONT_SIZE_KEY) is not None
|
|
42
|
+
and not block.get(_EFFECTIVE_ALL_BOLD_KEY, False)
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self._promote_level2_text_blocks(slide_blocks, body_font_size_pt)
|
|
48
|
+
self._promote_level3_text_blocks(slide_blocks, body_font_size_pt)
|
|
49
|
+
|
|
50
|
+
def _promote_level2_text_blocks(
|
|
51
|
+
self,
|
|
52
|
+
slide_blocks: list[dict],
|
|
53
|
+
body_font_size_pt: Optional[float],
|
|
54
|
+
) -> None:
|
|
55
|
+
"""按原有幻灯片标题判定规则执行 _promote_level2_text_blocks,保持输入顺序与降级行为。"""
|
|
56
|
+
bold_text_blocks = [
|
|
57
|
+
block
|
|
58
|
+
for block in slide_blocks
|
|
59
|
+
if (
|
|
60
|
+
block.get("type") == BlockType.TEXT
|
|
61
|
+
and block.get(_PPTX_TITLE_CANDIDATE_KEY) is not True
|
|
62
|
+
and block.get(_EFFECTIVE_ALL_BOLD_KEY, False)
|
|
63
|
+
and block.get(_EFFECTIVE_FONT_SIZE_KEY) is not None
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
if not bold_text_blocks:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
bold_font_sizes = sorted(
|
|
70
|
+
{block[_EFFECTIVE_FONT_SIZE_KEY] for block in bold_text_blocks},
|
|
71
|
+
reverse=True,
|
|
72
|
+
)
|
|
73
|
+
level2_font_size_pt = bold_font_sizes[0]
|
|
74
|
+
level2_candidates = [block for block in bold_text_blocks if block[_EFFECTIVE_FONT_SIZE_KEY] == level2_font_size_pt]
|
|
75
|
+
|
|
76
|
+
if len(level2_candidates) != 1:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if body_font_size_pt is not None and level2_font_size_pt < body_font_size_pt + 4:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if len(bold_font_sizes) > 1 and level2_font_size_pt < bold_font_sizes[1] + 2:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
level2_candidates[0][_PPTX_TITLE_CANDIDATE_KEY] = True
|
|
86
|
+
level2_candidates[0]["level"] = 2
|
|
87
|
+
|
|
88
|
+
def _promote_level3_text_blocks(
|
|
89
|
+
self,
|
|
90
|
+
slide_blocks: list[dict],
|
|
91
|
+
body_font_size_pt: Optional[float],
|
|
92
|
+
) -> None:
|
|
93
|
+
"""按原有幻灯片标题判定规则执行 _promote_level3_text_blocks,保持输入顺序与降级行为。"""
|
|
94
|
+
if body_font_size_pt is None:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
level2_font_sizes = sorted(
|
|
98
|
+
{
|
|
99
|
+
block[_EFFECTIVE_FONT_SIZE_KEY]
|
|
100
|
+
for block in slide_blocks
|
|
101
|
+
if (
|
|
102
|
+
block.get(_PPTX_TITLE_CANDIDATE_KEY) is True
|
|
103
|
+
and block.get("level") == 2
|
|
104
|
+
and block.get(_EFFECTIVE_FONT_SIZE_KEY) is not None
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
reverse=True,
|
|
108
|
+
)
|
|
109
|
+
if not level2_font_sizes:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
level2_font_size_pt = level2_font_sizes[0]
|
|
113
|
+
level3_font_sizes = sorted(
|
|
114
|
+
{
|
|
115
|
+
block[_EFFECTIVE_FONT_SIZE_KEY]
|
|
116
|
+
for block in slide_blocks
|
|
117
|
+
if (
|
|
118
|
+
block.get("type") == BlockType.TEXT
|
|
119
|
+
and block.get(_PPTX_TITLE_CANDIDATE_KEY) is not True
|
|
120
|
+
and block.get(_EFFECTIVE_ALL_BOLD_KEY, False)
|
|
121
|
+
and block.get(_EFFECTIVE_FONT_SIZE_KEY) is not None
|
|
122
|
+
and block[_EFFECTIVE_FONT_SIZE_KEY] < level2_font_size_pt
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
reverse=True,
|
|
126
|
+
)
|
|
127
|
+
if not level3_font_sizes:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
level3_font_size_pt = level3_font_sizes[0]
|
|
131
|
+
if level3_font_size_pt < body_font_size_pt + 2:
|
|
132
|
+
return
|
|
133
|
+
if level2_font_size_pt < level3_font_size_pt + 2:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
for block in slide_blocks:
|
|
137
|
+
if (
|
|
138
|
+
block.get("type") == BlockType.TEXT
|
|
139
|
+
and block.get(_PPTX_TITLE_CANDIDATE_KEY) is not True
|
|
140
|
+
and block.get(_EFFECTIVE_ALL_BOLD_KEY, False)
|
|
141
|
+
and block.get(_EFFECTIVE_FONT_SIZE_KEY) == level3_font_size_pt
|
|
142
|
+
):
|
|
143
|
+
block[_PPTX_TITLE_CANDIDATE_KEY] = True
|
|
144
|
+
block["level"] = 3
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _finalize_slide_title_types(
|
|
148
|
+
slide_blocks: list[dict],
|
|
149
|
+
*,
|
|
150
|
+
is_first_visible_slide: bool,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""将 PPTX 标题候选统一拆分为文档标题、段落标题或普通文本。"""
|
|
153
|
+
for block in slide_blocks:
|
|
154
|
+
is_title_candidate = block.pop(_PPTX_TITLE_CANDIDATE_KEY, False) is True
|
|
155
|
+
title_role = block.pop(_PPTX_TITLE_ROLE_KEY, None)
|
|
156
|
+
if not is_title_candidate:
|
|
157
|
+
continue
|
|
158
|
+
if title_role == _PPTX_TITLE_ROLE_SUBTITLE:
|
|
159
|
+
block["type"] = BlockType.TEXT
|
|
160
|
+
block.pop("level", None)
|
|
161
|
+
block.pop("is_numbered_style", None)
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
if title_role == _PPTX_TITLE_ROLE_CENTER and is_first_visible_slide:
|
|
165
|
+
block["type"] = BlockType.DOC_TITLE
|
|
166
|
+
block["level"] = 1
|
|
167
|
+
block.pop("is_numbered_style", None)
|
|
168
|
+
else:
|
|
169
|
+
block["type"] = BlockType.PARAGRAPH_TITLE
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _cleanup_slide_text_block_metadata(slide_blocks: list[dict]) -> None:
|
|
173
|
+
"""按原有幻灯片标题判定规则执行 _cleanup_slide_text_block_metadata,保持输入顺序与降级行为。"""
|
|
174
|
+
for block in slide_blocks:
|
|
175
|
+
block.pop(_EFFECTIVE_FONT_SIZE_KEY, None)
|
|
176
|
+
block.pop(_EFFECTIVE_ALL_BOLD_KEY, None)
|
|
177
|
+
block.pop(_PPTX_TITLE_CANDIDATE_KEY, None)
|
|
178
|
+
block.pop(_PPTX_TITLE_ROLE_KEY, None)
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import html
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .._shared.hyperlink import OFFICE_EXTERNAL_HYPERLINK_SCHEMES, sanitize_hyperlink_target
|
|
6
|
+
from ....content.spans import append_hyperlink_span, append_text_span, extend_inline_spans, normalize_span_dicts
|
|
7
|
+
|
|
8
|
+
VISIBLE_SPACE_STYLES = {"underline", "emphasis", "strikethrough"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class OfficeRichTextSegment:
|
|
13
|
+
"""表示 Office 行内富文本片段,用于统一样式和超链接输出。"""
|
|
14
|
+
|
|
15
|
+
text: str
|
|
16
|
+
style: str | list[str] | tuple[str, ...] | None = None
|
|
17
|
+
hyperlink: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _style_list(style: str | list[str] | tuple[str, ...] | None) -> list[str]:
|
|
21
|
+
"""把样式字符串或列表规范为样式列表。"""
|
|
22
|
+
if not style:
|
|
23
|
+
return []
|
|
24
|
+
if isinstance(style, str):
|
|
25
|
+
return [item.strip() for item in style.split(",") if item.strip()]
|
|
26
|
+
return [str(item).strip() for item in style if str(item).strip()]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _style_str(style: str | list[str] | tuple[str, ...] | None) -> Optional[str]:
|
|
30
|
+
"""把样式字符串或列表规范为逗号分隔字符串。"""
|
|
31
|
+
styles = _style_list(style)
|
|
32
|
+
return ",".join(styles) if styles else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _script_to_style_name(format_obj: Any) -> Optional[str]:
|
|
36
|
+
"""把 DOCX 上下标脚本位置转换为 Office 内部富文本样式名。"""
|
|
37
|
+
script = getattr(format_obj, "script", None)
|
|
38
|
+
script_value = getattr(script, "value", script)
|
|
39
|
+
if script_value == "super":
|
|
40
|
+
return "superscript"
|
|
41
|
+
if script_value == "sub":
|
|
42
|
+
return "subscript"
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def formatting_to_style_str(format_obj: Any) -> Optional[str]:
|
|
47
|
+
"""从 Formatting-like 对象提取 Office 内部富文本样式字符串。"""
|
|
48
|
+
if format_obj is None:
|
|
49
|
+
return None
|
|
50
|
+
styles = []
|
|
51
|
+
if getattr(format_obj, "bold", False):
|
|
52
|
+
styles.append("bold")
|
|
53
|
+
if getattr(format_obj, "italic", False):
|
|
54
|
+
styles.append("italic")
|
|
55
|
+
if getattr(format_obj, "underline", False):
|
|
56
|
+
styles.append("underline")
|
|
57
|
+
if getattr(format_obj, "emphasis", False):
|
|
58
|
+
styles.append("emphasis")
|
|
59
|
+
if getattr(format_obj, "strikethrough", False):
|
|
60
|
+
styles.append("strikethrough")
|
|
61
|
+
script_style = _script_to_style_name(format_obj)
|
|
62
|
+
if script_style:
|
|
63
|
+
styles.append(script_style)
|
|
64
|
+
return ",".join(styles) if styles else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def has_visible_style(format_obj: Any) -> bool:
|
|
68
|
+
"""判断格式是否包含让空白文本也可见的样式。"""
|
|
69
|
+
if format_obj is None:
|
|
70
|
+
return False
|
|
71
|
+
return bool(
|
|
72
|
+
getattr(format_obj, "underline", False)
|
|
73
|
+
or getattr(format_obj, "emphasis", False)
|
|
74
|
+
or getattr(format_obj, "strikethrough", False)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def has_non_visible_text_style(format_obj: Any) -> bool:
|
|
79
|
+
"""判断格式是否只包含空白文本不可见的字形样式。"""
|
|
80
|
+
if format_obj is None:
|
|
81
|
+
return False
|
|
82
|
+
return bool(getattr(format_obj, "bold", False) or getattr(format_obj, "italic", False))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalize_format_for_text(
|
|
86
|
+
format_obj: Any,
|
|
87
|
+
text: str,
|
|
88
|
+
*,
|
|
89
|
+
preserve_blank_non_visible_style: bool = False,
|
|
90
|
+
) -> Any:
|
|
91
|
+
"""按文本内容规范 run 格式,避免空白 run 误把不可见样式带到输出。"""
|
|
92
|
+
if format_obj is None:
|
|
93
|
+
return None
|
|
94
|
+
if text.strip():
|
|
95
|
+
return format_obj
|
|
96
|
+
|
|
97
|
+
updates = {}
|
|
98
|
+
if getattr(format_obj, "underline_style", "") == "words":
|
|
99
|
+
updates["underline"] = False
|
|
100
|
+
updates["underline_style"] = ""
|
|
101
|
+
if has_non_visible_text_style(format_obj) and not preserve_blank_non_visible_style:
|
|
102
|
+
updates["bold"] = False
|
|
103
|
+
updates["italic"] = False
|
|
104
|
+
|
|
105
|
+
if updates and hasattr(format_obj, "model_copy"):
|
|
106
|
+
format_obj = format_obj.model_copy(update=updates)
|
|
107
|
+
|
|
108
|
+
if not has_visible_style(format_obj):
|
|
109
|
+
if preserve_blank_non_visible_style and has_non_visible_text_style(format_obj):
|
|
110
|
+
return format_obj
|
|
111
|
+
return None
|
|
112
|
+
return format_obj
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def should_keep_group_text(
|
|
116
|
+
text: str,
|
|
117
|
+
format_obj: Any,
|
|
118
|
+
*,
|
|
119
|
+
preserve_plain_blank: bool = False,
|
|
120
|
+
) -> bool:
|
|
121
|
+
"""判断当前累积文本是否应输出,保留可见样式或被显式保留的空白。"""
|
|
122
|
+
if not text:
|
|
123
|
+
return False
|
|
124
|
+
if text.strip():
|
|
125
|
+
return True
|
|
126
|
+
if has_visible_style(format_obj):
|
|
127
|
+
return True
|
|
128
|
+
return preserve_plain_blank
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def append_rich_text_element(
|
|
132
|
+
paragraph_elements: list[tuple[str, Any, Any]],
|
|
133
|
+
text: str,
|
|
134
|
+
format_obj: Any,
|
|
135
|
+
hyperlink: Any,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""追加段落元素;相邻同 URL 且同格式的片段合并为一个元素。"""
|
|
138
|
+
if (
|
|
139
|
+
hyperlink is not None
|
|
140
|
+
and paragraph_elements
|
|
141
|
+
and paragraph_elements[-1][2] is not None
|
|
142
|
+
and str(paragraph_elements[-1][2]) == str(hyperlink)
|
|
143
|
+
and paragraph_elements[-1][1] == format_obj
|
|
144
|
+
):
|
|
145
|
+
previous_text, previous_format, previous_hyperlink = paragraph_elements[-1]
|
|
146
|
+
paragraph_elements[-1] = (
|
|
147
|
+
f"{previous_text}{text}",
|
|
148
|
+
previous_format,
|
|
149
|
+
previous_hyperlink,
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
paragraph_elements.append((text, format_obj, hyperlink))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def format_text_spans(
|
|
156
|
+
text: str,
|
|
157
|
+
hyperlink: Any = None,
|
|
158
|
+
style: str | list[str] | tuple[str, ...] | None = None,
|
|
159
|
+
) -> list[dict[str, Any]]:
|
|
160
|
+
"""把 Office 文字、样式和安全超链接直接构造为 Span。"""
|
|
161
|
+
if not text:
|
|
162
|
+
return []
|
|
163
|
+
normalized_text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
164
|
+
children: list[dict[str, Any]] = []
|
|
165
|
+
append_text_span(children, normalized_text, _style_list(style))
|
|
166
|
+
safe_target = sanitize_hyperlink_target(
|
|
167
|
+
hyperlink,
|
|
168
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
169
|
+
allow_relative=True,
|
|
170
|
+
allow_fragment=True,
|
|
171
|
+
)
|
|
172
|
+
if safe_target is None:
|
|
173
|
+
return children
|
|
174
|
+
output: list[dict[str, Any]] = []
|
|
175
|
+
append_hyperlink_span(output, children, safe_target)
|
|
176
|
+
return output
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def is_valid_hyperlink_target(hyperlink: Any) -> bool:
|
|
180
|
+
"""判断超链接目标是否可作为真实链接输出。"""
|
|
181
|
+
return (
|
|
182
|
+
sanitize_hyperlink_target(
|
|
183
|
+
hyperlink,
|
|
184
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
185
|
+
allow_relative=True,
|
|
186
|
+
allow_fragment=True,
|
|
187
|
+
)
|
|
188
|
+
is not None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _format_hyperlink_segments(group: list[OfficeRichTextSegment]) -> list[dict[str, Any]]:
|
|
193
|
+
"""将连续同 URL 的多个片段构造成单个 HyperlinkSpan。"""
|
|
194
|
+
if not group:
|
|
195
|
+
return []
|
|
196
|
+
safe_target = sanitize_hyperlink_target(
|
|
197
|
+
group[0].hyperlink,
|
|
198
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
199
|
+
allow_relative=True,
|
|
200
|
+
allow_fragment=True,
|
|
201
|
+
)
|
|
202
|
+
children: list[dict[str, Any]] = []
|
|
203
|
+
for segment in group:
|
|
204
|
+
append_text_span(children, segment.text, _style_list(segment.style))
|
|
205
|
+
if safe_target is None:
|
|
206
|
+
return children
|
|
207
|
+
output: list[dict[str, Any]] = []
|
|
208
|
+
append_hyperlink_span(output, children, safe_target)
|
|
209
|
+
return output
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def format_hyperlink_group(
|
|
213
|
+
group: list[tuple[str, Any, Any]],
|
|
214
|
+
) -> list[dict[str, Any]]:
|
|
215
|
+
"""将 DOCX paragraph element 分组构造成单个 HyperlinkSpan。"""
|
|
216
|
+
return _format_hyperlink_segments(
|
|
217
|
+
[
|
|
218
|
+
OfficeRichTextSegment(
|
|
219
|
+
text=text,
|
|
220
|
+
style=formatting_to_style_str(format_obj),
|
|
221
|
+
hyperlink=str(hyperlink) if hyperlink is not None else None,
|
|
222
|
+
)
|
|
223
|
+
for text, format_obj, hyperlink in group
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _style_has_visible_space(style: str | list[str] | tuple[str, ...] | None) -> bool:
|
|
229
|
+
"""判断样式列表是否会让空白文本在渲染结果中可见。"""
|
|
230
|
+
return any(style_name in VISIBLE_SPACE_STYLES for style_name in _style_list(style))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _trim_plain_edge_spaces(
|
|
234
|
+
segments: list[OfficeRichTextSegment],
|
|
235
|
+
) -> list[OfficeRichTextSegment]:
|
|
236
|
+
"""只裁剪段落首尾普通空白,不裁剪带可见样式的空白。"""
|
|
237
|
+
trimmed_segments = [segment for segment in segments if segment.text is not None]
|
|
238
|
+
if not trimmed_segments:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
start_idx = 0
|
|
242
|
+
while start_idx < len(trimmed_segments):
|
|
243
|
+
segment = trimmed_segments[start_idx]
|
|
244
|
+
if segment.text.strip() or _style_has_visible_space(segment.style):
|
|
245
|
+
if not _style_has_visible_space(segment.style):
|
|
246
|
+
trimmed_segments[start_idx] = OfficeRichTextSegment(
|
|
247
|
+
segment.text.lstrip(),
|
|
248
|
+
segment.style,
|
|
249
|
+
segment.hyperlink,
|
|
250
|
+
)
|
|
251
|
+
break
|
|
252
|
+
start_idx += 1
|
|
253
|
+
if start_idx == len(trimmed_segments):
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
trimmed_segments = trimmed_segments[start_idx:]
|
|
257
|
+
end_idx = len(trimmed_segments) - 1
|
|
258
|
+
while end_idx >= 0:
|
|
259
|
+
segment = trimmed_segments[end_idx]
|
|
260
|
+
if segment.text.strip() or _style_has_visible_space(segment.style):
|
|
261
|
+
if not _style_has_visible_space(segment.style):
|
|
262
|
+
trimmed_segments[end_idx] = OfficeRichTextSegment(
|
|
263
|
+
segment.text.rstrip(),
|
|
264
|
+
segment.style,
|
|
265
|
+
segment.hyperlink,
|
|
266
|
+
)
|
|
267
|
+
break
|
|
268
|
+
end_idx -= 1
|
|
269
|
+
if end_idx < 0:
|
|
270
|
+
return []
|
|
271
|
+
return trimmed_segments[: end_idx + 1]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _merge_non_link_segments(
|
|
275
|
+
segments: list[OfficeRichTextSegment],
|
|
276
|
+
) -> list[OfficeRichTextSegment]:
|
|
277
|
+
"""合并相邻同样式的非超链接片段,避免输出碎片化样式标记。"""
|
|
278
|
+
merged: list[OfficeRichTextSegment] = []
|
|
279
|
+
for segment in segments:
|
|
280
|
+
if (
|
|
281
|
+
merged
|
|
282
|
+
and not is_valid_hyperlink_target(merged[-1].hyperlink)
|
|
283
|
+
and not is_valid_hyperlink_target(segment.hyperlink)
|
|
284
|
+
and _style_str(merged[-1].style) == _style_str(segment.style)
|
|
285
|
+
):
|
|
286
|
+
previous = merged[-1]
|
|
287
|
+
merged[-1] = OfficeRichTextSegment(
|
|
288
|
+
f"{previous.text}{segment.text}",
|
|
289
|
+
previous.style,
|
|
290
|
+
previous.hyperlink,
|
|
291
|
+
)
|
|
292
|
+
continue
|
|
293
|
+
merged.append(segment)
|
|
294
|
+
return merged
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def build_rich_text_from_segments(
|
|
298
|
+
segments: list[OfficeRichTextSegment],
|
|
299
|
+
*,
|
|
300
|
+
trim_plain_edges: bool = False,
|
|
301
|
+
) -> list[dict[str, Any]]:
|
|
302
|
+
"""从 Office 富文本片段直接构建规范化行内 Span。"""
|
|
303
|
+
normalized_segments = [
|
|
304
|
+
OfficeRichTextSegment(
|
|
305
|
+
segment.text,
|
|
306
|
+
_style_str(segment.style),
|
|
307
|
+
sanitize_hyperlink_target(
|
|
308
|
+
segment.hyperlink,
|
|
309
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
310
|
+
allow_relative=True,
|
|
311
|
+
allow_fragment=True,
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
for segment in segments
|
|
315
|
+
if segment.text is not None and segment.text != ""
|
|
316
|
+
]
|
|
317
|
+
if trim_plain_edges:
|
|
318
|
+
normalized_segments = _trim_plain_edge_spaces(normalized_segments)
|
|
319
|
+
normalized_segments = _merge_non_link_segments(normalized_segments)
|
|
320
|
+
|
|
321
|
+
rendered_spans: list[dict[str, Any]] = []
|
|
322
|
+
index = 0
|
|
323
|
+
while index < len(normalized_segments):
|
|
324
|
+
segment = normalized_segments[index]
|
|
325
|
+
if is_valid_hyperlink_target(segment.hyperlink):
|
|
326
|
+
group = [segment]
|
|
327
|
+
index += 1
|
|
328
|
+
while index < len(normalized_segments):
|
|
329
|
+
next_segment = normalized_segments[index]
|
|
330
|
+
if not is_valid_hyperlink_target(next_segment.hyperlink) or str(next_segment.hyperlink) != str(
|
|
331
|
+
segment.hyperlink
|
|
332
|
+
):
|
|
333
|
+
break
|
|
334
|
+
group.append(next_segment)
|
|
335
|
+
index += 1
|
|
336
|
+
extend_inline_spans(rendered_spans, _format_hyperlink_segments(group))
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
extend_inline_spans(
|
|
340
|
+
rendered_spans,
|
|
341
|
+
format_text_spans(
|
|
342
|
+
segment.text,
|
|
343
|
+
segment.hyperlink,
|
|
344
|
+
segment.style,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
index += 1
|
|
348
|
+
|
|
349
|
+
return normalize_span_dicts(rendered_spans)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def build_spans_from_elements(
|
|
353
|
+
paragraph_elements: list[tuple[str, Any, Any]],
|
|
354
|
+
) -> list[dict[str, Any]]:
|
|
355
|
+
"""把 DOCX paragraph element 直接构造成结构化 Span。"""
|
|
356
|
+
return build_rich_text_from_segments(
|
|
357
|
+
[
|
|
358
|
+
OfficeRichTextSegment(
|
|
359
|
+
text=text,
|
|
360
|
+
style=formatting_to_style_str(format_obj),
|
|
361
|
+
hyperlink=str(hyperlink) if hyperlink is not None else None,
|
|
362
|
+
)
|
|
363
|
+
for text, format_obj, hyperlink in paragraph_elements
|
|
364
|
+
if text
|
|
365
|
+
]
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def build_rich_text_html_from_segments(
|
|
370
|
+
segments: list[OfficeRichTextSegment],
|
|
371
|
+
*,
|
|
372
|
+
trim_plain_edges: bool = False,
|
|
373
|
+
) -> str:
|
|
374
|
+
"""把 Office 富文本片段序列化为表格单元格使用的安全 HTML。"""
|
|
375
|
+
normalized = _trim_plain_edge_spaces(segments) if trim_plain_edges else list(segments)
|
|
376
|
+
parts: list[str] = []
|
|
377
|
+
for segment in normalized:
|
|
378
|
+
if not segment.text:
|
|
379
|
+
continue
|
|
380
|
+
rendered = html.escape(segment.text, quote=False).replace("\r\n", "\n").replace("\r", "\n")
|
|
381
|
+
styles = _style_list(segment.style)
|
|
382
|
+
if "superscript" in styles:
|
|
383
|
+
rendered = f"<sup>{rendered}</sup>"
|
|
384
|
+
elif "subscript" in styles:
|
|
385
|
+
rendered = f"<sub>{rendered}</sub>"
|
|
386
|
+
if "underline" in styles:
|
|
387
|
+
rendered = f"<u>{rendered}</u>"
|
|
388
|
+
if "bold" in styles:
|
|
389
|
+
rendered = f"<strong>{rendered}</strong>"
|
|
390
|
+
if "italic" in styles:
|
|
391
|
+
rendered = f"<em>{rendered}</em>"
|
|
392
|
+
if "strikethrough" in styles:
|
|
393
|
+
rendered = f"<s>{rendered}</s>"
|
|
394
|
+
safe_target = sanitize_hyperlink_target(
|
|
395
|
+
segment.hyperlink,
|
|
396
|
+
allowed_schemes=OFFICE_EXTERNAL_HYPERLINK_SCHEMES,
|
|
397
|
+
allow_relative=True,
|
|
398
|
+
allow_fragment=True,
|
|
399
|
+
)
|
|
400
|
+
if safe_target:
|
|
401
|
+
rendered = f'<a href="{html.escape(safe_target, quote=True)}">{rendered}</a>'
|
|
402
|
+
parts.append(rendered)
|
|
403
|
+
return "".join(parts)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
__all__ = [
|
|
407
|
+
"OfficeRichTextSegment",
|
|
408
|
+
"append_rich_text_element",
|
|
409
|
+
"build_rich_text_from_segments",
|
|
410
|
+
"build_rich_text_html_from_segments",
|
|
411
|
+
"build_spans_from_elements",
|
|
412
|
+
"format_hyperlink_group",
|
|
413
|
+
"format_text_spans",
|
|
414
|
+
"formatting_to_style_str",
|
|
415
|
+
"has_non_visible_text_style",
|
|
416
|
+
"has_visible_style",
|
|
417
|
+
"is_valid_hyperlink_target",
|
|
418
|
+
"normalize_format_for_text",
|
|
419
|
+
"should_keep_group_text",
|
|
420
|
+
]
|