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,195 @@
|
|
|
1
|
+
"""DOCX renderer 的页面几何与 Word 样式定义。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from docx.document import Document
|
|
6
|
+
from docx.enum.style import WD_STYLE_TYPE
|
|
7
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
8
|
+
from docx.oxml import OxmlElement
|
|
9
|
+
from docx.oxml.ns import qn
|
|
10
|
+
from docx.shared import Mm, Pt, RGBColor
|
|
11
|
+
from docx.styles.style import _ParagraphStyle
|
|
12
|
+
|
|
13
|
+
BODY_STYLE = "Normal"
|
|
14
|
+
CODE_STYLE = "DocVortex Code"
|
|
15
|
+
SPATIAL_TABLE_STYLE = "DocVortex Spatial Table"
|
|
16
|
+
CAPTION_STYLE = "DocVortex Caption"
|
|
17
|
+
FOOTNOTE_STYLE = "DocVortex Footnote"
|
|
18
|
+
FORMULA_FALLBACK_STYLE = "DocVortex Formula Fallback"
|
|
19
|
+
|
|
20
|
+
_HEADING_SIZES = (20, 18, 16, 14, 13, 12, 11, 10.5, 10.5)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure_document(document: Document) -> None:
|
|
24
|
+
"""设置 A4 页面几何,并创建 renderer 使用的全部显式样式。"""
|
|
25
|
+
section = document.sections[0]
|
|
26
|
+
section.page_width = Mm(210)
|
|
27
|
+
section.page_height = Mm(297)
|
|
28
|
+
section.top_margin = Mm(20)
|
|
29
|
+
section.bottom_margin = Mm(20)
|
|
30
|
+
section.left_margin = Mm(20)
|
|
31
|
+
section.right_margin = Mm(20)
|
|
32
|
+
|
|
33
|
+
normal = document.styles[BODY_STYLE]
|
|
34
|
+
_configure_paragraph_style(
|
|
35
|
+
normal,
|
|
36
|
+
western_font="Times New Roman",
|
|
37
|
+
east_asia_font="宋体",
|
|
38
|
+
size_pt=10.5,
|
|
39
|
+
space_before_pt=0,
|
|
40
|
+
space_after_pt=6,
|
|
41
|
+
line_spacing=1.15,
|
|
42
|
+
)
|
|
43
|
+
for level, size_pt in enumerate(_HEADING_SIZES, start=1):
|
|
44
|
+
heading = document.styles[f"Heading {level}"]
|
|
45
|
+
_configure_paragraph_style(
|
|
46
|
+
heading,
|
|
47
|
+
western_font="Arial",
|
|
48
|
+
east_asia_font="黑体",
|
|
49
|
+
size_pt=size_pt,
|
|
50
|
+
bold=True,
|
|
51
|
+
space_before_pt=10 if level <= 3 else 8,
|
|
52
|
+
space_after_pt=5 if level <= 3 else 4,
|
|
53
|
+
line_spacing=1.0,
|
|
54
|
+
)
|
|
55
|
+
heading.paragraph_format.keep_with_next = True
|
|
56
|
+
|
|
57
|
+
code = _get_or_add_paragraph_style(document, CODE_STYLE)
|
|
58
|
+
_configure_paragraph_style(
|
|
59
|
+
code,
|
|
60
|
+
western_font="Courier New",
|
|
61
|
+
east_asia_font="宋体",
|
|
62
|
+
size_pt=9,
|
|
63
|
+
space_before_pt=4,
|
|
64
|
+
space_after_pt=6,
|
|
65
|
+
line_spacing=1.0,
|
|
66
|
+
)
|
|
67
|
+
_set_paragraph_shading(code, "F3F4F6")
|
|
68
|
+
|
|
69
|
+
spatial_table = _get_or_add_paragraph_style(document, SPATIAL_TABLE_STYLE)
|
|
70
|
+
_configure_paragraph_style(
|
|
71
|
+
spatial_table,
|
|
72
|
+
western_font="Courier New",
|
|
73
|
+
east_asia_font="宋体",
|
|
74
|
+
size_pt=9,
|
|
75
|
+
space_before_pt=4,
|
|
76
|
+
space_after_pt=6,
|
|
77
|
+
line_spacing=1.0,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
caption = _get_or_add_paragraph_style(document, CAPTION_STYLE)
|
|
81
|
+
_configure_paragraph_style(
|
|
82
|
+
caption,
|
|
83
|
+
western_font="Times New Roman",
|
|
84
|
+
east_asia_font="宋体",
|
|
85
|
+
size_pt=9,
|
|
86
|
+
space_before_pt=3,
|
|
87
|
+
space_after_pt=5,
|
|
88
|
+
line_spacing=1.0,
|
|
89
|
+
)
|
|
90
|
+
caption.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
91
|
+
caption.paragraph_format.keep_with_next = True
|
|
92
|
+
|
|
93
|
+
footnote = _get_or_add_paragraph_style(document, FOOTNOTE_STYLE)
|
|
94
|
+
_configure_paragraph_style(
|
|
95
|
+
footnote,
|
|
96
|
+
western_font="Times New Roman",
|
|
97
|
+
east_asia_font="宋体",
|
|
98
|
+
size_pt=8,
|
|
99
|
+
color="666666",
|
|
100
|
+
space_before_pt=2,
|
|
101
|
+
space_after_pt=4,
|
|
102
|
+
line_spacing=1.0,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
formula_fallback = _get_or_add_paragraph_style(document, FORMULA_FALLBACK_STYLE)
|
|
106
|
+
_configure_paragraph_style(
|
|
107
|
+
formula_fallback,
|
|
108
|
+
western_font="Courier New",
|
|
109
|
+
east_asia_font="宋体",
|
|
110
|
+
size_pt=9,
|
|
111
|
+
color="555555",
|
|
112
|
+
space_before_pt=4,
|
|
113
|
+
space_after_pt=5,
|
|
114
|
+
line_spacing=1.0,
|
|
115
|
+
)
|
|
116
|
+
formula_fallback.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def usable_width_twips(document: Document) -> int:
|
|
120
|
+
"""返回首个 section 扣除左右页边距后的可用宽度,单位 twip。"""
|
|
121
|
+
section = document.sections[0]
|
|
122
|
+
usable_emu = int(section.page_width) - int(section.left_margin) - int(section.right_margin)
|
|
123
|
+
return max(1, round(usable_emu / 635))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def usable_width_emu(document: Document) -> int:
|
|
127
|
+
"""返回首个 section 扣除左右页边距后的可用宽度,单位 EMU。"""
|
|
128
|
+
section = document.sections[0]
|
|
129
|
+
return max(1, int(section.page_width) - int(section.left_margin) - int(section.right_margin))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_or_add_paragraph_style(document: Document, name: str) -> _ParagraphStyle:
|
|
133
|
+
"""获取既有段落样式,缺失时创建同名样式。"""
|
|
134
|
+
styles = document.styles
|
|
135
|
+
try:
|
|
136
|
+
style = styles[name]
|
|
137
|
+
except KeyError:
|
|
138
|
+
style = styles.add_style(name, WD_STYLE_TYPE.PARAGRAPH)
|
|
139
|
+
if not isinstance(style, _ParagraphStyle):
|
|
140
|
+
raise TypeError(f"Style is not a paragraph style: {name}")
|
|
141
|
+
return style
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _configure_paragraph_style(
|
|
145
|
+
style: _ParagraphStyle,
|
|
146
|
+
*,
|
|
147
|
+
western_font: str,
|
|
148
|
+
east_asia_font: str,
|
|
149
|
+
size_pt: float,
|
|
150
|
+
bold: bool = False,
|
|
151
|
+
italic: bool = False,
|
|
152
|
+
color: str | None = None,
|
|
153
|
+
space_before_pt: float,
|
|
154
|
+
space_after_pt: float,
|
|
155
|
+
line_spacing: float,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""给一个 Word 段落样式写入确定性的字体与段落节奏。"""
|
|
158
|
+
style.font.name = western_font
|
|
159
|
+
style.font.size = Pt(size_pt)
|
|
160
|
+
style.font.bold = bold
|
|
161
|
+
style.font.italic = italic
|
|
162
|
+
if color is not None:
|
|
163
|
+
style.font.color.rgb = RGBColor.from_string(color)
|
|
164
|
+
run_properties = style._element.get_or_add_rPr()
|
|
165
|
+
run_fonts = run_properties.get_or_add_rFonts()
|
|
166
|
+
run_fonts.set(qn("w:ascii"), western_font)
|
|
167
|
+
run_fonts.set(qn("w:hAnsi"), western_font)
|
|
168
|
+
run_fonts.set(qn("w:eastAsia"), east_asia_font)
|
|
169
|
+
paragraph_format = style.paragraph_format
|
|
170
|
+
paragraph_format.space_before = Pt(space_before_pt)
|
|
171
|
+
paragraph_format.space_after = Pt(space_after_pt)
|
|
172
|
+
paragraph_format.line_spacing = line_spacing
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _set_paragraph_shading(style: _ParagraphStyle, fill: str) -> None:
|
|
176
|
+
"""给段落样式设置稳定的背景色。"""
|
|
177
|
+
paragraph_properties = style._element.get_or_add_pPr()
|
|
178
|
+
shading = paragraph_properties.find(qn("w:shd"))
|
|
179
|
+
if shading is None:
|
|
180
|
+
shading = OxmlElement("w:shd")
|
|
181
|
+
paragraph_properties.append(shading)
|
|
182
|
+
shading.set(qn("w:fill"), fill)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__all__ = [
|
|
186
|
+
"BODY_STYLE",
|
|
187
|
+
"CAPTION_STYLE",
|
|
188
|
+
"CODE_STYLE",
|
|
189
|
+
"FOOTNOTE_STYLE",
|
|
190
|
+
"FORMULA_FALLBACK_STYLE",
|
|
191
|
+
"SPATIAL_TABLE_STYLE",
|
|
192
|
+
"configure_document",
|
|
193
|
+
"usable_width_emu",
|
|
194
|
+
"usable_width_twips",
|
|
195
|
+
]
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""DOCX renderer 的 HTML 表格占位网格解析与原生表格物化。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Protocol, TypeAlias
|
|
8
|
+
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
from bs4.element import Tag
|
|
11
|
+
from docx.document import Document as DocxDocument
|
|
12
|
+
from docx.exceptions import InvalidSpanError
|
|
13
|
+
from docx.oxml import OxmlElement
|
|
14
|
+
from docx.oxml.ns import qn
|
|
15
|
+
from docx.shared import Twips
|
|
16
|
+
from docx.table import Table, _Cell
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_TABLE_WIDTH_TWIPS = 8640
|
|
20
|
+
MAX_NESTED_TABLE_DEPTH = 4
|
|
21
|
+
_POSITIVE_INTEGER_RE = re.compile(r"[0-9]+")
|
|
22
|
+
|
|
23
|
+
HtmlTableSource: TypeAlias = str | BeautifulSoup | Tag
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DocxTableError(ValueError):
|
|
27
|
+
"""表示 HTML 表格结构或 DOCX 表格几何无法安全物化。"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class HtmlTableCell:
|
|
32
|
+
"""保存一个 HTML 原始单元格在逻辑占位网格中的位置。"""
|
|
33
|
+
|
|
34
|
+
tag: Tag
|
|
35
|
+
row: int
|
|
36
|
+
column: int
|
|
37
|
+
rowspan: int
|
|
38
|
+
colspan: int
|
|
39
|
+
is_header: bool
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def end_row(self) -> int:
|
|
43
|
+
"""返回单元格占用的末行下标。"""
|
|
44
|
+
return self.row + self.rowspan - 1
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def end_column(self) -> int:
|
|
48
|
+
"""返回单元格占用的末列下标。"""
|
|
49
|
+
return self.column + self.colspan - 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class HtmlTableGrid:
|
|
54
|
+
"""保存一个经过完整矩形校验的 HTML 表格占位网格。"""
|
|
55
|
+
|
|
56
|
+
tag: Tag
|
|
57
|
+
row_count: int
|
|
58
|
+
column_count: int
|
|
59
|
+
cells: tuple[HtmlTableCell, ...]
|
|
60
|
+
occupancy: tuple[tuple[HtmlTableCell, ...], ...]
|
|
61
|
+
header_rows: tuple[int, ...]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NestedTableWriter(Protocol):
|
|
65
|
+
"""定义单元格回调可调用的嵌套表格写入函数。"""
|
|
66
|
+
|
|
67
|
+
def __call__(
|
|
68
|
+
self,
|
|
69
|
+
source: HtmlTableSource,
|
|
70
|
+
width_twips: int | None = None,
|
|
71
|
+
) -> tuple[Table, ...]:
|
|
72
|
+
"""把 source 中的直接嵌套表格写入当前 Word 单元格。"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CellFillCallback(Protocol):
|
|
77
|
+
"""定义由上层 renderer 填充原始单元格内容的回调。"""
|
|
78
|
+
|
|
79
|
+
def __call__(
|
|
80
|
+
self,
|
|
81
|
+
cell: _Cell,
|
|
82
|
+
source: Tag,
|
|
83
|
+
write_nested: NestedTableWriter,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""使用原始 td/th 标签填充 origin cell,并按需递归写入嵌套表格。"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_html_table(table: Tag) -> HtmlTableGrid:
|
|
90
|
+
"""把单个 table 标签解析为经过重叠、越界和矩形校验的占位网格。"""
|
|
91
|
+
if table.name != "table":
|
|
92
|
+
raise DocxTableError("Expected a <table> tag")
|
|
93
|
+
|
|
94
|
+
rows = tuple(row for row in table.find_all("tr") if row.find_parent("table") is table)
|
|
95
|
+
if not rows:
|
|
96
|
+
raise DocxTableError("Table must contain at least one row")
|
|
97
|
+
|
|
98
|
+
occupied: dict[tuple[int, int], HtmlTableCell] = {}
|
|
99
|
+
cells: list[HtmlTableCell] = []
|
|
100
|
+
|
|
101
|
+
for row_index, row in enumerate(rows):
|
|
102
|
+
column_index = 0
|
|
103
|
+
source_cells = row.find_all(("td", "th"), recursive=False)
|
|
104
|
+
for source_cell in source_cells:
|
|
105
|
+
while (row_index, column_index) in occupied:
|
|
106
|
+
column_index += 1
|
|
107
|
+
|
|
108
|
+
rowspan = _parse_span(source_cell, "rowspan")
|
|
109
|
+
colspan = _parse_span(source_cell, "colspan")
|
|
110
|
+
if row_index + rowspan > len(rows):
|
|
111
|
+
raise DocxTableError(f"rowspan exceeds table bounds at row={row_index}, column={column_index}")
|
|
112
|
+
|
|
113
|
+
coordinates = tuple(
|
|
114
|
+
(target_row, target_column)
|
|
115
|
+
for target_row in range(row_index, row_index + rowspan)
|
|
116
|
+
for target_column in range(column_index, column_index + colspan)
|
|
117
|
+
)
|
|
118
|
+
overlap = next((coordinate for coordinate in coordinates if coordinate in occupied), None)
|
|
119
|
+
if overlap is not None:
|
|
120
|
+
raise DocxTableError(f"Cell span overlaps an occupied coordinate at row={overlap[0]}, column={overlap[1]}")
|
|
121
|
+
|
|
122
|
+
placement = HtmlTableCell(
|
|
123
|
+
tag=source_cell,
|
|
124
|
+
row=row_index,
|
|
125
|
+
column=column_index,
|
|
126
|
+
rowspan=rowspan,
|
|
127
|
+
colspan=colspan,
|
|
128
|
+
is_header=source_cell.name == "th",
|
|
129
|
+
)
|
|
130
|
+
cells.append(placement)
|
|
131
|
+
occupied.update(dict.fromkeys(coordinates, placement))
|
|
132
|
+
column_index += colspan
|
|
133
|
+
|
|
134
|
+
if not occupied:
|
|
135
|
+
raise DocxTableError("Table must contain at least one cell")
|
|
136
|
+
|
|
137
|
+
column_count = max(column for _, column in occupied) + 1
|
|
138
|
+
missing = next(
|
|
139
|
+
(
|
|
140
|
+
(row_index, column_index)
|
|
141
|
+
for row_index in range(len(rows))
|
|
142
|
+
for column_index in range(column_count)
|
|
143
|
+
if (row_index, column_index) not in occupied
|
|
144
|
+
),
|
|
145
|
+
None,
|
|
146
|
+
)
|
|
147
|
+
if missing is not None:
|
|
148
|
+
raise DocxTableError(f"Table occupancy must be rectangular; missing row={missing[0]}, column={missing[1]}")
|
|
149
|
+
|
|
150
|
+
occupancy = tuple(
|
|
151
|
+
tuple(occupied[(row_index, column_index)] for column_index in range(column_count)) for row_index in range(len(rows))
|
|
152
|
+
)
|
|
153
|
+
header_rows = tuple(
|
|
154
|
+
row_index
|
|
155
|
+
for row_index, row in enumerate(rows)
|
|
156
|
+
if _row_belongs_to_thead(row, table) or all(cell.is_header for cell in occupancy[row_index])
|
|
157
|
+
)
|
|
158
|
+
return HtmlTableGrid(
|
|
159
|
+
tag=table,
|
|
160
|
+
row_count=len(rows),
|
|
161
|
+
column_count=column_count,
|
|
162
|
+
cells=tuple(cells),
|
|
163
|
+
occupancy=occupancy,
|
|
164
|
+
header_rows=header_rows,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def parse_html_tables(source: HtmlTableSource) -> tuple[HtmlTableGrid, ...]:
|
|
169
|
+
"""解析 source 中相对当前上下文的一个或多个顶层 table。"""
|
|
170
|
+
root = BeautifulSoup(source, "html.parser") if isinstance(source, str) else source
|
|
171
|
+
if not isinstance(root, (BeautifulSoup, Tag)):
|
|
172
|
+
raise DocxTableError("HTML table source must be a string or BeautifulSoup Tag")
|
|
173
|
+
|
|
174
|
+
if isinstance(root, Tag) and root.name == "table":
|
|
175
|
+
table_tags = (root,)
|
|
176
|
+
else:
|
|
177
|
+
parent_table = root.find_parent("table") if isinstance(root, Tag) else None
|
|
178
|
+
table_tags = tuple(table for table in root.find_all("table") if table.find_parent("table") is parent_table)
|
|
179
|
+
if not table_tags:
|
|
180
|
+
raise DocxTableError("HTML does not contain a top-level table")
|
|
181
|
+
return tuple(parse_html_table(table) for table in table_tags)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def materialize_docx_tables(
|
|
185
|
+
container: Any,
|
|
186
|
+
source: HtmlTableSource,
|
|
187
|
+
*,
|
|
188
|
+
width_twips: int = DEFAULT_TABLE_WIDTH_TWIPS,
|
|
189
|
+
fill_cell: CellFillCallback,
|
|
190
|
+
) -> tuple[Table, ...]:
|
|
191
|
+
"""把 source 中的全部顶层表格物化到 Document、Header/Footer 或 Cell。"""
|
|
192
|
+
grids = parse_html_tables(source)
|
|
193
|
+
return _materialize_grids(
|
|
194
|
+
container,
|
|
195
|
+
grids,
|
|
196
|
+
width_twips=width_twips,
|
|
197
|
+
fill_cell=fill_cell,
|
|
198
|
+
depth=1,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def materialize_docx_table(
|
|
203
|
+
container: Any,
|
|
204
|
+
grid: HtmlTableGrid,
|
|
205
|
+
*,
|
|
206
|
+
width_twips: int = DEFAULT_TABLE_WIDTH_TWIPS,
|
|
207
|
+
fill_cell: CellFillCallback,
|
|
208
|
+
) -> Table:
|
|
209
|
+
"""把一个已解析表格网格物化到指定 python-docx 容器。"""
|
|
210
|
+
return _materialize_grid(
|
|
211
|
+
container,
|
|
212
|
+
grid,
|
|
213
|
+
width_twips=width_twips,
|
|
214
|
+
fill_cell=fill_cell,
|
|
215
|
+
depth=1,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _materialize_grids(
|
|
220
|
+
container: Any,
|
|
221
|
+
grids: tuple[HtmlTableGrid, ...],
|
|
222
|
+
*,
|
|
223
|
+
width_twips: int,
|
|
224
|
+
fill_cell: CellFillCallback,
|
|
225
|
+
depth: int,
|
|
226
|
+
) -> tuple[Table, ...]:
|
|
227
|
+
"""在同一递归层级内按源码顺序物化全部表格网格。"""
|
|
228
|
+
if depth > MAX_NESTED_TABLE_DEPTH:
|
|
229
|
+
raise DocxTableError(f"Nested table depth exceeds {MAX_NESTED_TABLE_DEPTH}")
|
|
230
|
+
_validate_width(width_twips)
|
|
231
|
+
return tuple(
|
|
232
|
+
_materialize_grid(
|
|
233
|
+
container,
|
|
234
|
+
grid,
|
|
235
|
+
width_twips=width_twips,
|
|
236
|
+
fill_cell=fill_cell,
|
|
237
|
+
depth=depth,
|
|
238
|
+
)
|
|
239
|
+
for grid in grids
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _materialize_grid(
|
|
244
|
+
container: Any,
|
|
245
|
+
grid: HtmlTableGrid,
|
|
246
|
+
*,
|
|
247
|
+
width_twips: int,
|
|
248
|
+
fill_cell: CellFillCallback,
|
|
249
|
+
depth: int,
|
|
250
|
+
) -> Table:
|
|
251
|
+
"""创建单个 Word 表格、应用合并几何并回调填充所有 origin cell。"""
|
|
252
|
+
if depth > MAX_NESTED_TABLE_DEPTH:
|
|
253
|
+
raise DocxTableError(f"Nested table depth exceeds {MAX_NESTED_TABLE_DEPTH}")
|
|
254
|
+
column_widths = _split_width(width_twips, grid.column_count)
|
|
255
|
+
table = _add_table(container, grid.row_count, grid.column_count, width_twips)
|
|
256
|
+
_configure_table_geometry(table, width_twips, column_widths)
|
|
257
|
+
_apply_cell_merges(table, grid)
|
|
258
|
+
_normalize_cell_widths(table, column_widths)
|
|
259
|
+
_clear_fixed_row_heights(table)
|
|
260
|
+
_mark_header_rows(table, grid.header_rows)
|
|
261
|
+
|
|
262
|
+
for placement in grid.cells:
|
|
263
|
+
origin_cell = table.cell(placement.row, placement.column)
|
|
264
|
+
origin_width = sum(column_widths[placement.column : placement.column + placement.colspan])
|
|
265
|
+
|
|
266
|
+
def write_nested(
|
|
267
|
+
nested_source: HtmlTableSource,
|
|
268
|
+
nested_width_twips: int | None = None,
|
|
269
|
+
*,
|
|
270
|
+
_origin_cell: _Cell = origin_cell,
|
|
271
|
+
_origin_width: int = origin_width,
|
|
272
|
+
_depth: int = depth,
|
|
273
|
+
) -> tuple[Table, ...]:
|
|
274
|
+
"""在当前 origin cell 内继续物化直接嵌套表格。"""
|
|
275
|
+
if _depth >= MAX_NESTED_TABLE_DEPTH:
|
|
276
|
+
raise DocxTableError(f"Nested table depth exceeds {MAX_NESTED_TABLE_DEPTH}")
|
|
277
|
+
nested_grids = parse_html_tables(nested_source)
|
|
278
|
+
return _materialize_grids(
|
|
279
|
+
_origin_cell,
|
|
280
|
+
nested_grids,
|
|
281
|
+
width_twips=_origin_width if nested_width_twips is None else nested_width_twips,
|
|
282
|
+
fill_cell=fill_cell,
|
|
283
|
+
depth=_depth + 1,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
fill_cell(origin_cell, placement.tag, write_nested)
|
|
287
|
+
return table
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _parse_span(cell: Tag, attribute: str) -> int:
|
|
291
|
+
"""读取严格正整数 rowspan/colspan,缺失属性时返回一。"""
|
|
292
|
+
raw_value = cell.get(attribute, "1")
|
|
293
|
+
if isinstance(raw_value, list):
|
|
294
|
+
raise DocxTableError(f"Invalid {attribute}: {raw_value!r}")
|
|
295
|
+
value = str(raw_value).strip()
|
|
296
|
+
if _POSITIVE_INTEGER_RE.fullmatch(value) is None:
|
|
297
|
+
raise DocxTableError(f"Invalid {attribute}: {raw_value!r}")
|
|
298
|
+
span = int(value)
|
|
299
|
+
if span < 1:
|
|
300
|
+
raise DocxTableError(f"Invalid {attribute}: {raw_value!r}")
|
|
301
|
+
return span
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _row_belongs_to_thead(row: Tag, table: Tag) -> bool:
|
|
305
|
+
"""判断当前 tr 是否位于本 table 的 thead 内。"""
|
|
306
|
+
parent = row.parent
|
|
307
|
+
while isinstance(parent, Tag) and parent is not table:
|
|
308
|
+
if parent.name == "thead":
|
|
309
|
+
return True
|
|
310
|
+
parent = parent.parent
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _validate_width(width_twips: int) -> None:
|
|
315
|
+
"""校验调用方传入的 DXA/twips 表格宽度。"""
|
|
316
|
+
if isinstance(width_twips, bool) or not isinstance(width_twips, int) or width_twips <= 0:
|
|
317
|
+
raise DocxTableError("width_twips must be a positive integer")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _split_width(width_twips: int, column_count: int) -> tuple[int, ...]:
|
|
321
|
+
"""把总宽度确定性地均分到列,并保证列宽之和严格等于总宽。"""
|
|
322
|
+
_validate_width(width_twips)
|
|
323
|
+
if column_count <= 0:
|
|
324
|
+
raise DocxTableError("Table must contain at least one column")
|
|
325
|
+
if width_twips < column_count:
|
|
326
|
+
raise DocxTableError("width_twips is too small for the table column count")
|
|
327
|
+
base_width, remainder = divmod(width_twips, column_count)
|
|
328
|
+
return tuple(base_width + (1 if column_index < remainder else 0) for column_index in range(column_count))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _add_table(container: Any, row_count: int, column_count: int, width_twips: int) -> Table:
|
|
332
|
+
"""按 python-docx 容器的不同 add_table 签名创建空表格。"""
|
|
333
|
+
if not hasattr(container, "add_table"):
|
|
334
|
+
raise DocxTableError("DOCX container must provide add_table()")
|
|
335
|
+
try:
|
|
336
|
+
if isinstance(container, (DocxDocument, _Cell)):
|
|
337
|
+
return container.add_table(rows=row_count, cols=column_count)
|
|
338
|
+
return container.add_table(
|
|
339
|
+
rows=row_count,
|
|
340
|
+
cols=column_count,
|
|
341
|
+
width=Twips(width_twips),
|
|
342
|
+
)
|
|
343
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
344
|
+
raise DocxTableError("Failed to create DOCX table in the target container") from exc
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _configure_table_geometry(
|
|
348
|
+
table: Table,
|
|
349
|
+
width_twips: int,
|
|
350
|
+
column_widths: tuple[int, ...],
|
|
351
|
+
) -> None:
|
|
352
|
+
"""设置固定布局、Table Grid 样式及确定性的 tblW、tblGrid、tcW。"""
|
|
353
|
+
try:
|
|
354
|
+
table.style = "Table Grid"
|
|
355
|
+
except KeyError as exc:
|
|
356
|
+
raise DocxTableError("DOCX template does not define the 'Table Grid' style") from exc
|
|
357
|
+
table.autofit = False
|
|
358
|
+
|
|
359
|
+
table_width = table._tbl.tblPr.find(qn("w:tblW"))
|
|
360
|
+
if table_width is None:
|
|
361
|
+
table_width = OxmlElement("w:tblW")
|
|
362
|
+
table._tbl.tblPr.insert(0, table_width)
|
|
363
|
+
table_width.set(qn("w:type"), "dxa")
|
|
364
|
+
table_width.set(qn("w:w"), str(width_twips))
|
|
365
|
+
|
|
366
|
+
grid_columns = table._tbl.tblGrid.gridCol_lst
|
|
367
|
+
if len(grid_columns) != len(column_widths):
|
|
368
|
+
raise DocxTableError("DOCX table grid column count is inconsistent")
|
|
369
|
+
for grid_column, column_width in zip(grid_columns, column_widths):
|
|
370
|
+
grid_column.w = Twips(column_width)
|
|
371
|
+
|
|
372
|
+
for row in table._tbl.tr_lst:
|
|
373
|
+
if len(row.tc_lst) != len(column_widths):
|
|
374
|
+
raise DocxTableError("DOCX table row is inconsistent before cell merging")
|
|
375
|
+
for cell, column_width in zip(row.tc_lst, column_widths):
|
|
376
|
+
cell_width = cell.get_or_add_tcPr().get_or_add_tcW()
|
|
377
|
+
cell_width.type = "dxa"
|
|
378
|
+
cell_width.w = column_width
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _apply_cell_merges(table: Table, grid: HtmlTableGrid) -> None:
|
|
382
|
+
"""按已验证 origin placement 为 Word 表格应用水平和垂直合并。"""
|
|
383
|
+
for placement in grid.cells:
|
|
384
|
+
if placement.rowspan == 1 and placement.colspan == 1:
|
|
385
|
+
continue
|
|
386
|
+
try:
|
|
387
|
+
table.cell(placement.row, placement.column).merge(table.cell(placement.end_row, placement.end_column))
|
|
388
|
+
except (IndexError, InvalidSpanError, ValueError) as exc:
|
|
389
|
+
raise DocxTableError(f"Failed to merge cell at row={placement.row}, column={placement.column}") from exc
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _normalize_cell_widths(table: Table, column_widths: tuple[int, ...]) -> None:
|
|
393
|
+
"""合并后按 gridSpan 重新写入每个物理 tc 的确定性 DXA 宽度。"""
|
|
394
|
+
for row_index, row in enumerate(table._tbl.tr_lst):
|
|
395
|
+
column_index = 0
|
|
396
|
+
for cell in row.tc_lst:
|
|
397
|
+
colspan = cell.grid_span
|
|
398
|
+
if colspan < 1 or column_index + colspan > len(column_widths):
|
|
399
|
+
raise DocxTableError(f"DOCX cell geometry is invalid at row={row_index}")
|
|
400
|
+
width = sum(column_widths[column_index : column_index + colspan])
|
|
401
|
+
cell_width = cell.get_or_add_tcPr().get_or_add_tcW()
|
|
402
|
+
cell_width.type = "dxa"
|
|
403
|
+
cell_width.w = width
|
|
404
|
+
column_index += colspan
|
|
405
|
+
if column_index != len(column_widths):
|
|
406
|
+
raise DocxTableError(f"DOCX row width is inconsistent at row={row_index}")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _clear_fixed_row_heights(table: Table) -> None:
|
|
410
|
+
"""删除所有固定 trHeight,使 Word 根据单元格内容自然扩展行高。"""
|
|
411
|
+
for row in table._tbl.tr_lst:
|
|
412
|
+
row_properties = row.trPr
|
|
413
|
+
if row_properties is None:
|
|
414
|
+
continue
|
|
415
|
+
for height in tuple(row_properties.findall(qn("w:trHeight"))):
|
|
416
|
+
row_properties.remove(height)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _mark_header_rows(table: Table, header_rows: tuple[int, ...]) -> None:
|
|
420
|
+
"""把 thead 或全 th 行标记为可跨页重复的 Word 表头行。"""
|
|
421
|
+
for row_index in header_rows:
|
|
422
|
+
row_properties = table.rows[row_index]._tr.get_or_add_trPr()
|
|
423
|
+
header = row_properties.find(qn("w:tblHeader"))
|
|
424
|
+
if header is None:
|
|
425
|
+
header = OxmlElement("w:tblHeader")
|
|
426
|
+
row_properties.append(header)
|
|
427
|
+
header.set(qn("w:val"), "1")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
__all__ = [
|
|
431
|
+
"CellFillCallback",
|
|
432
|
+
"DEFAULT_TABLE_WIDTH_TWIPS",
|
|
433
|
+
"DocxTableError",
|
|
434
|
+
"HtmlTableCell",
|
|
435
|
+
"HtmlTableGrid",
|
|
436
|
+
"MAX_NESTED_TABLE_DEPTH",
|
|
437
|
+
"NestedTableWriter",
|
|
438
|
+
"materialize_docx_table",
|
|
439
|
+
"materialize_docx_tables",
|
|
440
|
+
"parse_html_table",
|
|
441
|
+
"parse_html_tables",
|
|
442
|
+
]
|