@_vrsen/openswarm 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/openswarm.js +38 -0
- package/config.py +34 -0
- package/data_analyst_agent/.cursor/rules/data_analyst.mdc +43 -0
- package/data_analyst_agent/__init__.py +3 -0
- package/data_analyst_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/data_analyst_agent/__pycache__/data_analyst_agent.cpython-312.pyc +0 -0
- package/data_analyst_agent/data_analyst_agent.py +45 -0
- package/data_analyst_agent/instructions.md +173 -0
- package/data_analyst_agent/test_files/test_file.csv +21 -0
- package/data_analyst_agent/tools/__init__.py +6 -0
- package/deep_research/__init__.py +1 -0
- package/deep_research/__pycache__/__init__.cpython-312.pyc +0 -0
- package/deep_research/__pycache__/deep_research.cpython-312.pyc +0 -0
- package/deep_research/deep_research.py +27 -0
- package/deep_research/instructions.md +104 -0
- package/deep_research/tools/__init__.py +1 -0
- package/docs_agent/__init__.py +3 -0
- package/docs_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/__pycache__/docs_agent.cpython-312.pyc +0 -0
- package/docs_agent/docs_agent.py +61 -0
- package/docs_agent/instructions.md +418 -0
- package/docs_agent/tools/ConvertDocument.py +323 -0
- package/docs_agent/tools/CreateDocument.py +287 -0
- package/docs_agent/tools/ListDocuments.py +134 -0
- package/docs_agent/tools/ModifyDocument.py +247 -0
- package/docs_agent/tools/RestoreDocument.py +79 -0
- package/docs_agent/tools/ViewDocument.py +153 -0
- package/docs_agent/tools/__init__.py +1 -0
- package/docs_agent/tools/__pycache__/ConvertDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/CreateDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ListDocuments.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ModifyDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/RestoreDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/ViewDocument.cpython-312.pyc +0 -0
- package/docs_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__init__.py +1 -0
- package/docs_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/doc_file_utils.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_blocks.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_constants.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_core.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_css.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_images.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_page.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_paragraphs.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_playwright.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_selectors.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_docx_shared.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/__pycache__/html_validation.cpython-312.pyc +0 -0
- package/docs_agent/tools/utils/doc_file_utils.py +29 -0
- package/docs_agent/tools/utils/html_docx_blocks.py +262 -0
- package/docs_agent/tools/utils/html_docx_constants.py +78 -0
- package/docs_agent/tools/utils/html_docx_core.py +138 -0
- package/docs_agent/tools/utils/html_docx_css.py +262 -0
- package/docs_agent/tools/utils/html_docx_images.py +293 -0
- package/docs_agent/tools/utils/html_docx_page.py +185 -0
- package/docs_agent/tools/utils/html_docx_paragraphs.py +342 -0
- package/docs_agent/tools/utils/html_docx_playwright.py +184 -0
- package/docs_agent/tools/utils/html_docx_selectors.py +196 -0
- package/docs_agent/tools/utils/html_docx_shared.py +23 -0
- package/docs_agent/tools/utils/html_docx_tables.py +942 -0
- package/docs_agent/tools/utils/html_validation.py +102 -0
- package/helpers.py +59 -0
- package/image_generation_agent/__init__.py +1 -0
- package/image_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/image_generation_agent/__pycache__/image_generation_agent.cpython-312.pyc +0 -0
- package/image_generation_agent/image_generation_agent.py +31 -0
- package/image_generation_agent/instructions.md +80 -0
- package/image_generation_agent/tools/CombineImages.py +211 -0
- package/image_generation_agent/tools/EditImages.py +200 -0
- package/image_generation_agent/tools/GenerateImages.py +184 -0
- package/image_generation_agent/tools/RemoveBackground.py +108 -0
- package/image_generation_agent/tools/__init__.py +2 -0
- package/image_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/EditImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/GenerateImages.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/__pycache__/RemoveBackground.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/__init__.py +2 -0
- package/image_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/__pycache__/image_io.cpython-312.pyc +0 -0
- package/image_generation_agent/tools/utils/image_io.py +308 -0
- package/onboard.py +325 -0
- package/orchestrator/__init__.py +3 -0
- package/orchestrator/__pycache__/__init__.cpython-312.pyc +0 -0
- package/orchestrator/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/orchestrator/instructions.md +90 -0
- package/orchestrator/orchestrator.py +33 -0
- package/package.json +49 -0
- package/patches/__init__.py +1 -0
- package/patches/__pycache__/__init__.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_agency_swarm_dual_comms.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_file_attachment_refs.cpython-312.pyc +0 -0
- package/patches/__pycache__/patch_ipython_interpreter_composio.cpython-312.pyc +0 -0
- package/patches/dom-to-pptx+1.1.5.patch +133440 -0
- package/patches/patch_agency_swarm_dual_comms.py +199 -0
- package/patches/patch_file_attachment_refs.py +74 -0
- package/patches/patch_ipython_interpreter_composio.py +54 -0
- package/pyproject.toml +67 -0
- package/run.py +343 -0
- package/server.py +26 -0
- package/shared_instructions.md +119 -0
- package/shared_tools/CopyFile.py +68 -0
- package/shared_tools/ExecuteTool.py +184 -0
- package/shared_tools/FindTools.py +101 -0
- package/shared_tools/ManageConnections.py +43 -0
- package/shared_tools/SearchTools.py +44 -0
- package/shared_tools/__init__.py +7 -0
- package/shared_tools/__pycache__/CopyFile.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/ExecuteTool.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/FindTools.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/ManageConnections.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/SearchTools.cpython-312.pyc +0 -0
- package/shared_tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/.cursor/rules/slides-agent-workflow.mdc +9 -0
- package/slides_agent/__init__.py +1 -0
- package/slides_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/__pycache__/slides_agent.cpython-312.pyc +0 -0
- package/slides_agent/instructions.md +298 -0
- package/slides_agent/pptx/SKILL.md +528 -0
- package/slides_agent/pptx/html2pptx.md +625 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/slides_agent/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/slides_agent/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/slides_agent/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/slides_agent/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/slides_agent/pptx/ooxml/scripts/pack.py +169 -0
- package/slides_agent/pptx/ooxml/scripts/unpack.py +29 -0
- package/slides_agent/pptx/ooxml/scripts/validate.py +69 -0
- package/slides_agent/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/slides_agent/pptx/ooxml/scripts/validation/base.py +951 -0
- package/slides_agent/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/slides_agent/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/slides_agent/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/slides_agent/pptx/ooxml.md +427 -0
- package/slides_agent/pptx/scripts/html2pptx.js +1092 -0
- package/slides_agent/pptx/scripts/inventory.py +1020 -0
- package/slides_agent/pptx/scripts/rearrange.py +231 -0
- package/slides_agent/pptx/scripts/replace.py +385 -0
- package/slides_agent/pptx/scripts/thumbnail.py +451 -0
- package/slides_agent/slides_agent.py +109 -0
- package/slides_agent/test_deck/_theme.css +92 -0
- package/slides_agent/test_deck/assets/placeholder.svg +11 -0
- package/slides_agent/test_deck/slide_01_title.html +10 -0
- package/slides_agent/test_deck/slide_02_image_split.html +23 -0
- package/slides_agent/test_deck/slide_03_kpi.html +21 -0
- package/slides_agent/tools/ApplyPptxTextReplacements.py +91 -0
- package/slides_agent/tools/BuildPptxFromHtmlSlides.py +221 -0
- package/slides_agent/tools/CheckSlide.py +218 -0
- package/slides_agent/tools/CheckSlideCanvasOverflow.py +221 -0
- package/slides_agent/tools/CreateImageMontage.py +261 -0
- package/slides_agent/tools/CreatePptxThumbnailGrid.py +168 -0
- package/slides_agent/tools/DeleteSlide.py +78 -0
- package/slides_agent/tools/DownloadImage.py +79 -0
- package/slides_agent/tools/EnsureRasterImage.py +157 -0
- package/slides_agent/tools/ExtractPptxTextInventory.py +104 -0
- package/slides_agent/tools/GenerateImage.py +189 -0
- package/slides_agent/tools/ImageSearch.py +127 -0
- package/slides_agent/tools/InsertNewSlides.py +393 -0
- package/slides_agent/tools/ManageTheme.py +112 -0
- package/slides_agent/tools/ModifySlide.py +563 -0
- package/slides_agent/tools/ReadSlide.py +26 -0
- package/slides_agent/tools/RearrangePptxSlidesFromTemplate.py +114 -0
- package/slides_agent/tools/RestoreSnapshot.py +89 -0
- package/slides_agent/tools/SlideScreenshot.py +66 -0
- package/slides_agent/tools/__init__.py +54 -0
- package/slides_agent/tools/__pycache__/ApplyPptxTextReplacements.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/BuildPptxFromHtmlSlides.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CheckSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CheckSlideCanvasOverflow.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CreateImageMontage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/CreatePptxThumbnailGrid.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/DeleteSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/DownloadImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/EnsureRasterImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ExtractPptxTextInventory.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ImageSearch.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/InsertNewSlides.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ManageTheme.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ModifySlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/ReadSlide.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/RearrangePptxSlidesFromTemplate.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/RestoreSnapshot.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/SlideScreenshot.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/slide_file_utils.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/slide_html_utils.cpython-312.pyc +0 -0
- package/slides_agent/tools/__pycache__/template_registry.cpython-312.pyc +0 -0
- package/slides_agent/tools/deck_utils.py +31 -0
- package/slides_agent/tools/html2pptx_runner.js +1183 -0
- package/slides_agent/tools/html_writer_instructions.md +149 -0
- package/slides_agent/tools/slide_file_utils.py +108 -0
- package/slides_agent/tools/slide_html_utils.py +354 -0
- package/slides_agent/tools/template_registry.py +55 -0
- package/swarm.py +82 -0
- package/video_generation_agent/__init__.py +1 -0
- package/video_generation_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/video_generation_agent/__pycache__/video_generation_agent.cpython-312.pyc +0 -0
- package/video_generation_agent/instructions.md +178 -0
- package/video_generation_agent/tools/AddSubtitles.py +425 -0
- package/video_generation_agent/tools/CombineImages.py +166 -0
- package/video_generation_agent/tools/CombineVideos.py +113 -0
- package/video_generation_agent/tools/EditAudio.py +297 -0
- package/video_generation_agent/tools/EditImage.py +144 -0
- package/video_generation_agent/tools/EditVideoContent.py +369 -0
- package/video_generation_agent/tools/GenerateImage.py +133 -0
- package/video_generation_agent/tools/GenerateVideo.py +556 -0
- package/video_generation_agent/tools/TrimVideo.py +233 -0
- package/video_generation_agent/tools/__init__.py +1 -0
- package/video_generation_agent/tools/__pycache__/AddSubtitles.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/CombineImages.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/CombineVideos.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditAudio.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditImage.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/EditVideoContent.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/GenerateImage.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/GenerateVideo.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/__pycache__/TrimVideo.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__init__.py +1 -0
- package/video_generation_agent/tools/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__pycache__/image_utils.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/__pycache__/video_utils.cpython-312.pyc +0 -0
- package/video_generation_agent/tools/utils/image_utils.py +174 -0
- package/video_generation_agent/tools/utils/video_utils.py +522 -0
- package/video_generation_agent/video_generation_agent.py +26 -0
- package/virtual_assistant/__init__.py +1 -0
- package/virtual_assistant/__pycache__/__init__.cpython-312.pyc +0 -0
- package/virtual_assistant/__pycache__/virtual_assistant.cpython-312.pyc +0 -0
- package/virtual_assistant/instructions.md +206 -0
- package/virtual_assistant/tools/AddLabelToEmail.py +154 -0
- package/virtual_assistant/tools/CheckEventsForDate.py +218 -0
- package/virtual_assistant/tools/CheckUnreadSlackMessages.py +216 -0
- package/virtual_assistant/tools/CreateCalendarEvent.py +261 -0
- package/virtual_assistant/tools/DeleteCalendarEvent.py +137 -0
- package/virtual_assistant/tools/DeleteDraft.py +95 -0
- package/virtual_assistant/tools/DraftEmail.py +239 -0
- package/virtual_assistant/tools/EditFile.py +113 -0
- package/virtual_assistant/tools/FindEmails.py +330 -0
- package/virtual_assistant/tools/GetCurrentTime.py +69 -0
- package/virtual_assistant/tools/GetSlackUserInfo.py +117 -0
- package/virtual_assistant/tools/ListDirectory.py +113 -0
- package/virtual_assistant/tools/ListSkills.py +94 -0
- package/virtual_assistant/tools/ManageLabels.py +295 -0
- package/virtual_assistant/tools/ProductSearch.py +254 -0
- package/virtual_assistant/tools/ReadEmail.py +251 -0
- package/virtual_assistant/tools/ReadFile.py +108 -0
- package/virtual_assistant/tools/ReadSlackMessages.py +191 -0
- package/virtual_assistant/tools/RemoveLabelFromEmail.py +137 -0
- package/virtual_assistant/tools/RescheduleCalendarEvent.py +227 -0
- package/virtual_assistant/tools/ScholarSearch.py +216 -0
- package/virtual_assistant/tools/SendDraft.py +101 -0
- package/virtual_assistant/tools/SendSlackMessage.py +148 -0
- package/virtual_assistant/tools/WriteFile.py +95 -0
- package/virtual_assistant/tools/__init__.py +1 -0
- package/virtual_assistant/tools/__pycache__/AddLabelToEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CheckEventsForDate.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CheckUnreadSlackMessages.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/CreateCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DeleteCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DeleteDraft.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/DraftEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/EditFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/FindEmails.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/GetCurrentTime.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/GetSlackUserInfo.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ListDirectory.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ListSkills.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ManageLabels.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ProductSearch.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ReadSlackMessages.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/RemoveLabelFromEmail.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/RescheduleCalendarEvent.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/ScholarSearch.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/SendDraft.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/SendSlackMessage.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/WriteFile.cpython-312.pyc +0 -0
- package/virtual_assistant/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/virtual_assistant/virtual_assistant.py +52 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from bs4.element import Tag
|
|
4
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
|
5
|
+
from docx.oxml import OxmlElement
|
|
6
|
+
from docx.oxml.ns import qn
|
|
7
|
+
|
|
8
|
+
from .html_docx_constants import _INHERITABLE_STYLES, _NAMED_COLORS, _PADDING_SCALE
|
|
9
|
+
from .html_docx_css import (
|
|
10
|
+
_border_sz,
|
|
11
|
+
_normalize_padding,
|
|
12
|
+
_parse_background_color,
|
|
13
|
+
_parse_border,
|
|
14
|
+
_parse_border_left,
|
|
15
|
+
_parse_percentage,
|
|
16
|
+
_parse_px_to_pt,
|
|
17
|
+
_resolve_padding,
|
|
18
|
+
)
|
|
19
|
+
from .html_docx_paragraphs import (
|
|
20
|
+
_add_list_indent_padding,
|
|
21
|
+
_add_paragraph_indent,
|
|
22
|
+
_add_paragraph_spacing,
|
|
23
|
+
)
|
|
24
|
+
from .html_docx_playwright import _extract_auto_widths
|
|
25
|
+
from .html_docx_selectors import _compute_style_map
|
|
26
|
+
from .html_docx_shared import _remove_trailing_empty_paragraph
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _handle_table(
|
|
30
|
+
table_node: Tag, target_container, css_rules, parent_style: Dict[str, str], table_auto_widths
|
|
31
|
+
) -> None:
|
|
32
|
+
table_style = _merge_styles(parent_style, _compute_style_map(table_node, css_rules))
|
|
33
|
+
parent_padding = _resolve_padding(parent_style)
|
|
34
|
+
parent_padding_right = None
|
|
35
|
+
if parent_padding:
|
|
36
|
+
_top, right, _bottom, _left = _normalize_padding(parent_padding)
|
|
37
|
+
parent_padding_right = right if right else None
|
|
38
|
+
rows = table_node.find_all("tr", recursive=False)
|
|
39
|
+
if not rows:
|
|
40
|
+
for section in table_node.find_all(["thead", "tbody", "tfoot"], recursive=False):
|
|
41
|
+
rows.extend(section.find_all("tr", recursive=False))
|
|
42
|
+
if not rows:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
max_cols = 0
|
|
46
|
+
row_cells: list[list[Tag]] = []
|
|
47
|
+
row_colspans: list[list[int]] = []
|
|
48
|
+
for row in rows:
|
|
49
|
+
cells = [cell for cell in row.find_all(["td", "th"], recursive=False)]
|
|
50
|
+
if not cells:
|
|
51
|
+
continue
|
|
52
|
+
colspans = []
|
|
53
|
+
col_count = 0
|
|
54
|
+
for cell in cells:
|
|
55
|
+
colspan = cell.get("colspan")
|
|
56
|
+
try:
|
|
57
|
+
span_value = int(colspan) if colspan else 1
|
|
58
|
+
except ValueError:
|
|
59
|
+
span_value = 1
|
|
60
|
+
span_value = max(span_value, 1)
|
|
61
|
+
colspans.append(span_value)
|
|
62
|
+
col_count += span_value
|
|
63
|
+
max_cols = max(max_cols, col_count)
|
|
64
|
+
row_cells.append(cells)
|
|
65
|
+
row_colspans.append(colspans)
|
|
66
|
+
|
|
67
|
+
if max_cols == 0:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
_remove_trailing_empty_paragraph(target_container)
|
|
71
|
+
docx_table = target_container.add_table(rows=len(rows), cols=max_cols)
|
|
72
|
+
_apply_table_styles(docx_table, table_style)
|
|
73
|
+
_apply_table_parent_padding(docx_table, parent_style)
|
|
74
|
+
if _should_prevent_row_split(table_node, table_style):
|
|
75
|
+
_set_table_cant_split(docx_table)
|
|
76
|
+
column_widths_pt = _extract_table_column_widths(table_node, table_style, max_cols, table_auto_widths)
|
|
77
|
+
if not column_widths_pt:
|
|
78
|
+
column_widths_pt = _extract_auto_widths(table_node, table_auto_widths, max_cols)
|
|
79
|
+
if column_widths_pt:
|
|
80
|
+
_apply_table_column_widths(docx_table, column_widths_pt)
|
|
81
|
+
|
|
82
|
+
is_collapsed = table_style.get("border-collapse", "").strip().lower() == "collapse"
|
|
83
|
+
collapsed_borders: dict = {}
|
|
84
|
+
if is_collapsed:
|
|
85
|
+
collapsed_borders = _collect_collapsed_borders(
|
|
86
|
+
row_cells, row_colspans, max_cols, css_rules
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
total_rows = len(row_cells)
|
|
90
|
+
for row_idx, cells in enumerate(row_cells):
|
|
91
|
+
col_idx = 0
|
|
92
|
+
for cell_idx, cell_node in enumerate(cells):
|
|
93
|
+
if col_idx >= max_cols:
|
|
94
|
+
break
|
|
95
|
+
docx_cell = docx_table.cell(row_idx, col_idx)
|
|
96
|
+
colspan = (
|
|
97
|
+
row_colspans[row_idx][cell_idx] if cell_idx < len(row_colspans[row_idx]) else 1
|
|
98
|
+
)
|
|
99
|
+
if colspan > 1 and col_idx + colspan - 1 < max_cols:
|
|
100
|
+
docx_cell = docx_cell.merge(docx_table.cell(row_idx, col_idx + colspan - 1))
|
|
101
|
+
if column_widths_pt and colspan == max_cols and len(cells) == 1:
|
|
102
|
+
span_width = sum(column_widths_pt)
|
|
103
|
+
if span_width > 0:
|
|
104
|
+
_set_cell_width(docx_cell, span_width)
|
|
105
|
+
|
|
106
|
+
cell_own_style = _compute_style_map(cell_node, css_rules)
|
|
107
|
+
cell_style = _merge_styles(table_style, cell_own_style)
|
|
108
|
+
# CSS `background` is not inherited, but it shows through transparent cells
|
|
109
|
+
# visually. In DOCX every cell is opaque by default (white), so we must
|
|
110
|
+
# explicitly forward the background to any cell that doesn't declare its own.
|
|
111
|
+
# We also fall back to parent_style so that cells inside a nested table that
|
|
112
|
+
# sits inside a coloured cell also receive the correct background.
|
|
113
|
+
effective_bg = _parse_background_color(table_style) or _parse_background_color(
|
|
114
|
+
parent_style
|
|
115
|
+
)
|
|
116
|
+
if effective_bg and not _parse_background_color(cell_own_style):
|
|
117
|
+
# Store with '#' prefix so _parse_background_color can re-read it
|
|
118
|
+
# when this cell_style is passed as parent_style to nested elements.
|
|
119
|
+
cell_style = {**cell_style, "background-color": f"#{effective_bg}"}
|
|
120
|
+
if cell_node.name == "th":
|
|
121
|
+
cell_style = {**cell_style, "font-weight": "bold"}
|
|
122
|
+
if (
|
|
123
|
+
parent_padding_right is not None
|
|
124
|
+
and cell_style.get("text-align", "").strip().lower() == "right"
|
|
125
|
+
and not cell_style.get("padding-right")
|
|
126
|
+
and not cell_style.get("padding")
|
|
127
|
+
):
|
|
128
|
+
cell_style = {**cell_style, "padding-right": f"{parent_padding_right}pt"}
|
|
129
|
+
|
|
130
|
+
# For border-collapse:collapse tables, interior borders are handled by
|
|
131
|
+
# tblBorders/insideH+insideV (applied once after the loop). Suppress cell-level
|
|
132
|
+
# borders on interior edges so they don't double up with the table-level lines.
|
|
133
|
+
# Outer-edge cells keep their cell-level borders (one side each).
|
|
134
|
+
suppress_borders: Optional[set] = None
|
|
135
|
+
if is_collapsed:
|
|
136
|
+
is_first_row = row_idx == 0
|
|
137
|
+
is_last_row = row_idx == total_rows - 1
|
|
138
|
+
is_first_col = col_idx == 0
|
|
139
|
+
is_last_col = col_idx + max(colspan, 1) - 1 >= max_cols - 1
|
|
140
|
+
suppress_borders = set()
|
|
141
|
+
if not is_first_row:
|
|
142
|
+
suppress_borders.add("top")
|
|
143
|
+
if not is_last_row:
|
|
144
|
+
suppress_borders.add("bottom")
|
|
145
|
+
if not is_first_col:
|
|
146
|
+
suppress_borders.add("left")
|
|
147
|
+
if not is_last_col:
|
|
148
|
+
suppress_borders.add("right")
|
|
149
|
+
|
|
150
|
+
_apply_cell_styles(docx_cell, cell_style, suppress_borders=suppress_borders)
|
|
151
|
+
if (
|
|
152
|
+
len(cells) == 2
|
|
153
|
+
and cell_style.get("text-align", "").strip().lower() == "right"
|
|
154
|
+
):
|
|
155
|
+
_set_cell_no_wrap(docx_cell)
|
|
156
|
+
_set_cell_width(docx_cell, _estimate_right_column_width_pt(column_widths_pt))
|
|
157
|
+
if column_widths_pt and col_idx < len(column_widths_pt):
|
|
158
|
+
col_width_pt = column_widths_pt[col_idx]
|
|
159
|
+
_set_cell_width(docx_cell, col_width_pt)
|
|
160
|
+
# Inject absolute cell width so nested image sizing can cap to it.
|
|
161
|
+
cell_style = {**cell_style, "_cell_width_pt": str(col_width_pt)}
|
|
162
|
+
|
|
163
|
+
from .html_docx_blocks import (
|
|
164
|
+
_add_inline_runs,
|
|
165
|
+
_handle_block,
|
|
166
|
+
_has_block_children,
|
|
167
|
+
)
|
|
168
|
+
from .html_docx_paragraphs import _apply_paragraph_style
|
|
169
|
+
from .html_docx_blocks import _ensure_paragraph
|
|
170
|
+
|
|
171
|
+
# Cell borders are applied at the cell level (w:tcBorders). Strip
|
|
172
|
+
# border-* from the style passed to paragraph content so it doesn't
|
|
173
|
+
# also produce paragraph-level borders (w:pBdr), which would render
|
|
174
|
+
# as a second visible line on top of the cell border.
|
|
175
|
+
content_style = {k: v for k, v in cell_style.items() if not k.startswith("border")}
|
|
176
|
+
|
|
177
|
+
if _has_block_children(cell_node):
|
|
178
|
+
for child in cell_node.children:
|
|
179
|
+
_handle_block(child, docx_cell, css_rules, content_style, table_auto_widths)
|
|
180
|
+
else:
|
|
181
|
+
# Cell contains only inline content (text, spans, <br>) —
|
|
182
|
+
# render as one paragraph so <br> becomes a soft line break
|
|
183
|
+
# rather than each text node becoming a separate paragraph.
|
|
184
|
+
paragraph = _ensure_paragraph(docx_cell)
|
|
185
|
+
_apply_paragraph_style(paragraph, content_style)
|
|
186
|
+
_add_inline_runs(cell_node, paragraph, css_rules, content_style)
|
|
187
|
+
|
|
188
|
+
col_idx += max(colspan, 1)
|
|
189
|
+
|
|
190
|
+
if is_collapsed:
|
|
191
|
+
inside_h = collapsed_borders.get("inside_h")
|
|
192
|
+
inside_v = collapsed_borders.get("inside_v")
|
|
193
|
+
if inside_h or inside_v:
|
|
194
|
+
_apply_collapsed_table_borders(
|
|
195
|
+
docx_table,
|
|
196
|
+
{"inside_h": inside_h, "inside_v": inside_v},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _extract_table_column_widths(
|
|
201
|
+
table_node: Tag,
|
|
202
|
+
table_style: Dict[str, str],
|
|
203
|
+
column_count: int,
|
|
204
|
+
table_auto_widths=None,
|
|
205
|
+
) -> Optional[List[float]]:
|
|
206
|
+
if column_count <= 0:
|
|
207
|
+
return None
|
|
208
|
+
rows = table_node.find_all("tr", recursive=False)
|
|
209
|
+
if not rows:
|
|
210
|
+
for section in table_node.find_all(["thead", "tbody", "tfoot"], recursive=False):
|
|
211
|
+
rows.extend(section.find_all("tr", recursive=False))
|
|
212
|
+
if not rows:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
widths: List[Optional[float]] | None = None
|
|
216
|
+
for row in rows:
|
|
217
|
+
cells = [cell for cell in row.find_all(["td", "th"], recursive=False)]
|
|
218
|
+
if not cells:
|
|
219
|
+
continue
|
|
220
|
+
candidate_widths: List[Optional[float]] = []
|
|
221
|
+
for cell in cells:
|
|
222
|
+
style_map = _compute_style_map(cell, [])
|
|
223
|
+
width_value = style_map.get("width", "") or cell.get("width", "")
|
|
224
|
+
if isinstance(width_value, str) and width_value.endswith("%"):
|
|
225
|
+
candidate_widths.append(_parse_percentage(width_value))
|
|
226
|
+
else:
|
|
227
|
+
candidate_widths.append(_parse_px_to_pt(str(width_value)) if width_value else None)
|
|
228
|
+
if any(candidate_widths) and len(candidate_widths) >= column_count:
|
|
229
|
+
widths = candidate_widths
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if not widths or not any(widths):
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
total_width_pt = _parse_px_to_pt(table_style.get("width", ""))
|
|
236
|
+
if total_width_pt is None:
|
|
237
|
+
# width:100% or a CSS percentage — look up the Playwright-rendered width.
|
|
238
|
+
# Without the actual rendered width, percentage column values can't be
|
|
239
|
+
# resolved correctly (the fallback of 547pt is wrong for nested tables).
|
|
240
|
+
auto_id = table_node.get("data-table-id")
|
|
241
|
+
if auto_id and table_auto_widths and auto_id in table_auto_widths:
|
|
242
|
+
auto = table_auto_widths[auto_id]
|
|
243
|
+
if auto:
|
|
244
|
+
total_width_pt = sum(auto)
|
|
245
|
+
if total_width_pt is None:
|
|
246
|
+
# No context available — let _extract_auto_widths handle this table.
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
resolved: List[float | None] = []
|
|
250
|
+
fixed_total = sum(w for w in widths if isinstance(w, float) and w > 1.0)
|
|
251
|
+
remaining = max(total_width_pt - fixed_total, 0)
|
|
252
|
+
|
|
253
|
+
for width in widths:
|
|
254
|
+
if width is None:
|
|
255
|
+
resolved.append(None)
|
|
256
|
+
elif width <= 1.0:
|
|
257
|
+
# Treat as an absolute fraction of total_width_pt, not normalised
|
|
258
|
+
# against other percentage columns. This correctly handles mixed
|
|
259
|
+
# rows where some columns specify a % and others have no width.
|
|
260
|
+
resolved.append(total_width_pt * width)
|
|
261
|
+
else:
|
|
262
|
+
resolved.append(width)
|
|
263
|
+
|
|
264
|
+
none_count = sum(1 for value in resolved if value is None)
|
|
265
|
+
if none_count:
|
|
266
|
+
filled_total = sum(value for value in resolved if isinstance(value, float))
|
|
267
|
+
filler = max(total_width_pt - filled_total, 0) / none_count
|
|
268
|
+
resolved = [value if value is not None else filler for value in resolved]
|
|
269
|
+
|
|
270
|
+
if len(resolved) < column_count:
|
|
271
|
+
missing = column_count - len(resolved)
|
|
272
|
+
filler = (total_width_pt - sum(resolved)) / max(missing, 1)
|
|
273
|
+
resolved.extend([filler] * missing)
|
|
274
|
+
return [float(value) for value in resolved[:column_count]]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _apply_table_column_widths(table, widths_pt: List[float]) -> None:
|
|
278
|
+
widths_pt = _adjust_column_widths_for_parent_padding(table, widths_pt)
|
|
279
|
+
widths_pt = _adjust_column_widths_for_outer_borders(table, widths_pt)
|
|
280
|
+
_update_table_grid(table, widths_pt)
|
|
281
|
+
for row in table.rows:
|
|
282
|
+
for idx, cell in enumerate(row.cells):
|
|
283
|
+
if idx < len(widths_pt) and widths_pt[idx] > 0:
|
|
284
|
+
_set_cell_width(cell, widths_pt[idx])
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _update_table_grid(table, widths_pt: List[float]) -> None:
|
|
288
|
+
"""Replace w:tblGrid with column definitions matching widths_pt.
|
|
289
|
+
|
|
290
|
+
python-docx creates w:tblGrid with equal-width columns when add_table() is
|
|
291
|
+
called. Without updating it, Word ignores the per-cell w:tcW values and
|
|
292
|
+
renders equal columns. This must be called before setting cell widths.
|
|
293
|
+
"""
|
|
294
|
+
tbl = table._tbl
|
|
295
|
+
existing_grid = tbl.find(qn("w:tblGrid"))
|
|
296
|
+
if existing_grid is not None:
|
|
297
|
+
tbl.remove(existing_grid)
|
|
298
|
+
grid = OxmlElement("w:tblGrid")
|
|
299
|
+
for width in widths_pt:
|
|
300
|
+
col = OxmlElement("w:gridCol")
|
|
301
|
+
col.set(qn("w:w"), str(int(width * 20)))
|
|
302
|
+
grid.append(col)
|
|
303
|
+
tbl_pr = tbl.find(qn("w:tblPr"))
|
|
304
|
+
if tbl_pr is not None:
|
|
305
|
+
tbl_pr.addnext(grid)
|
|
306
|
+
else:
|
|
307
|
+
tbl.insert(0, grid)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _set_cell_width(cell, width_pt: float) -> None:
|
|
311
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
312
|
+
tc_w = tc_pr.find(qn("w:tcW"))
|
|
313
|
+
if tc_w is None:
|
|
314
|
+
tc_w = OxmlElement("w:tcW")
|
|
315
|
+
tc_pr.append(tc_w)
|
|
316
|
+
tc_w.set(qn("w:type"), "dxa")
|
|
317
|
+
tc_w.set(qn("w:w"), str(int(width_pt * 20)))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _set_cell_no_wrap(cell) -> None:
|
|
321
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
322
|
+
no_wrap = tc_pr.find(qn("w:noWrap"))
|
|
323
|
+
if no_wrap is None:
|
|
324
|
+
no_wrap = OxmlElement("w:noWrap")
|
|
325
|
+
tc_pr.append(no_wrap)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _estimate_right_column_width_pt(column_widths_pt: Optional[List[float]]) -> float:
|
|
329
|
+
if column_widths_pt and len(column_widths_pt) >= 2:
|
|
330
|
+
return max(column_widths_pt[1], 160.0)
|
|
331
|
+
return 180.0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _merge_styles(parent_style: Dict[str, str], own_style: Dict[str, str]) -> Dict[str, str]:
|
|
335
|
+
merged: Dict[str, str] = {}
|
|
336
|
+
for key in _INHERITABLE_STYLES:
|
|
337
|
+
if key in parent_style:
|
|
338
|
+
merged[key] = parent_style[key]
|
|
339
|
+
for key, value in own_style.items():
|
|
340
|
+
merged[key] = value
|
|
341
|
+
return merged
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _apply_table_styles(table, style_map: Dict[str, str]) -> None:
|
|
345
|
+
width_value = style_map.get("width", "") or style_map.get("max-width", "")
|
|
346
|
+
width_pt = _parse_px_to_pt(width_value)
|
|
347
|
+
if width_pt:
|
|
348
|
+
_set_table_width(table, width_pt)
|
|
349
|
+
|
|
350
|
+
if _should_center_table(style_map):
|
|
351
|
+
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
|
352
|
+
|
|
353
|
+
_set_table_normal_style(table)
|
|
354
|
+
_clear_table_look(table)
|
|
355
|
+
_set_table_default_cell_margins(table, 0, 0, 0, 0)
|
|
356
|
+
_set_table_cell_spacing(table, 0)
|
|
357
|
+
_apply_table_border(table, style_map)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _set_table_width(table, width_pt: float) -> None:
|
|
361
|
+
tbl_pr = table._tbl.tblPr
|
|
362
|
+
if tbl_pr is None:
|
|
363
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
364
|
+
table._tbl.append(tbl_pr)
|
|
365
|
+
tbl_w = tbl_pr.find(qn("w:tblW"))
|
|
366
|
+
if tbl_w is None:
|
|
367
|
+
tbl_w = OxmlElement("w:tblW")
|
|
368
|
+
tbl_pr.append(tbl_w)
|
|
369
|
+
tbl_w.set(qn("w:type"), "dxa")
|
|
370
|
+
tbl_w.set(qn("w:w"), str(int(width_pt * 20)))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _apply_table_border(table, style_map: Dict[str, str]) -> None:
|
|
374
|
+
border = style_map.get("border", "")
|
|
375
|
+
if not border:
|
|
376
|
+
return
|
|
377
|
+
parts = border.split()
|
|
378
|
+
width_pt = _parse_px_to_pt(parts[0]) if parts else None
|
|
379
|
+
color = None
|
|
380
|
+
for part in parts:
|
|
381
|
+
if part.startswith("#"):
|
|
382
|
+
color = part[1:].upper()
|
|
383
|
+
break
|
|
384
|
+
if part.lower() in _NAMED_COLORS:
|
|
385
|
+
r, g, b = _NAMED_COLORS[part.lower()]
|
|
386
|
+
color = f"{r:02X}{g:02X}{b:02X}"
|
|
387
|
+
break
|
|
388
|
+
if not width_pt or not color:
|
|
389
|
+
return
|
|
390
|
+
_apply_table_outer_borders(table, width_pt, color)
|
|
391
|
+
table._docs_border_width_pt = width_pt
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _set_table_normal_style(table) -> None:
|
|
395
|
+
tbl_pr = table._tbl.tblPr
|
|
396
|
+
if tbl_pr is None:
|
|
397
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
398
|
+
table._tbl.append(tbl_pr)
|
|
399
|
+
tbl_style = tbl_pr.find(qn("w:tblStyle"))
|
|
400
|
+
if tbl_style is None:
|
|
401
|
+
tbl_style = OxmlElement("w:tblStyle")
|
|
402
|
+
tbl_pr.insert(0, tbl_style)
|
|
403
|
+
tbl_style.set(qn("w:val"), "TableNormal")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _clear_table_look(table) -> None:
|
|
407
|
+
tbl_pr = table._tbl.tblPr
|
|
408
|
+
if tbl_pr is None:
|
|
409
|
+
return
|
|
410
|
+
tbl_look = tbl_pr.find(qn("w:tblLook"))
|
|
411
|
+
if tbl_look is not None:
|
|
412
|
+
tbl_pr.remove(tbl_look)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _set_table_default_cell_margins(
|
|
416
|
+
table, top: float, right: float, bottom: float, left: float
|
|
417
|
+
) -> None:
|
|
418
|
+
tbl_pr = table._tbl.tblPr
|
|
419
|
+
if tbl_pr is None:
|
|
420
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
421
|
+
table._tbl.append(tbl_pr)
|
|
422
|
+
tbl_cell_mar = tbl_pr.find(qn("w:tblCellMar"))
|
|
423
|
+
if tbl_cell_mar is None:
|
|
424
|
+
tbl_cell_mar = OxmlElement("w:tblCellMar")
|
|
425
|
+
tbl_pr.append(tbl_cell_mar)
|
|
426
|
+
for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
|
|
427
|
+
elem = tbl_cell_mar.find(qn(f"w:{side}"))
|
|
428
|
+
if elem is None:
|
|
429
|
+
elem = OxmlElement(f"w:{side}")
|
|
430
|
+
tbl_cell_mar.append(elem)
|
|
431
|
+
elem.set(qn("w:w"), str(int(value * 20)))
|
|
432
|
+
elem.set(qn("w:type"), "dxa")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _set_table_cell_spacing(table, spacing_pt: float) -> None:
|
|
436
|
+
tbl_pr = table._tbl.tblPr
|
|
437
|
+
if tbl_pr is None:
|
|
438
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
439
|
+
table._tbl.append(tbl_pr)
|
|
440
|
+
tbl_cell_spacing = tbl_pr.find(qn("w:tblCellSpacing"))
|
|
441
|
+
if spacing_pt <= 0:
|
|
442
|
+
if tbl_cell_spacing is not None:
|
|
443
|
+
tbl_pr.remove(tbl_cell_spacing)
|
|
444
|
+
return
|
|
445
|
+
if tbl_cell_spacing is None:
|
|
446
|
+
tbl_cell_spacing = OxmlElement("w:tblCellSpacing")
|
|
447
|
+
tbl_pr.append(tbl_cell_spacing)
|
|
448
|
+
tbl_cell_spacing.set(qn("w:w"), str(int(spacing_pt * 20)))
|
|
449
|
+
tbl_cell_spacing.set(qn("w:type"), "dxa")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _should_center_table(style_map: Dict[str, str]) -> bool:
|
|
453
|
+
margin = style_map.get("margin", "")
|
|
454
|
+
if margin and "auto" in margin:
|
|
455
|
+
return True
|
|
456
|
+
left = style_map.get("margin-left", "").strip().lower()
|
|
457
|
+
right = style_map.get("margin-right", "").strip().lower()
|
|
458
|
+
if left == "auto" and right == "auto":
|
|
459
|
+
return True
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _apply_cell_styles(
|
|
464
|
+
cell,
|
|
465
|
+
style_map: Dict[str, str],
|
|
466
|
+
apply_padding: bool = True,
|
|
467
|
+
suppress_borders: Optional[set] = None,
|
|
468
|
+
) -> None:
|
|
469
|
+
bg_color = _parse_background_color(style_map)
|
|
470
|
+
if bg_color:
|
|
471
|
+
_set_cell_shading(cell, bg_color)
|
|
472
|
+
|
|
473
|
+
_apply_cell_vertical_alignment(cell, style_map)
|
|
474
|
+
|
|
475
|
+
suppress = suppress_borders or set()
|
|
476
|
+
border = _parse_border(style_map.get("border", ""))
|
|
477
|
+
top_border = _parse_border(style_map.get("border-top", ""))
|
|
478
|
+
right_border = _parse_border(style_map.get("border-right", ""))
|
|
479
|
+
bottom_border = _parse_border(style_map.get("border-bottom", ""))
|
|
480
|
+
left_border = _parse_border(style_map.get("border-left", ""))
|
|
481
|
+
|
|
482
|
+
effective_top = (top_border or border) if "top" not in suppress else None
|
|
483
|
+
effective_right = (right_border or border) if "right" not in suppress else None
|
|
484
|
+
effective_bottom = (bottom_border or border) if "bottom" not in suppress else None
|
|
485
|
+
effective_left = (left_border or border) if "left" not in suppress else None
|
|
486
|
+
|
|
487
|
+
if any([effective_top, effective_right, effective_bottom, effective_left]):
|
|
488
|
+
_set_cell_border(
|
|
489
|
+
cell,
|
|
490
|
+
top=effective_top,
|
|
491
|
+
right=effective_right,
|
|
492
|
+
bottom=effective_bottom,
|
|
493
|
+
left=effective_left,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if "left" not in suppress:
|
|
497
|
+
border_left = _parse_border_left(style_map)
|
|
498
|
+
if border_left:
|
|
499
|
+
width_pt, color = border_left
|
|
500
|
+
_set_cell_border(cell, left=(width_pt, color))
|
|
501
|
+
|
|
502
|
+
padding = _resolve_padding(style_map)
|
|
503
|
+
if padding and apply_padding:
|
|
504
|
+
top, right, bottom, left = _normalize_padding(padding)
|
|
505
|
+
_set_cell_margins(
|
|
506
|
+
cell,
|
|
507
|
+
top * _PADDING_SCALE,
|
|
508
|
+
right * _PADDING_SCALE,
|
|
509
|
+
bottom * _PADDING_SCALE,
|
|
510
|
+
left * _PADDING_SCALE,
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
_set_cell_margins(cell, 0, 0, 0, 0)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _apply_cell_vertical_alignment(cell, style_map: Dict[str, str]) -> None:
|
|
517
|
+
vertical_align = style_map.get("vertical-align", "").strip().lower()
|
|
518
|
+
if not vertical_align:
|
|
519
|
+
return
|
|
520
|
+
if vertical_align in {"middle", "center"}:
|
|
521
|
+
val = "center"
|
|
522
|
+
elif vertical_align in {"top", "bottom"}:
|
|
523
|
+
val = vertical_align
|
|
524
|
+
else:
|
|
525
|
+
return
|
|
526
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
527
|
+
v_align = tc_pr.find(qn("w:vAlign"))
|
|
528
|
+
if v_align is None:
|
|
529
|
+
v_align = OxmlElement("w:vAlign")
|
|
530
|
+
tc_pr.append(v_align)
|
|
531
|
+
v_align.set(qn("w:val"), val)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _apply_cell_padding_spacing(cell, padding: Tuple[float, float, float, float]) -> None:
|
|
535
|
+
paragraphs = [p for p in cell.paragraphs if _is_direct_cell_paragraph(cell, p)]
|
|
536
|
+
if not paragraphs:
|
|
537
|
+
return
|
|
538
|
+
top, right, bottom, left = _normalize_padding(padding)
|
|
539
|
+
if top:
|
|
540
|
+
_add_paragraph_spacing(paragraphs[0], before_pt=top * _PADDING_SCALE)
|
|
541
|
+
if bottom:
|
|
542
|
+
_add_paragraph_spacing(paragraphs[-1], after_pt=bottom * _PADDING_SCALE)
|
|
543
|
+
if left or right:
|
|
544
|
+
for paragraph in paragraphs:
|
|
545
|
+
if paragraph.style and paragraph.style.name == "List Bullet":
|
|
546
|
+
# "List Bullet" indentation is controlled by the numbering definition;
|
|
547
|
+
# adding paragraph-level w:ind here overrides it and misaligns bullets.
|
|
548
|
+
pass
|
|
549
|
+
else:
|
|
550
|
+
_add_paragraph_indent(
|
|
551
|
+
paragraph,
|
|
552
|
+
left_pt=left * _PADDING_SCALE,
|
|
553
|
+
right_pt=right * _PADDING_SCALE,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _cell_has_only_tables(cell) -> bool:
|
|
558
|
+
try:
|
|
559
|
+
if not cell.tables:
|
|
560
|
+
return False
|
|
561
|
+
for paragraph in cell.paragraphs:
|
|
562
|
+
if paragraph.text.strip():
|
|
563
|
+
return False
|
|
564
|
+
return True
|
|
565
|
+
except Exception:
|
|
566
|
+
return "<w:tbl>" in cell._tc.xml
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _is_direct_cell_paragraph(cell, paragraph) -> bool:
|
|
570
|
+
try:
|
|
571
|
+
return paragraph._p.getparent() is cell._tc
|
|
572
|
+
except Exception:
|
|
573
|
+
return True
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _apply_nested_table_vertical_padding(
|
|
577
|
+
cell, padding: Tuple[float, float, float, float]
|
|
578
|
+
) -> None:
|
|
579
|
+
if not cell.tables:
|
|
580
|
+
return
|
|
581
|
+
top, _right, bottom, _left = _normalize_padding(padding)
|
|
582
|
+
top_pt = top * _PADDING_SCALE if top else 0
|
|
583
|
+
bottom_pt = bottom * _PADDING_SCALE if bottom else 0
|
|
584
|
+
tables = cell.tables
|
|
585
|
+
if top_pt and tables:
|
|
586
|
+
first_table = tables[0]
|
|
587
|
+
if first_table.rows:
|
|
588
|
+
for tcell in first_table.rows[0].cells:
|
|
589
|
+
_add_cell_margins(tcell, top=top_pt)
|
|
590
|
+
if bottom_pt and tables:
|
|
591
|
+
last_table = tables[-1]
|
|
592
|
+
if last_table.rows:
|
|
593
|
+
last_row = last_table.rows[-1]
|
|
594
|
+
for tcell in last_row.cells:
|
|
595
|
+
_add_cell_margins(tcell, bottom=bottom_pt)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _set_cell_shading(cell, color_hex: str) -> None:
|
|
599
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
600
|
+
shd = OxmlElement("w:shd")
|
|
601
|
+
shd.set(qn("w:val"), "clear")
|
|
602
|
+
shd.set(qn("w:color"), "auto")
|
|
603
|
+
shd.set(qn("w:fill"), color_hex)
|
|
604
|
+
tc_pr.append(shd)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _set_cell_border(
|
|
608
|
+
cell,
|
|
609
|
+
top: Optional[Tuple[float, str]] = None,
|
|
610
|
+
right: Optional[Tuple[float, str]] = None,
|
|
611
|
+
bottom: Optional[Tuple[float, str]] = None,
|
|
612
|
+
left: Optional[Tuple[float, str]] = None,
|
|
613
|
+
) -> None:
|
|
614
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
615
|
+
borders = tc_pr.find(qn("w:tcBorders"))
|
|
616
|
+
if borders is None:
|
|
617
|
+
borders = OxmlElement("w:tcBorders")
|
|
618
|
+
tc_pr.append(borders)
|
|
619
|
+
|
|
620
|
+
for side_name, val in [
|
|
621
|
+
("w:top", top),
|
|
622
|
+
("w:right", right),
|
|
623
|
+
("w:bottom", bottom),
|
|
624
|
+
("w:left", left),
|
|
625
|
+
]:
|
|
626
|
+
if not val:
|
|
627
|
+
continue
|
|
628
|
+
width_pt, color = val
|
|
629
|
+
# Remove any existing element for this side to prevent duplicates when
|
|
630
|
+
# _set_cell_border is called more than once on the same cell.
|
|
631
|
+
for existing in borders.findall(qn(side_name)):
|
|
632
|
+
borders.remove(existing)
|
|
633
|
+
el = OxmlElement(side_name)
|
|
634
|
+
el.set(qn("w:val"), "single")
|
|
635
|
+
el.set(qn("w:sz"), _border_sz(width_pt))
|
|
636
|
+
el.set(qn("w:color"), color)
|
|
637
|
+
borders.append(el)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _apply_table_outer_borders(table, width_pt: float, color: str) -> None:
|
|
641
|
+
if not table.rows:
|
|
642
|
+
return
|
|
643
|
+
last_row_idx = len(table.rows) - 1
|
|
644
|
+
last_col_idx = len(table.rows[0].cells) - 1
|
|
645
|
+
for row_idx, row in enumerate(table.rows):
|
|
646
|
+
for col_idx, cell in enumerate(row.cells):
|
|
647
|
+
top = (width_pt, color) if row_idx == 0 else None
|
|
648
|
+
bottom = (width_pt, color) if row_idx == last_row_idx else None
|
|
649
|
+
left = (width_pt, color) if col_idx == 0 else None
|
|
650
|
+
right = (width_pt, color) if col_idx == last_col_idx else None
|
|
651
|
+
if top or right or bottom or left:
|
|
652
|
+
_set_cell_border(
|
|
653
|
+
cell,
|
|
654
|
+
top=top,
|
|
655
|
+
right=right,
|
|
656
|
+
bottom=bottom,
|
|
657
|
+
left=left,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _adjust_column_widths_for_outer_borders(
|
|
662
|
+
table, widths_pt: List[float]
|
|
663
|
+
) -> List[float]:
|
|
664
|
+
border_width = getattr(table, "_docs_border_width_pt", 0)
|
|
665
|
+
if border_width <= 0 or not widths_pt:
|
|
666
|
+
return widths_pt
|
|
667
|
+
adjusted = list(widths_pt)
|
|
668
|
+
if len(adjusted) == 1:
|
|
669
|
+
adjusted[0] = max(adjusted[0] - (border_width * 2), 0)
|
|
670
|
+
return adjusted
|
|
671
|
+
adjusted[0] = max(adjusted[0] - border_width, 0)
|
|
672
|
+
adjusted[-1] = max(adjusted[-1] - border_width, 0)
|
|
673
|
+
return adjusted
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _apply_table_parent_padding(table, parent_style: Dict[str, str]) -> None:
|
|
677
|
+
padding = _resolve_padding(parent_style)
|
|
678
|
+
if not padding:
|
|
679
|
+
return
|
|
680
|
+
top, right, bottom, left = _normalize_padding(padding)
|
|
681
|
+
left_pt = left * _PADDING_SCALE
|
|
682
|
+
right_pt = right * _PADDING_SCALE
|
|
683
|
+
if left_pt:
|
|
684
|
+
_set_table_indent(table, left_pt)
|
|
685
|
+
table._docs_parent_padding_pt = (
|
|
686
|
+
left_pt,
|
|
687
|
+
right_pt,
|
|
688
|
+
top * _PADDING_SCALE,
|
|
689
|
+
bottom * _PADDING_SCALE,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _set_table_indent(table, indent_pt: float) -> None:
|
|
694
|
+
if indent_pt <= 0:
|
|
695
|
+
return
|
|
696
|
+
tbl_pr = table._tbl.tblPr
|
|
697
|
+
if tbl_pr is None:
|
|
698
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
699
|
+
table._tbl.append(tbl_pr)
|
|
700
|
+
tbl_ind = tbl_pr.find(qn("w:tblInd"))
|
|
701
|
+
if tbl_ind is None:
|
|
702
|
+
tbl_ind = OxmlElement("w:tblInd")
|
|
703
|
+
tbl_pr.append(tbl_ind)
|
|
704
|
+
tbl_ind.set(qn("w:w"), str(int(indent_pt * 20)))
|
|
705
|
+
tbl_ind.set(qn("w:type"), "dxa")
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _adjust_column_widths_for_parent_padding(
|
|
709
|
+
table, widths_pt: List[float]
|
|
710
|
+
) -> List[float]:
|
|
711
|
+
padding = getattr(table, "_docs_parent_padding_pt", None)
|
|
712
|
+
if not padding:
|
|
713
|
+
return widths_pt
|
|
714
|
+
left_pt, right_pt, _top_pt, _bottom_pt = padding
|
|
715
|
+
shrink = max(left_pt + right_pt, 0)
|
|
716
|
+
if shrink <= 0:
|
|
717
|
+
return widths_pt
|
|
718
|
+
total = sum(widths_pt)
|
|
719
|
+
if total <= 0 or total <= shrink:
|
|
720
|
+
return widths_pt
|
|
721
|
+
scale = (total - shrink) / total
|
|
722
|
+
return [max(width * scale, 0) for width in widths_pt]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _set_cell_margins(cell, top: float, right: float, bottom: float, left: float) -> None:
|
|
726
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
727
|
+
tc_mar = tc_pr.find(qn("w:tcMar"))
|
|
728
|
+
if tc_mar is None:
|
|
729
|
+
tc_mar = OxmlElement("w:tcMar")
|
|
730
|
+
tc_pr.append(tc_mar)
|
|
731
|
+
|
|
732
|
+
for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
|
|
733
|
+
elem = tc_mar.find(qn(f"w:{side}"))
|
|
734
|
+
if elem is None:
|
|
735
|
+
elem = OxmlElement(f"w:{side}")
|
|
736
|
+
tc_mar.append(elem)
|
|
737
|
+
elem.set(qn("w:w"), str(int(value * 20)))
|
|
738
|
+
elem.set(qn("w:type"), "dxa")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _should_prevent_row_split(table_node: Tag, style_map: Dict[str, str]) -> bool:
|
|
742
|
+
"""Return True if w:cantSplit should be applied to this table's rows.
|
|
743
|
+
|
|
744
|
+
Triggers when:
|
|
745
|
+
- auto_page_breaks marked the table as fitting within one page
|
|
746
|
+
(data-docx-cant-split attribute), OR
|
|
747
|
+
- the agent explicitly set page-break-inside:avoid on the table.
|
|
748
|
+
"""
|
|
749
|
+
if table_node.get("data-docx-cant-split"):
|
|
750
|
+
return True
|
|
751
|
+
for key in ("page-break-inside", "break-inside"):
|
|
752
|
+
if style_map.get(key, "").strip().lower() == "avoid":
|
|
753
|
+
return True
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _set_table_cant_split(table) -> None:
|
|
758
|
+
"""Add w:cantSplit to every row so Word won't split a row across pages."""
|
|
759
|
+
for row in table.rows:
|
|
760
|
+
tr_pr = row._tr.get_or_add_trPr()
|
|
761
|
+
if tr_pr.find(qn("w:cantSplit")) is None:
|
|
762
|
+
cant_split = OxmlElement("w:cantSplit")
|
|
763
|
+
tr_pr.append(cant_split)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _collect_collapsed_borders(
|
|
767
|
+
row_cells: list[list],
|
|
768
|
+
row_colspans: list[list[int]],
|
|
769
|
+
max_cols: int,
|
|
770
|
+
css_rules,
|
|
771
|
+
) -> dict[str, Optional[tuple[float, str]]]:
|
|
772
|
+
"""Collect all six border positions for a border-collapse:collapse table.
|
|
773
|
+
|
|
774
|
+
Returns a dict with keys: inside_h, inside_v, top, bottom, left, right.
|
|
775
|
+
Each value is (width_pt, color_hex) or None.
|
|
776
|
+
|
|
777
|
+
The complete set is needed so _apply_collapsed_table_borders can write all six
|
|
778
|
+
directions into w:tblBorders without mixing in any cell-level tcBorders.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
def first_cell_border(cells, *props) -> Optional[tuple[float, str]]:
|
|
782
|
+
for cell_node in cells:
|
|
783
|
+
style = _compute_style_map(cell_node, css_rules)
|
|
784
|
+
for prop in props:
|
|
785
|
+
val = _parse_border(style.get(prop, ""))
|
|
786
|
+
if val:
|
|
787
|
+
return val
|
|
788
|
+
# Fall back to shorthand border
|
|
789
|
+
val = _parse_border(style.get("border", ""))
|
|
790
|
+
if val:
|
|
791
|
+
return val
|
|
792
|
+
return None
|
|
793
|
+
|
|
794
|
+
inside_h: Optional[tuple[float, str]] = None
|
|
795
|
+
inside_v: Optional[tuple[float, str]] = None
|
|
796
|
+
|
|
797
|
+
# interior-H: border-bottom on non-last rows, then border-top on non-first rows
|
|
798
|
+
for cells in row_cells[:-1]:
|
|
799
|
+
inside_h = first_cell_border(cells, "border-bottom")
|
|
800
|
+
if inside_h:
|
|
801
|
+
break
|
|
802
|
+
if not inside_h and len(row_cells) > 1:
|
|
803
|
+
for cells in row_cells[1:]:
|
|
804
|
+
inside_h = first_cell_border(cells, "border-top")
|
|
805
|
+
if inside_h:
|
|
806
|
+
break
|
|
807
|
+
|
|
808
|
+
# interior-V: border-right on non-last cols, then border-left on non-first cols
|
|
809
|
+
if max_cols > 1:
|
|
810
|
+
for row_idx, cells in enumerate(row_cells):
|
|
811
|
+
colspans = row_colspans[row_idx]
|
|
812
|
+
col_pos = 0
|
|
813
|
+
for cell_idx, cell_node in enumerate(cells):
|
|
814
|
+
span = colspans[cell_idx] if cell_idx < len(colspans) else 1
|
|
815
|
+
if col_pos + span < max_cols:
|
|
816
|
+
style = _compute_style_map(cell_node, css_rules)
|
|
817
|
+
inside_v = _parse_border(style.get("border-right", "")) or _parse_border(
|
|
818
|
+
style.get("border", "")
|
|
819
|
+
)
|
|
820
|
+
if inside_v:
|
|
821
|
+
break
|
|
822
|
+
col_pos += span
|
|
823
|
+
if inside_v:
|
|
824
|
+
break
|
|
825
|
+
|
|
826
|
+
if not inside_v:
|
|
827
|
+
for row_idx, cells in enumerate(row_cells):
|
|
828
|
+
colspans = row_colspans[row_idx]
|
|
829
|
+
col_pos = 0
|
|
830
|
+
for cell_idx, cell_node in enumerate(cells):
|
|
831
|
+
span = colspans[cell_idx] if cell_idx < len(colspans) else 1
|
|
832
|
+
if col_pos > 0:
|
|
833
|
+
style = _compute_style_map(cell_node, css_rules)
|
|
834
|
+
inside_v = _parse_border(style.get("border-left", ""))
|
|
835
|
+
if inside_v:
|
|
836
|
+
break
|
|
837
|
+
col_pos += span
|
|
838
|
+
if inside_v:
|
|
839
|
+
break
|
|
840
|
+
|
|
841
|
+
# outer edges — collect from the boundary cells
|
|
842
|
+
outer_top = first_cell_border(row_cells[0], "border-top") if row_cells else None
|
|
843
|
+
outer_bottom = first_cell_border(row_cells[-1], "border-bottom") if row_cells else None
|
|
844
|
+
|
|
845
|
+
first_col_cells = [cells[0] for cells in row_cells if cells]
|
|
846
|
+
outer_left = first_cell_border(first_col_cells, "border-left") if first_col_cells else None
|
|
847
|
+
|
|
848
|
+
def last_col_cell(cells, colspans):
|
|
849
|
+
col_pos = 0
|
|
850
|
+
last = None
|
|
851
|
+
for idx, cell_node in enumerate(cells):
|
|
852
|
+
span = colspans[idx] if idx < len(colspans) else 1
|
|
853
|
+
last = cell_node
|
|
854
|
+
col_pos += span
|
|
855
|
+
return last
|
|
856
|
+
|
|
857
|
+
last_col_cells = [
|
|
858
|
+
last_col_cell(cells, row_colspans[ri]) for ri, cells in enumerate(row_cells) if cells
|
|
859
|
+
]
|
|
860
|
+
last_col_cells = [c for c in last_col_cells if c is not None]
|
|
861
|
+
outer_right = first_cell_border(last_col_cells, "border-right") if last_col_cells else None
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
"inside_h": inside_h,
|
|
865
|
+
"inside_v": inside_v,
|
|
866
|
+
"top": outer_top,
|
|
867
|
+
"bottom": outer_bottom,
|
|
868
|
+
"left": outer_left,
|
|
869
|
+
"right": outer_right,
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _apply_collapsed_table_borders(
|
|
874
|
+
table,
|
|
875
|
+
collapsed_borders: dict[str, Optional[tuple[float, str]]],
|
|
876
|
+
) -> None:
|
|
877
|
+
"""Write all six border positions into w:tblBorders for a border-collapse:collapse table.
|
|
878
|
+
|
|
879
|
+
Using tblBorders exclusively (no tcBorders on any cell) guarantees that Word
|
|
880
|
+
draws exactly one line per edge and never mixes table-level and cell-level borders,
|
|
881
|
+
which is the root cause of double-line rendering.
|
|
882
|
+
"""
|
|
883
|
+
tbl_pr = table._tbl.tblPr
|
|
884
|
+
if tbl_pr is None:
|
|
885
|
+
tbl_pr = OxmlElement("w:tblPr")
|
|
886
|
+
table._tbl.append(tbl_pr)
|
|
887
|
+
borders = tbl_pr.find(qn("w:tblBorders"))
|
|
888
|
+
if borders is None:
|
|
889
|
+
borders = OxmlElement("w:tblBorders")
|
|
890
|
+
tbl_pr.append(borders)
|
|
891
|
+
|
|
892
|
+
# OOXML requires a specific child order inside w:tblBorders.
|
|
893
|
+
ordered_sides = [
|
|
894
|
+
("w:top", collapsed_borders.get("top")),
|
|
895
|
+
("w:left", collapsed_borders.get("left")),
|
|
896
|
+
("w:bottom", collapsed_borders.get("bottom")),
|
|
897
|
+
("w:right", collapsed_borders.get("right")),
|
|
898
|
+
("w:insideH", collapsed_borders.get("inside_h")),
|
|
899
|
+
("w:insideV", collapsed_borders.get("inside_v")),
|
|
900
|
+
]
|
|
901
|
+
# Clear existing children to guarantee order and avoid stale entries.
|
|
902
|
+
for child in list(borders):
|
|
903
|
+
borders.remove(child)
|
|
904
|
+
|
|
905
|
+
for tag_name, border_val in ordered_sides:
|
|
906
|
+
if not border_val:
|
|
907
|
+
continue
|
|
908
|
+
width_pt, color = border_val
|
|
909
|
+
el = OxmlElement(tag_name)
|
|
910
|
+
el.set(qn("w:val"), "single")
|
|
911
|
+
el.set(qn("w:sz"), _border_sz(width_pt))
|
|
912
|
+
el.set(qn("w:color"), color)
|
|
913
|
+
borders.append(el)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _add_cell_margins(
|
|
917
|
+
cell,
|
|
918
|
+
top: Optional[float] = None,
|
|
919
|
+
right: Optional[float] = None,
|
|
920
|
+
bottom: Optional[float] = None,
|
|
921
|
+
left: Optional[float] = None,
|
|
922
|
+
) -> None:
|
|
923
|
+
tc_pr = cell._tc.get_or_add_tcPr()
|
|
924
|
+
tc_mar = tc_pr.find(qn("w:tcMar"))
|
|
925
|
+
if tc_mar is None:
|
|
926
|
+
tc_mar = OxmlElement("w:tcMar")
|
|
927
|
+
tc_pr.append(tc_mar)
|
|
928
|
+
|
|
929
|
+
for side, value in [("top", top), ("right", right), ("bottom", bottom), ("left", left)]:
|
|
930
|
+
if value is None:
|
|
931
|
+
continue
|
|
932
|
+
elem = tc_mar.find(qn(f"w:{side}"))
|
|
933
|
+
current_val = 0
|
|
934
|
+
if elem is not None:
|
|
935
|
+
current = elem.get(qn("w:w"))
|
|
936
|
+
if current and current.isdigit():
|
|
937
|
+
current_val = int(current)
|
|
938
|
+
else:
|
|
939
|
+
elem = OxmlElement(f"w:{side}")
|
|
940
|
+
tc_mar.append(elem)
|
|
941
|
+
elem.set(qn("w:w"), str(current_val + int(value * 20)))
|
|
942
|
+
elem.set(qn("w:type"), "dxa")
|