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,469 @@
|
|
|
1
|
+
"""构造受限 ODF 表格网格并序列化为安全 HTML。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from lxml import etree # type: ignore[reportMissingImports]
|
|
11
|
+
|
|
12
|
+
from ..limits import MAX_GRID_SLOTS
|
|
13
|
+
from .constants import MAX_EXPANSION_TEXT_BYTES, qname
|
|
14
|
+
from .errors import OdfResourceLimitError
|
|
15
|
+
from .models import GridCell, TableGrid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CellRenderer = Callable[[etree._Element], str]
|
|
19
|
+
_HTML_TEXT_RE = re.compile(r"<[^>]+>")
|
|
20
|
+
_CELL_ADDRESS_RE = re.compile(r"\$?(?P<col>[A-Za-z]+)\$?(?P<row>[1-9][0-9]*)")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class OdfTableExpansionBudget:
|
|
25
|
+
"""累计单个 ODF 文档全部表格的网格与重复文本膨胀。"""
|
|
26
|
+
|
|
27
|
+
used_grid_slots: int = 0
|
|
28
|
+
used_duplicated_text_bytes: int = 0
|
|
29
|
+
|
|
30
|
+
def start_table(self) -> _TableGridReservation:
|
|
31
|
+
"""为一次表格解析创建只累计新增矩形面积的局部预留。"""
|
|
32
|
+
return _TableGridReservation(self)
|
|
33
|
+
|
|
34
|
+
def charge_grid_slots(self, additional_slots: int) -> None:
|
|
35
|
+
"""在表格网格扩容前累计文档级槽位。"""
|
|
36
|
+
if additional_slots < 0 or self.used_grid_slots > MAX_GRID_SLOTS - additional_slots:
|
|
37
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_grid_slots={MAX_GRID_SLOTS}")
|
|
38
|
+
self.used_grid_slots += additional_slots
|
|
39
|
+
|
|
40
|
+
def charge_duplicated_text(self, byte_count: int) -> None:
|
|
41
|
+
"""在复制单元格文本前累计文档级膨胀字节。"""
|
|
42
|
+
if byte_count < 0 or self.used_duplicated_text_bytes > MAX_EXPANSION_TEXT_BYTES - byte_count:
|
|
43
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_expansion_text_bytes={MAX_EXPANSION_TEXT_BYTES}")
|
|
44
|
+
self.used_duplicated_text_bytes += byte_count
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class _TableGridReservation:
|
|
49
|
+
"""记录当前表格已经计入文档预算的最大矩形面积。"""
|
|
50
|
+
|
|
51
|
+
budget: OdfTableExpansionBudget
|
|
52
|
+
reserved_slots: int = 0
|
|
53
|
+
|
|
54
|
+
def reserve(self, row_count: int, width: int) -> None:
|
|
55
|
+
"""仅把当前表格增长部分计入文档级预算。"""
|
|
56
|
+
projected_slots = row_count * max(width, 1)
|
|
57
|
+
if projected_slots <= self.reserved_slots:
|
|
58
|
+
return
|
|
59
|
+
self.budget.charge_grid_slots(projected_slots - self.reserved_slots)
|
|
60
|
+
self.reserved_slots = projected_slots
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _positive_int(value: str | None, default: int = 1) -> int:
|
|
64
|
+
"""在转换前约束 ODF 计数属性,并把损坏值回退为至少一。"""
|
|
65
|
+
if value is None:
|
|
66
|
+
return default
|
|
67
|
+
normalized = value.strip()
|
|
68
|
+
if normalized.startswith("+"):
|
|
69
|
+
normalized = normalized[1:]
|
|
70
|
+
if not normalized.isascii() or not normalized.isdigit():
|
|
71
|
+
return default
|
|
72
|
+
limit_text = str(MAX_GRID_SLOTS)
|
|
73
|
+
if len(normalized) > len(limit_text):
|
|
74
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_grid_slots={MAX_GRID_SLOTS}")
|
|
75
|
+
significant = normalized.lstrip("0") or "0"
|
|
76
|
+
if len(significant) > len(limit_text) or (len(significant) == len(limit_text) and significant > limit_text):
|
|
77
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_grid_slots={MAX_GRID_SLOTS}")
|
|
78
|
+
return max(1, int(significant))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _iter_rows(container: etree._Element, *, header: bool = False) -> Iterator[tuple[etree._Element, bool]]:
|
|
82
|
+
"""按 ODF 容器顺序递归产出普通行和表头行。"""
|
|
83
|
+
for child in container:
|
|
84
|
+
if not isinstance(child.tag, str):
|
|
85
|
+
continue
|
|
86
|
+
if child.tag == qname("table", "table-row"):
|
|
87
|
+
yield child, header
|
|
88
|
+
elif child.tag == qname("table", "table-header-rows"):
|
|
89
|
+
yield from _iter_rows(child, header=True)
|
|
90
|
+
elif child.tag in {qname("table", "table-rows"), qname("table", "table-row-group")}:
|
|
91
|
+
yield from _iter_rows(child, header=header)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _typed_value_text(cell: etree._Element) -> str:
|
|
95
|
+
"""在单元格无显示段落时把 ODF typed cached value 转为文本。"""
|
|
96
|
+
value_type = cell.get(qname("office", "value-type"), "")
|
|
97
|
+
if value_type == "percentage":
|
|
98
|
+
try:
|
|
99
|
+
return f"{float(cell.get(qname('office', 'value'), '0')) * 100:g}%"
|
|
100
|
+
except ValueError:
|
|
101
|
+
return ""
|
|
102
|
+
if value_type == "currency":
|
|
103
|
+
value = cell.get(qname("office", "value"), "")
|
|
104
|
+
currency = cell.get(qname("office", "currency"), "")
|
|
105
|
+
return f"{value} {currency}".strip()
|
|
106
|
+
if value_type == "float":
|
|
107
|
+
value = cell.get(qname("office", "value"), "")
|
|
108
|
+
try:
|
|
109
|
+
return f"{float(value):g}"
|
|
110
|
+
except ValueError:
|
|
111
|
+
return value
|
|
112
|
+
if value_type == "date":
|
|
113
|
+
return cell.get(qname("office", "date-value"), "")
|
|
114
|
+
if value_type == "time":
|
|
115
|
+
return cell.get(qname("office", "time-value"), "")
|
|
116
|
+
if value_type == "boolean":
|
|
117
|
+
return "TRUE" if cell.get(qname("office", "boolean-value"), "false").casefold() == "true" else "FALSE"
|
|
118
|
+
if value_type == "string":
|
|
119
|
+
return cell.get(qname("office", "string-value"), "")
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _has_visible_html(value: str) -> bool:
|
|
124
|
+
"""判断单元格 HTML 是否包含非空文本或图片/公式结构。"""
|
|
125
|
+
if any(token in value.casefold() for token in ("<img", "<eq", "<table", "<ul", "<ol")):
|
|
126
|
+
return True
|
|
127
|
+
return bool(html.unescape(_HTML_TEXT_RE.sub("", value)).strip())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _validate_grid_extent(row_count: int, width: int) -> None:
|
|
131
|
+
"""在分配或遍历前校验预计矩形不会超过共享网格预算。"""
|
|
132
|
+
projected_width = max(width, 1)
|
|
133
|
+
if row_count > MAX_GRID_SLOTS // projected_width:
|
|
134
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_grid_slots={MAX_GRID_SLOTS}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _ensure_row(
|
|
138
|
+
grid: TableGrid,
|
|
139
|
+
row_index: int,
|
|
140
|
+
width: int = 0,
|
|
141
|
+
reservation: _TableGridReservation | None = None,
|
|
142
|
+
) -> list[GridCell | None]:
|
|
143
|
+
"""确保网格存在指定行和最小列宽。"""
|
|
144
|
+
_validate_grid_extent(max(len(grid.rows), row_index + 1), max(grid.width, width))
|
|
145
|
+
if reservation is not None:
|
|
146
|
+
reservation.reserve(max(len(grid.rows), row_index + 1), max(grid.width, width))
|
|
147
|
+
while len(grid.rows) <= row_index:
|
|
148
|
+
grid.rows.append([])
|
|
149
|
+
row = grid.rows[row_index]
|
|
150
|
+
if len(row) < width:
|
|
151
|
+
row.extend([None] * (width - len(row)))
|
|
152
|
+
return row
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _charge_grid(grid: TableGrid) -> None:
|
|
156
|
+
"""按当前矩形边界检查最大网格槽位。"""
|
|
157
|
+
_validate_grid_extent(len(grid.rows), grid.width)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def parse_table_grid(
|
|
161
|
+
table: etree._Element,
|
|
162
|
+
render_cell: CellRenderer,
|
|
163
|
+
*,
|
|
164
|
+
expansion_budget: OdfTableExpansionBudget | None = None,
|
|
165
|
+
) -> TableGrid:
|
|
166
|
+
"""展开受限重复行列与合并单元格,构造规范二维网格。"""
|
|
167
|
+
grid = TableGrid()
|
|
168
|
+
reservation = expansion_budget.start_table() if expansion_budget is not None else None
|
|
169
|
+
duplicated_text_bytes = 0
|
|
170
|
+
row_index = 0
|
|
171
|
+
pending_empty_rows = 0
|
|
172
|
+
pending_empty_width = 0
|
|
173
|
+
|
|
174
|
+
def ensure_row(target_row: int, width: int = 0) -> list[GridCell | None]:
|
|
175
|
+
"""在兼容独立表格调用的同时应用可选文档级预留。"""
|
|
176
|
+
if reservation is None:
|
|
177
|
+
return _ensure_row(grid, target_row, width)
|
|
178
|
+
return _ensure_row(grid, target_row, width, reservation)
|
|
179
|
+
|
|
180
|
+
for row_element, header in _iter_rows(table):
|
|
181
|
+
row_repeat = _positive_int(row_element.get(qname("table", "number-rows-repeated")))
|
|
182
|
+
_validate_grid_extent(row_index + row_repeat, grid.width)
|
|
183
|
+
cell_templates: list[tuple[bool, int, GridCell | None]] = []
|
|
184
|
+
for cell in row_element:
|
|
185
|
+
if not isinstance(cell.tag, str):
|
|
186
|
+
continue
|
|
187
|
+
if cell.tag == qname("table", "covered-table-cell"):
|
|
188
|
+
cell_templates.append((True, _positive_int(cell.get(qname("table", "number-columns-repeated"))), None))
|
|
189
|
+
continue
|
|
190
|
+
if cell.tag != qname("table", "table-cell"):
|
|
191
|
+
continue
|
|
192
|
+
column_repeat = _positive_int(cell.get(qname("table", "number-columns-repeated")))
|
|
193
|
+
row_span = _positive_int(cell.get(qname("table", "number-rows-spanned")))
|
|
194
|
+
col_span = _positive_int(cell.get(qname("table", "number-columns-spanned")))
|
|
195
|
+
_validate_grid_extent(row_span, col_span)
|
|
196
|
+
cell_html = render_cell(cell)
|
|
197
|
+
if not _has_visible_html(cell_html):
|
|
198
|
+
typed_value = _typed_value_text(cell)
|
|
199
|
+
cell_html = html.escape(typed_value) if typed_value else ""
|
|
200
|
+
duplicated_bytes = len(cell_html.encode("utf-8")) * max(0, row_repeat * column_repeat - 1)
|
|
201
|
+
if expansion_budget is None:
|
|
202
|
+
duplicated_text_bytes += duplicated_bytes
|
|
203
|
+
if duplicated_text_bytes > MAX_EXPANSION_TEXT_BYTES:
|
|
204
|
+
raise OdfResourceLimitError(
|
|
205
|
+
f"ODF resource limit exceeded: max_expansion_text_bytes={MAX_EXPANSION_TEXT_BYTES}"
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
expansion_budget.charge_duplicated_text(duplicated_bytes)
|
|
209
|
+
cell_templates.append(
|
|
210
|
+
(
|
|
211
|
+
False,
|
|
212
|
+
column_repeat,
|
|
213
|
+
GridCell(html=cell_html, row_span=row_span, col_span=col_span, header=header),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
row_has_content = header or any(
|
|
217
|
+
template is not None and (template.has_content or template.row_span > 1 or template.col_span > 1)
|
|
218
|
+
for _, _, template in cell_templates
|
|
219
|
+
)
|
|
220
|
+
if not row_has_content:
|
|
221
|
+
pending_empty_rows += row_repeat
|
|
222
|
+
pending_empty_width = max(
|
|
223
|
+
pending_empty_width,
|
|
224
|
+
sum(repeat * (template.col_span if template is not None else 1) for _, repeat, template in cell_templates),
|
|
225
|
+
)
|
|
226
|
+
continue
|
|
227
|
+
if pending_empty_rows:
|
|
228
|
+
_validate_grid_extent(row_index + pending_empty_rows, max(grid.width, pending_empty_width))
|
|
229
|
+
for _ in range(pending_empty_rows):
|
|
230
|
+
ensure_row(row_index, pending_empty_width)
|
|
231
|
+
row_index += 1
|
|
232
|
+
pending_empty_rows = 0
|
|
233
|
+
pending_empty_width = 0
|
|
234
|
+
for _ in range(row_repeat):
|
|
235
|
+
row = ensure_row(row_index)
|
|
236
|
+
col_index = 0
|
|
237
|
+
pending_empty_columns = 0
|
|
238
|
+
for is_covered, repeat, template in cell_templates:
|
|
239
|
+
if repeat > MAX_GRID_SLOTS:
|
|
240
|
+
raise OdfResourceLimitError(f"ODF resource limit exceeded: max_grid_slots={MAX_GRID_SLOTS}")
|
|
241
|
+
if (
|
|
242
|
+
not is_covered
|
|
243
|
+
and template is not None
|
|
244
|
+
and not template.has_content
|
|
245
|
+
and template.row_span == 1
|
|
246
|
+
and template.col_span == 1
|
|
247
|
+
):
|
|
248
|
+
pending_empty_columns += repeat
|
|
249
|
+
continue
|
|
250
|
+
if pending_empty_columns:
|
|
251
|
+
col_index += pending_empty_columns
|
|
252
|
+
ensure_row(row_index, col_index)
|
|
253
|
+
pending_empty_columns = 0
|
|
254
|
+
if not is_covered:
|
|
255
|
+
_validate_grid_extent(row_index + 1, max(grid.width, col_index + repeat))
|
|
256
|
+
for _ in range(repeat):
|
|
257
|
+
if is_covered:
|
|
258
|
+
if (row_index, col_index) in grid.covered:
|
|
259
|
+
ensure_row(row_index, col_index + 1)
|
|
260
|
+
col_index += 1
|
|
261
|
+
continue
|
|
262
|
+
while (row_index, col_index) in grid.covered:
|
|
263
|
+
col_index += 1
|
|
264
|
+
assert template is not None
|
|
265
|
+
placed = GridCell(
|
|
266
|
+
html=template.html,
|
|
267
|
+
row_span=template.row_span,
|
|
268
|
+
col_span=template.col_span,
|
|
269
|
+
header=template.header,
|
|
270
|
+
)
|
|
271
|
+
_validate_grid_extent(
|
|
272
|
+
row_index + placed.row_span,
|
|
273
|
+
max(grid.width, col_index + placed.col_span),
|
|
274
|
+
)
|
|
275
|
+
row = ensure_row(row_index, col_index + placed.col_span)
|
|
276
|
+
row[col_index] = placed
|
|
277
|
+
for row_offset in range(placed.row_span):
|
|
278
|
+
covered_row = ensure_row(row_index + row_offset, col_index + placed.col_span)
|
|
279
|
+
for col_offset in range(placed.col_span):
|
|
280
|
+
if row_offset == 0 and col_offset == 0:
|
|
281
|
+
continue
|
|
282
|
+
grid.covered.add((row_index + row_offset, col_index + col_offset))
|
|
283
|
+
if covered_row[col_index + col_offset] is not None:
|
|
284
|
+
covered_row[col_index + col_offset] = None
|
|
285
|
+
col_index += placed.col_span
|
|
286
|
+
if header:
|
|
287
|
+
grid.header_rows = max(grid.header_rows, row_index + 1)
|
|
288
|
+
_charge_grid(grid)
|
|
289
|
+
row_index += 1
|
|
290
|
+
return trim_table_grid(grid)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def trim_table_grid(grid: TableGrid) -> TableGrid:
|
|
294
|
+
"""移除尾部全空行列,同时保留已用范围内部的空白坐标。"""
|
|
295
|
+
last_row = -1
|
|
296
|
+
last_col = -1
|
|
297
|
+
for row_index, row in enumerate(grid.rows):
|
|
298
|
+
for col_index, cell in enumerate(row):
|
|
299
|
+
if cell is not None and (cell.has_content or cell.row_span > 1 or cell.col_span > 1):
|
|
300
|
+
last_row = max(last_row, row_index + cell.row_span - 1)
|
|
301
|
+
last_col = max(last_col, col_index + cell.col_span - 1)
|
|
302
|
+
if last_row < 0 or last_col < 0:
|
|
303
|
+
return TableGrid()
|
|
304
|
+
rows = []
|
|
305
|
+
for row_index in range(min(last_row + 1, len(grid.rows))):
|
|
306
|
+
row = list(grid.rows[row_index][: last_col + 1])
|
|
307
|
+
if len(row) < last_col + 1:
|
|
308
|
+
row.extend([None] * (last_col + 1 - len(row)))
|
|
309
|
+
rows.append(row)
|
|
310
|
+
covered = {(row, col) for row, col in grid.covered if row <= last_row and col <= last_col}
|
|
311
|
+
return TableGrid(rows=rows, header_rows=min(grid.header_rows, len(rows)), covered=covered)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def crop_table_grid(grid: TableGrid, bounds: tuple[int, int, int, int]) -> TableGrid:
|
|
315
|
+
"""按闭区间行列边界裁剪网格并重映射合并占位。"""
|
|
316
|
+
row_start, row_end, col_start, col_end = bounds
|
|
317
|
+
if row_start < 0 or col_start < 0 or row_end < row_start or col_end < col_start:
|
|
318
|
+
return TableGrid()
|
|
319
|
+
rows: list[list[GridCell | None]] = []
|
|
320
|
+
for source_row in range(row_start, min(row_end + 1, len(grid.rows))):
|
|
321
|
+
row = grid.rows[source_row]
|
|
322
|
+
selected = list(row[col_start : col_end + 1])
|
|
323
|
+
if len(selected) < col_end - col_start + 1:
|
|
324
|
+
selected.extend([None] * (col_end - col_start + 1 - len(selected)))
|
|
325
|
+
rows.append(selected)
|
|
326
|
+
covered = {
|
|
327
|
+
(row - row_start, col - col_start)
|
|
328
|
+
for row, col in grid.covered
|
|
329
|
+
if row_start <= row <= row_end and col_start <= col <= col_end
|
|
330
|
+
}
|
|
331
|
+
header_rows = max(0, min(grid.header_rows - row_start, len(rows)))
|
|
332
|
+
return trim_table_grid(TableGrid(rows=rows, header_rows=header_rows, covered=covered))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def split_table_regions(grid: TableGrid) -> list[TableGrid]:
|
|
336
|
+
"""按至少两条全空行列分隔电子表格中的离散数据区域。"""
|
|
337
|
+
nonempty = [
|
|
338
|
+
(row_index, col_index)
|
|
339
|
+
for row_index, row in enumerate(grid.rows)
|
|
340
|
+
for col_index, cell in enumerate(row)
|
|
341
|
+
if cell is not None and cell.has_content
|
|
342
|
+
]
|
|
343
|
+
if not nonempty:
|
|
344
|
+
return []
|
|
345
|
+
row_values = sorted({row for row, _ in nonempty})
|
|
346
|
+
row_bands: list[tuple[int, int]] = []
|
|
347
|
+
start = previous = row_values[0]
|
|
348
|
+
for current in row_values[1:]:
|
|
349
|
+
if current - previous > 2:
|
|
350
|
+
row_bands.append((start, previous))
|
|
351
|
+
start = current
|
|
352
|
+
previous = current
|
|
353
|
+
row_bands.append((start, previous))
|
|
354
|
+
result: list[TableGrid] = []
|
|
355
|
+
for row_start, row_end in row_bands:
|
|
356
|
+
cols = sorted({col for row, col in nonempty if row_start <= row <= row_end})
|
|
357
|
+
col_start = col_previous = cols[0]
|
|
358
|
+
for current in cols[1:] + [cols[-1] + 3]:
|
|
359
|
+
if current - col_previous > 2:
|
|
360
|
+
region = crop_table_grid(grid, (row_start, row_end, col_start, col_previous))
|
|
361
|
+
if region.rows:
|
|
362
|
+
result.append(region)
|
|
363
|
+
col_start = current
|
|
364
|
+
col_previous = current
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _render_html_row(grid: TableGrid, row_index: int, *, header: bool) -> str:
|
|
369
|
+
"""把网格中的一行序列化为 tr,并跳过合并占位。"""
|
|
370
|
+
row = grid.rows[row_index]
|
|
371
|
+
tag = "th" if header else "td"
|
|
372
|
+
parts = ["<tr>"]
|
|
373
|
+
for col_index in range(grid.width):
|
|
374
|
+
if (row_index, col_index) in grid.covered:
|
|
375
|
+
continue
|
|
376
|
+
cell = row[col_index] if col_index < len(row) else None
|
|
377
|
+
attrs = ""
|
|
378
|
+
content = ""
|
|
379
|
+
if cell is not None:
|
|
380
|
+
if cell.row_span > 1:
|
|
381
|
+
attrs += f' rowspan="{cell.row_span}"'
|
|
382
|
+
if cell.col_span > 1:
|
|
383
|
+
attrs += f' colspan="{cell.col_span}"'
|
|
384
|
+
content = cell.html
|
|
385
|
+
parts.append(f"<{tag}{attrs}>{content}</{tag}>")
|
|
386
|
+
parts.append("</tr>")
|
|
387
|
+
return "".join(parts)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def table_grid_to_html(grid: TableGrid) -> str:
|
|
391
|
+
"""把规范网格稳定序列化为带 thead/tbody 和跨度的 HTML 表格。"""
|
|
392
|
+
if not grid.rows:
|
|
393
|
+
return ""
|
|
394
|
+
header_rows = min(grid.header_rows, len(grid.rows))
|
|
395
|
+
parts = ["<table>"]
|
|
396
|
+
if header_rows:
|
|
397
|
+
parts.append("<thead>")
|
|
398
|
+
parts.extend(_render_html_row(grid, row_index, header=True) for row_index in range(header_rows))
|
|
399
|
+
parts.append("</thead>")
|
|
400
|
+
if header_rows < len(grid.rows):
|
|
401
|
+
parts.append("<tbody>")
|
|
402
|
+
parts.extend(_render_html_row(grid, row_index, header=False) for row_index in range(header_rows, len(grid.rows)))
|
|
403
|
+
parts.append("</tbody>")
|
|
404
|
+
parts.append("</table>")
|
|
405
|
+
return "".join(parts)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _column_index(label: str) -> int | None:
|
|
409
|
+
"""在共享网格预算内把 A1 地址中的列字母转换为零基列号。"""
|
|
410
|
+
max_label_length = 0
|
|
411
|
+
remaining = MAX_GRID_SLOTS
|
|
412
|
+
while remaining > 0:
|
|
413
|
+
max_label_length += 1
|
|
414
|
+
remaining = (remaining - 1) // 26
|
|
415
|
+
if not label or len(label) > max_label_length:
|
|
416
|
+
return None
|
|
417
|
+
result = 0
|
|
418
|
+
for char in label.upper():
|
|
419
|
+
result = result * 26 + ord(char) - ord("A") + 1
|
|
420
|
+
return result - 1 if result <= MAX_GRID_SLOTS else None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _row_index(label: str) -> int | None:
|
|
424
|
+
"""在整数转换前把 A1 地址中的行号约束到共享网格预算。"""
|
|
425
|
+
normalized = label.lstrip("0")
|
|
426
|
+
if not normalized or len(normalized) > len(str(MAX_GRID_SLOTS)):
|
|
427
|
+
return None
|
|
428
|
+
result = int(normalized)
|
|
429
|
+
return result - 1 if result <= MAX_GRID_SLOTS else None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def parse_cell_range_bounds(address: str) -> tuple[int, int, int, int] | None:
|
|
433
|
+
"""从 ODF cell-range-address 中提取零基闭区间边界。"""
|
|
434
|
+
matches = list(_CELL_ADDRESS_RE.finditer(address or ""))
|
|
435
|
+
if not matches:
|
|
436
|
+
return None
|
|
437
|
+
first = matches[0]
|
|
438
|
+
last = matches[-1]
|
|
439
|
+
row_start = _row_index(first.group("row"))
|
|
440
|
+
col_start = _column_index(first.group("col"))
|
|
441
|
+
row_end = _row_index(last.group("row"))
|
|
442
|
+
col_end = _column_index(last.group("col"))
|
|
443
|
+
if row_start is None or col_start is None or row_end is None or col_end is None:
|
|
444
|
+
return None
|
|
445
|
+
return min(row_start, row_end), max(row_start, row_end), min(col_start, col_end), max(col_start, col_end)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def union_bounds(bounds: list[tuple[int, int, int, int]]) -> tuple[int, int, int, int] | None:
|
|
449
|
+
"""返回多个表格范围的最小包围矩形。"""
|
|
450
|
+
if not bounds:
|
|
451
|
+
return None
|
|
452
|
+
return (
|
|
453
|
+
min(item[0] for item in bounds),
|
|
454
|
+
max(item[1] for item in bounds),
|
|
455
|
+
min(item[2] for item in bounds),
|
|
456
|
+
max(item[3] for item in bounds),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
__all__ = [
|
|
461
|
+
"OdfTableExpansionBudget",
|
|
462
|
+
"crop_table_grid",
|
|
463
|
+
"parse_cell_range_bounds",
|
|
464
|
+
"parse_table_grid",
|
|
465
|
+
"split_table_regions",
|
|
466
|
+
"table_grid_to_html",
|
|
467
|
+
"trim_table_grid",
|
|
468
|
+
"union_bounds",
|
|
469
|
+
]
|