@_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,1183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dom-to-pptx-based HTML → PPTX runner.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* 1. Read each slide HTML file, extract its <head> assets and body content.
|
|
7
|
+
* 2. Build a temporary "orchestrator" HTML file in the SAME directory as the
|
|
8
|
+
* slides so that relative paths (./assets/, ./_theme.css) resolve correctly.
|
|
9
|
+
* 3. Open the orchestrator in a headless Chromium page via Playwright.
|
|
10
|
+
* 4. Inject the dom-to-pptx self-contained browser bundle.
|
|
11
|
+
* 5. Call exportToPptx() on all .slide-host divs — each becomes one PPTX slide.
|
|
12
|
+
* dom-to-pptx uses window.getComputedStyle + getBoundingClientRect so every
|
|
13
|
+
* CSS effect (gradients, SVGs, custom fonts, shadows) is faithfully converted
|
|
14
|
+
* to native editable PPTX shapes and text boxes.
|
|
15
|
+
* 6. Intercept the browser download and write the PPTX to outputPath.
|
|
16
|
+
* 7. Delete the orchestrator file.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node html2pptx_runner.js \
|
|
20
|
+
* --output out.pptx \
|
|
21
|
+
* --layout LAYOUT_16x9_1280 \
|
|
22
|
+
* [--tmp-dir /tmp] \
|
|
23
|
+
* -- slide1.html slide2.html ...
|
|
24
|
+
*
|
|
25
|
+
* (--html2pptx accepted for backwards compat but unused)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// Isolate Node.js Playwright browsers from Python Playwright to prevent
|
|
31
|
+
// npm's cleanup from deleting Python's browser binaries.
|
|
32
|
+
const path = require('path');
|
|
33
|
+
if (!process.env.PLAYWRIGHT_BROWSERS_PATH) {
|
|
34
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(__dirname, '..', '..', '.playwright-browsers');
|
|
35
|
+
}
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const os = require('os');
|
|
38
|
+
const crypto = require('crypto');
|
|
39
|
+
const https = require('https');
|
|
40
|
+
const http = require('http');
|
|
41
|
+
|
|
42
|
+
// Layout → PPTX dimensions (inches) + expected HTML viewport (px)
|
|
43
|
+
const LAYOUTS = {
|
|
44
|
+
'LAYOUT_16x9_1280': { name: 'LAYOUT_16x9_1280', width: 13.333, height: 7.5, viewportW: 1280, viewportH: 720 },
|
|
45
|
+
'LAYOUT_16x9_1920': { name: 'LAYOUT_16x9_1920', width: 20.0, height: 11.25, viewportW: 1920, viewportH: 1080 },
|
|
46
|
+
'LAYOUT_16x9': { name: 'LAYOUT_16x9', width: 10.0, height: 5.625, viewportW: 960, viewportH: 540 },
|
|
47
|
+
'LAYOUT_4x3': { name: 'LAYOUT_4x3', width: 10.0, height: 7.5, viewportW: 960, viewportH: 720 },
|
|
48
|
+
'LAYOUT_16x10': { name: 'LAYOUT_16x10', width: 10.0, height: 6.25, viewportW: 960, viewportH: 625 },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Extra wait after page load so CDN fonts, Tailwind, etc. fully apply.
|
|
52
|
+
const SETTLE_MS = 2000;
|
|
53
|
+
|
|
54
|
+
// Timeout for the full export (all slides).
|
|
55
|
+
const EXPORT_TIMEOUT_MS = 180_000;
|
|
56
|
+
|
|
57
|
+
// ─── CSS scoping ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transform an inline <style> block from a slide so it is safe inside the
|
|
61
|
+
* orchestrator page that hosts multiple slides as sibling divs.
|
|
62
|
+
*
|
|
63
|
+
* Single-pass block-level parser — each CSS rule is handled exactly once:
|
|
64
|
+
*
|
|
65
|
+
* • html / body rules → selector replaced with ".slide-host:nth-child(N)"
|
|
66
|
+
* Background-color/font etc. are preserved.
|
|
67
|
+
* Width/height/overflow conflicts are harmless because
|
|
68
|
+
* the .slide-host inline style has higher specificity.
|
|
69
|
+
*
|
|
70
|
+
* • @-rules → kept verbatim (@keyframes, @font-face, @media …)
|
|
71
|
+
*
|
|
72
|
+
* • everything else → prefixed with ".slide-host:nth-child(N)" to prevent
|
|
73
|
+
* cross-slide class-name bleed.
|
|
74
|
+
*/
|
|
75
|
+
function scopeSelector(selector, scope) {
|
|
76
|
+
if (!selector) return selector;
|
|
77
|
+
if (/^:root\b/i.test(selector)) return selector.replace(/^:root\b/i, scope);
|
|
78
|
+
if (/^(html|body)\b/i.test(selector)) return selector.replace(/^(html|body)\b/i, scope);
|
|
79
|
+
return `${scope} ${selector}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function scopeSlideStyle(css, slideIndex) {
|
|
83
|
+
const scope = `.slide-host:nth-child(${slideIndex + 1})`;
|
|
84
|
+
let result = '';
|
|
85
|
+
let i = 0;
|
|
86
|
+
|
|
87
|
+
while (i < css.length) {
|
|
88
|
+
const braceOpen = css.indexOf('{', i);
|
|
89
|
+
if (braceOpen === -1) { result += css.slice(i); break; }
|
|
90
|
+
|
|
91
|
+
const selectorText = css.slice(i, braceOpen);
|
|
92
|
+
const trimmed = selectorText.trim();
|
|
93
|
+
|
|
94
|
+
// Walk to the matching closing brace (handles one level of nesting)
|
|
95
|
+
let depth = 1, j = braceOpen + 1;
|
|
96
|
+
while (j < css.length && depth > 0) {
|
|
97
|
+
if (css[j] === '{') depth++;
|
|
98
|
+
else if (css[j] === '}') depth--;
|
|
99
|
+
j++;
|
|
100
|
+
}
|
|
101
|
+
const block = css.slice(braceOpen, j); // "{ … }"
|
|
102
|
+
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
result += selectorText + block;
|
|
105
|
+
} else if (trimmed.startsWith('@')) {
|
|
106
|
+
// @-rules: keep verbatim — don't scope keyframes, font-face, etc.
|
|
107
|
+
result += selectorText + block;
|
|
108
|
+
} else {
|
|
109
|
+
const selectors = trimmed.split(',').map(s => s.trim()).filter(Boolean);
|
|
110
|
+
const scoped = selectors.map(s => scopeSelector(s, scope)).join(', ');
|
|
111
|
+
result += `${scoped} ${block}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
i = j;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── HTML parsing helpers ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert CSS ::before / ::after pseudo-element rules that carry tiling
|
|
124
|
+
* background patterns (background-image + background-size) into real <div>
|
|
125
|
+
* nodes injected as the first child of the matching element.
|
|
126
|
+
*
|
|
127
|
+
* dom-to-pptx iterates actual DOM nodes and cannot see pseudo-elements, so
|
|
128
|
+
* without this step any grid/dot pattern defined via ::before is invisible in
|
|
129
|
+
* the exported PPTX.
|
|
130
|
+
*
|
|
131
|
+
* Also strips those background properties from the pseudo-element rule so the
|
|
132
|
+
* browser does not double-render the pattern.
|
|
133
|
+
*/
|
|
134
|
+
function materializePseudoBackgrounds(html) {
|
|
135
|
+
// Collect all <style> block contents
|
|
136
|
+
const styleBlocks = [...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)].map(m => m[1]);
|
|
137
|
+
const styleContent = styleBlocks.join('\n');
|
|
138
|
+
|
|
139
|
+
const pseudoInjections = []; // { className, divStyle }
|
|
140
|
+
|
|
141
|
+
// Match every selector::before / ::after rule
|
|
142
|
+
const ruleRe = /([\w\s.#:-]+?)\s*::(?:before|after)\s*\{/g;
|
|
143
|
+
let rm;
|
|
144
|
+
while ((rm = ruleRe.exec(styleContent)) !== null) {
|
|
145
|
+
const selectorRaw = rm[1].trim();
|
|
146
|
+
const blockStart = rm.index + rm[0].length;
|
|
147
|
+
|
|
148
|
+
// Find the matching closing brace (depth-aware)
|
|
149
|
+
let depth = 1, pos = blockStart;
|
|
150
|
+
while (pos < styleContent.length && depth > 0) {
|
|
151
|
+
if (styleContent[pos] === '{') depth++;
|
|
152
|
+
else if (styleContent[pos] === '}') depth--;
|
|
153
|
+
pos++;
|
|
154
|
+
}
|
|
155
|
+
const ruleBody = styleContent.slice(blockStart, pos - 1);
|
|
156
|
+
|
|
157
|
+
if (!/background-image\s*:/i.test(ruleBody) || !/background-size\s*:/i.test(ruleBody)) continue;
|
|
158
|
+
|
|
159
|
+
// Extract background-image with paren-aware scan so gradient commas
|
|
160
|
+
// don't prematurely terminate the value
|
|
161
|
+
const bgImageKeyStart = ruleBody.search(/background-image\s*:/i);
|
|
162
|
+
if (bgImageKeyStart < 0) continue;
|
|
163
|
+
let bgImageValue = '';
|
|
164
|
+
let valStart = ruleBody.indexOf(':', bgImageKeyStart) + 1;
|
|
165
|
+
let d = 0;
|
|
166
|
+
for (let vi = valStart; vi < ruleBody.length; vi++) {
|
|
167
|
+
const ch = ruleBody[vi];
|
|
168
|
+
if (ch === '(') d++;
|
|
169
|
+
else if (ch === ')') d--;
|
|
170
|
+
else if (ch === ';' && d === 0) break;
|
|
171
|
+
bgImageValue += ch;
|
|
172
|
+
}
|
|
173
|
+
bgImageValue = bgImageValue.trim().replace(/\s+/g, ' ');
|
|
174
|
+
|
|
175
|
+
const bgSizeM = ruleBody.match(/background-size\s*:\s*([^;]+)/i);
|
|
176
|
+
if (!bgSizeM) continue;
|
|
177
|
+
const bgSize = bgSizeM[1].trim();
|
|
178
|
+
|
|
179
|
+
const zIndexM = ruleBody.match(/z-index\s*:\s*([^;]+)/i);
|
|
180
|
+
const zIndex = zIndexM ? zIndexM[1].trim() : '0';
|
|
181
|
+
|
|
182
|
+
const divStyle = `position:absolute;inset:0;background-image:${bgImageValue};` +
|
|
183
|
+
`background-size:${bgSize};z-index:${zIndex};pointer-events:none;`;
|
|
184
|
+
|
|
185
|
+
// Extract every class name from the selector (.slide-root, .bg-grid, …)
|
|
186
|
+
for (const [, cls] of selectorRaw.matchAll(/\.([\w-]+)/g)) {
|
|
187
|
+
pseudoInjections.push({ className: cls, divStyle });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (pseudoInjections.length === 0) return html;
|
|
192
|
+
|
|
193
|
+
let result = html;
|
|
194
|
+
|
|
195
|
+
// 1. Neutralize the ::before/::after background in the <style> blocks so
|
|
196
|
+
// the browser doesn't double-render the pattern on top of our real div.
|
|
197
|
+
result = result.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (_, attrs, css) => {
|
|
198
|
+
const cleaned = css.replace(
|
|
199
|
+
/([\w\s.#:-]+?)\s*::(?:before|after)\s*\{([^}]*)\}/g,
|
|
200
|
+
(full, sel, body) => {
|
|
201
|
+
if (!/background-image\s*:/i.test(body) || !/background-size\s*:/i.test(body)) return full;
|
|
202
|
+
const neutralized = body
|
|
203
|
+
.replace(/background-image\s*:[^;]+;?/gi, 'background-image:none;')
|
|
204
|
+
.replace(/background-size\s*:[^;]+;?/gi, '');
|
|
205
|
+
return `${sel}::before{${neutralized}}`;
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
return `<style${attrs}>${cleaned}</style>`;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// 2. Inject a real <div> as the first child of each matched element
|
|
212
|
+
for (const { className, divStyle } of pseudoInjections) {
|
|
213
|
+
const injectedDiv = `<div style="${divStyle}"></div>`;
|
|
214
|
+
const escapedCls = className.replace(/[-]/g, '\\-');
|
|
215
|
+
// Match any block-level opening tag that carries the target class
|
|
216
|
+
const tagRe = new RegExp(
|
|
217
|
+
`(<(?:div|section|main|article|aside|header|footer|span|a)[^>]*class="[^"]*\\b${escapedCls}\\b[^"]*"[^>]*>)`,
|
|
218
|
+
'g'
|
|
219
|
+
);
|
|
220
|
+
result = result.replace(tagRe, `$1${injectedDiv}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseSlideHtml(html) {
|
|
227
|
+
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
228
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
229
|
+
return {
|
|
230
|
+
head: headMatch ? headMatch[1] : '',
|
|
231
|
+
body: bodyMatch ? bodyMatch[1] : html,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isRemoteHref(href) {
|
|
236
|
+
return /^https?:\/\//i.test(href) || href.startsWith('//') || href.startsWith('data:');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveStylesheetPath(href, slideDir) {
|
|
240
|
+
const cleanHref = href.split('#')[0].split('?')[0];
|
|
241
|
+
if (!cleanHref) return null;
|
|
242
|
+
|
|
243
|
+
if (cleanHref.startsWith('file://')) {
|
|
244
|
+
let filePath = cleanHref.replace(/^file:\/\//i, '');
|
|
245
|
+
if (/^\/[A-Za-z]:/.test(filePath)) filePath = filePath.slice(1);
|
|
246
|
+
return filePath.replace(/\//g, path.sep);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (path.isAbsolute(cleanHref)) return cleanHref;
|
|
250
|
+
return path.resolve(slideDir, cleanHref);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function patchDomToPptxBundle(bundleCode, layoutName, layout) {
|
|
254
|
+
const layoutString = JSON.stringify(layoutName);
|
|
255
|
+
const defineLayoutCode = [
|
|
256
|
+
`pptx.defineLayout({ name: ${layoutString}, width: ${layout.width}, height: ${layout.height} });`,
|
|
257
|
+
`pptx.layout = ${layoutString};`,
|
|
258
|
+
].join(' ');
|
|
259
|
+
|
|
260
|
+
let patchedCode = bundleCode;
|
|
261
|
+
let patchedLayoutName = false;
|
|
262
|
+
let patchedLayoutSize = false;
|
|
263
|
+
|
|
264
|
+
patchedCode = patchedCode.replace(
|
|
265
|
+
/pptx\.layout\s*=\s*'LAYOUT_16x9';/,
|
|
266
|
+
() => {
|
|
267
|
+
patchedLayoutName = true;
|
|
268
|
+
return defineLayoutCode;
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
patchedCode = patchedCode.replace(
|
|
273
|
+
/const PPTX_WIDTH_IN\s*=\s*10;\s*const PPTX_HEIGHT_IN\s*=\s*5\.625;/,
|
|
274
|
+
() => {
|
|
275
|
+
patchedLayoutSize = true;
|
|
276
|
+
return `const PPTX_WIDTH_IN = ${layout.width};\n const PPTX_HEIGHT_IN = ${layout.height};`;
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (!patchedLayoutName || !patchedLayoutSize) {
|
|
281
|
+
throw new Error('Failed to patch dom-to-pptx bundle for custom layout handling.');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return patchedCode;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Extract <link> tags (deduplicated by href) and <style> blocks (scoped to the
|
|
289
|
+
* slide's nth-child position) from a slide's <head> string.
|
|
290
|
+
*/
|
|
291
|
+
function extractHeadAssets(headHtml, seenLinks, slideIndex, slideDir) {
|
|
292
|
+
const assets = [];
|
|
293
|
+
|
|
294
|
+
// <link> tags — local stylesheets are inlined + scoped, remote assets stay links
|
|
295
|
+
for (const m of headHtml.matchAll(/<link\b[^>]*>/gi)) {
|
|
296
|
+
const tag = m[0];
|
|
297
|
+
const relMatch = tag.match(/rel\s*=\s*["']([^"']+)["']/i);
|
|
298
|
+
const rel = relMatch ? relMatch[1].toLowerCase() : '';
|
|
299
|
+
const hrefMatch = tag.match(/href\s*=\s*["']([^"']+)["']/i);
|
|
300
|
+
const href = hrefMatch ? hrefMatch[1] : tag;
|
|
301
|
+
|
|
302
|
+
if (hrefMatch && rel.includes('stylesheet') && !isRemoteHref(href)) {
|
|
303
|
+
const stylesheetPath = resolveStylesheetPath(href, slideDir);
|
|
304
|
+
if (stylesheetPath && fs.existsSync(stylesheetPath)) {
|
|
305
|
+
const rawCss = fs.readFileSync(stylesheetPath, 'utf8');
|
|
306
|
+
const scopedCss = scopeSlideStyle(rawCss, slideIndex);
|
|
307
|
+
assets.push(`<style>${scopedCss}</style>`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!seenLinks.has(href)) {
|
|
313
|
+
seenLinks.add(href);
|
|
314
|
+
const patched = href.includes('fonts.googleapis.com') && !tag.includes('crossorigin')
|
|
315
|
+
? tag.replace('>', ' crossorigin="anonymous">')
|
|
316
|
+
: tag;
|
|
317
|
+
assets.push(patched);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// <style> blocks — scoped per slide to prevent cross-slide bleed
|
|
322
|
+
for (const m of headHtml.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
323
|
+
const rawCss = m[1];
|
|
324
|
+
const scopedCss = scopeSlideStyle(rawCss, slideIndex);
|
|
325
|
+
assets.push(`<style>${scopedCss}</style>`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return assets;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Orchestrator builder ─────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
function buildOrchestratorHtml(slides, layout, domToPptxBundlePath) {
|
|
334
|
+
const seenLinks = new Set();
|
|
335
|
+
const allAssets = [];
|
|
336
|
+
const slideHostDivs = [];
|
|
337
|
+
|
|
338
|
+
slides.forEach(({ head, body, dir }, idx) => {
|
|
339
|
+
for (const asset of extractHeadAssets(head, seenLinks, idx, dir)) {
|
|
340
|
+
allAssets.push(asset);
|
|
341
|
+
}
|
|
342
|
+
slideHostDivs.push(
|
|
343
|
+
`<div class="slide-host" style="` +
|
|
344
|
+
`width:${layout.viewportW}px;height:${layout.viewportH}px;` +
|
|
345
|
+
`position:relative;overflow:hidden;flex-shrink:0;">` +
|
|
346
|
+
body +
|
|
347
|
+
`</div>`
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const bundleCode = patchDomToPptxBundle(
|
|
352
|
+
fs.readFileSync(domToPptxBundlePath, 'utf8'),
|
|
353
|
+
layout.name,
|
|
354
|
+
layout
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return `<!DOCTYPE html>
|
|
358
|
+
<html>
|
|
359
|
+
<head>
|
|
360
|
+
<meta charset="utf-8" />
|
|
361
|
+
${allAssets.join('\n ')}
|
|
362
|
+
<style>
|
|
363
|
+
* { box-sizing: border-box; }
|
|
364
|
+
/* Orchestrator resets — must NOT set overflow:hidden or fixed height on body */
|
|
365
|
+
html { margin: 0; padding: 0; background: #1a1a1a; }
|
|
366
|
+
body { margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
|
|
367
|
+
/* The .slide div often carries a redundant background identical to .slide-wrapper / .slide-host.
|
|
368
|
+
When .slide has z-index > 0 (stacking context), dom-to-pptx places its background shape
|
|
369
|
+
on top of decorative siblings (orbs, grid-lines) that have lower z-indices, making those
|
|
370
|
+
elements invisible. The background is already provided by .slide-host and .slide-wrapper,
|
|
371
|
+
so making .slide transparent here restores the correct layering in the exported PPTX. */
|
|
372
|
+
.slide-host .slide { background: transparent !important; }
|
|
373
|
+
</style>
|
|
374
|
+
</head>
|
|
375
|
+
<body>
|
|
376
|
+
${slideHostDivs.join('\n ')}
|
|
377
|
+
<script>${bundleCode}</script>
|
|
378
|
+
</body>
|
|
379
|
+
</html>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ─── File URL helper ──────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
function toFileUrl(absPath) {
|
|
385
|
+
let p = path.resolve(absPath).replace(/\\/g, '/');
|
|
386
|
+
if (!p.startsWith('/')) p = '/' + p;
|
|
387
|
+
return `file://${p}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Font Awesome materialization ────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Maps Font Awesome style-prefix classes to the font-family and font-weight
|
|
394
|
+
* that PowerPoint must use to render the glyph correctly.
|
|
395
|
+
*/
|
|
396
|
+
const FA_STYLE_MAP = {
|
|
397
|
+
// FA 6 long-form
|
|
398
|
+
'fa-solid': { family: 'Font Awesome 6 Free', weight: 900 },
|
|
399
|
+
'fa-regular': { family: 'Font Awesome 6 Free', weight: 400 },
|
|
400
|
+
'fa-light': { family: 'Font Awesome 6 Free', weight: 300 },
|
|
401
|
+
'fa-thin': { family: 'Font Awesome 6 Free', weight: 100 },
|
|
402
|
+
'fa-brands': { family: 'Font Awesome 6 Brands', weight: 400 },
|
|
403
|
+
// FA 6 / FA 5 short-form
|
|
404
|
+
'fas': { family: 'Font Awesome 6 Free', weight: 900 },
|
|
405
|
+
'far': { family: 'Font Awesome 6 Free', weight: 400 },
|
|
406
|
+
'fal': { family: 'Font Awesome 6 Free', weight: 300 },
|
|
407
|
+
'fat': { family: 'Font Awesome 6 Free', weight: 100 },
|
|
408
|
+
'fab': { family: 'Font Awesome 6 Brands', weight: 400 },
|
|
409
|
+
// FA 5 fallback (free tier only had solid + brands)
|
|
410
|
+
'fa': { family: 'Font Awesome 5 Free', weight: 900 },
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/** Return FA CSS hrefs found in any slide <head>. */
|
|
414
|
+
function collectFontAwesomeHrefs(slides) {
|
|
415
|
+
const FA_PATTERNS = ['fontawesome', 'font-awesome', 'fortawesome'];
|
|
416
|
+
const seen = new Set();
|
|
417
|
+
for (const { head } of slides) {
|
|
418
|
+
for (const m of head.matchAll(/<link\b[^>]*>/gi)) {
|
|
419
|
+
const hrefMatch = m[0].match(/href\s*=\s*["']([^"']+)["']/i);
|
|
420
|
+
if (!hrefMatch) continue;
|
|
421
|
+
const href = hrefMatch[1];
|
|
422
|
+
if (FA_PATTERNS.some(p => href.toLowerCase().includes(p)) && !seen.has(href)) {
|
|
423
|
+
seen.add(href);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return [...seen];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Parse Font Awesome CSS and return a map of icon class → Unicode character.
|
|
432
|
+
* Handles both minified and pretty-printed CSS, and both :before / ::before.
|
|
433
|
+
*/
|
|
434
|
+
function parseFontAwesomeIconMap(cssText) {
|
|
435
|
+
const iconMap = {};
|
|
436
|
+
const re = /\.fa-([\w-]+)::?before\s*\{[^}]*content:\s*["']\\([0-9a-fA-F]+)["']/g;
|
|
437
|
+
let m;
|
|
438
|
+
while ((m = re.exec(cssText)) !== null) {
|
|
439
|
+
iconMap[`fa-${m[1]}`] = String.fromCodePoint(parseInt(m[2], 16));
|
|
440
|
+
}
|
|
441
|
+
return iconMap;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Download Font Awesome TTF font files and build the icon map.
|
|
446
|
+
* Returns { fonts: [{name, url}], iconMap: {className: unicodeChar} }.
|
|
447
|
+
*
|
|
448
|
+
* Font Awesome uses CSS ::before pseudo-elements for icons, which dom-to-pptx
|
|
449
|
+
* cannot see. We parse the icon map here so materializeFontAwesomeIcons() can
|
|
450
|
+
* replace <i> elements with real <span> nodes containing the glyph character.
|
|
451
|
+
*/
|
|
452
|
+
async function downloadFontAwesomeFonts(faHrefs) {
|
|
453
|
+
if (!faHrefs.length) return { fonts: [], iconMap: {} };
|
|
454
|
+
|
|
455
|
+
const fonts = [];
|
|
456
|
+
const iconMap = {};
|
|
457
|
+
const seenUrls = new Set();
|
|
458
|
+
|
|
459
|
+
for (const href of faHrefs) {
|
|
460
|
+
let css;
|
|
461
|
+
try {
|
|
462
|
+
css = (await fetchBuf(href)).toString('utf8');
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.warn(`[fa] Could not fetch ${href}:`, e.message);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
Object.assign(iconMap, parseFontAwesomeIconMap(css));
|
|
469
|
+
|
|
470
|
+
// Resolve relative font URLs against the CSS file's base URL.
|
|
471
|
+
const baseUrl = href.substring(0, href.lastIndexOf('/') + 1);
|
|
472
|
+
|
|
473
|
+
const faceRe = /@font-face\s*\{([\s\S]*?)\}/g;
|
|
474
|
+
let fm;
|
|
475
|
+
while ((fm = faceRe.exec(css)) !== null) {
|
|
476
|
+
const block = fm[1];
|
|
477
|
+
const nameMatch = block.match(/font-family\s*:\s*['"]([^'"]+)['"]/);
|
|
478
|
+
if (!nameMatch) continue;
|
|
479
|
+
|
|
480
|
+
// Prefer TTF — the format PowerPoint can embed.
|
|
481
|
+
const ttfMatch = block.match(/url\(['"]?([^'")\s]+\.ttf)['"]?\)/i);
|
|
482
|
+
if (!ttfMatch) continue;
|
|
483
|
+
|
|
484
|
+
const rawUrl = ttfMatch[1];
|
|
485
|
+
const fontUrl = rawUrl.startsWith('http') ? rawUrl : new URL(rawUrl, baseUrl).href;
|
|
486
|
+
if (seenUrls.has(fontUrl)) continue;
|
|
487
|
+
seenUrls.add(fontUrl);
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const buf = await fetchBuf(fontUrl);
|
|
491
|
+
const name = nameMatch[1].trim();
|
|
492
|
+
fonts.push({ name, url: `data:font/ttf;base64,${buf.toString('base64')}` });
|
|
493
|
+
console.log(`[fa] "${name}" (${fontUrl.split('/').pop()}) — ${Math.round(buf.length / 1024)} KB`);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
console.warn(`[fa] Could not download font:`, e.message);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(`[fa] ${fonts.length} font file(s), ${Object.keys(iconMap).length} icons mapped`);
|
|
501
|
+
return { fonts, iconMap };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Replace Font Awesome <i> elements with real <span> nodes containing the
|
|
506
|
+
* Unicode glyph character so dom-to-pptx can see and embed them.
|
|
507
|
+
*
|
|
508
|
+
* dom-to-pptx traverses actual DOM nodes; it cannot read CSS ::before
|
|
509
|
+
* pseudo-element content. Without this step, FA icons would be invisible in
|
|
510
|
+
* the exported PPTX even if the font file is embedded.
|
|
511
|
+
*/
|
|
512
|
+
function materializeFontAwesomeIcons(html, iconMap) {
|
|
513
|
+
if (!Object.keys(iconMap).length) return html;
|
|
514
|
+
|
|
515
|
+
return html.replace(/<i\b[^>]*class=["']([^"']*)["'][^>]*>\s*<\/i>/gi, (match, classList) => {
|
|
516
|
+
const classes = classList.trim().split(/\s+/);
|
|
517
|
+
|
|
518
|
+
// Determine font-family + weight from the style-prefix class.
|
|
519
|
+
let styleInfo = null;
|
|
520
|
+
for (const cls of classes) {
|
|
521
|
+
if (FA_STYLE_MAP[cls]) { styleInfo = FA_STYLE_MAP[cls]; break; }
|
|
522
|
+
}
|
|
523
|
+
if (!styleInfo) return match;
|
|
524
|
+
|
|
525
|
+
// Find the icon class (e.g. fa-star) — starts with fa- but is not a prefix.
|
|
526
|
+
const iconClass = classes.find(c => c.startsWith('fa-') && !FA_STYLE_MAP[c]);
|
|
527
|
+
if (!iconClass || !iconMap[iconClass]) return match;
|
|
528
|
+
|
|
529
|
+
const { family, weight } = styleInfo;
|
|
530
|
+
const glyph = iconMap[iconClass];
|
|
531
|
+
return `<span style="font-family:'${family}';font-weight:${weight};font-style:normal;">${glyph}</span>`;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ─── Font pre-download (bypass CORS) ─────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Fetch a URL as a Buffer, following redirects.
|
|
539
|
+
* The minimal User-Agent causes Google Fonts to return TTF instead of WOFF2
|
|
540
|
+
* or EOT — TTF is the format PowerPoint can embed; WOFF2/EOT are browser-only.
|
|
541
|
+
*/
|
|
542
|
+
function fetchBuf(url, ua = 'Mozilla/5.0') {
|
|
543
|
+
return new Promise((resolve, reject) => {
|
|
544
|
+
const mod = url.startsWith('https:') ? https : http;
|
|
545
|
+
mod.get(url, { headers: { 'User-Agent': ua } }, res => {
|
|
546
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
547
|
+
return fetchBuf(res.headers.location, ua).then(resolve, reject);
|
|
548
|
+
}
|
|
549
|
+
if (res.statusCode < 200 || res.statusCode >= 400) {
|
|
550
|
+
res.resume();
|
|
551
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
552
|
+
}
|
|
553
|
+
const chunks = [];
|
|
554
|
+
res.on('data', c => chunks.push(c));
|
|
555
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
556
|
+
res.on('error', reject);
|
|
557
|
+
}).on('error', reject);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Collect unique Google Fonts hrefs from all slide <head> sections. */
|
|
562
|
+
function collectGoogleFontsHrefs(slides) {
|
|
563
|
+
const seen = new Set();
|
|
564
|
+
for (const { head } of slides) {
|
|
565
|
+
for (const m of head.matchAll(/<link\b[^>]*>/gi)) {
|
|
566
|
+
const hrefMatch = m[0].match(/href\s*=\s*["']([^"']+)["']/i);
|
|
567
|
+
if (!hrefMatch) continue;
|
|
568
|
+
const href = hrefMatch[1];
|
|
569
|
+
if (href.includes('fonts.googleapis.com') && !seen.has(href)) seen.add(href);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return [...seen];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Pre-download Google Fonts as TTF and return data: URI descriptors for
|
|
577
|
+
* dom-to-pptx's `fonts` option. One descriptor per unique font-family name
|
|
578
|
+
* (the first/lightest weight encountered, which serves as the "regular" face).
|
|
579
|
+
*
|
|
580
|
+
* Using a data: URI means the browser page can fetch() it without CORS issues.
|
|
581
|
+
* The `type` will default to 'ttf' inside dom-to-pptx because the URL has no
|
|
582
|
+
* recognisable extension — and TTF is exactly what we embed.
|
|
583
|
+
*/
|
|
584
|
+
async function downloadGoogleFonts(googleFontsHrefs) {
|
|
585
|
+
if (!googleFontsHrefs.length) return [];
|
|
586
|
+
|
|
587
|
+
const fonts = [];
|
|
588
|
+
const seenFonts = new Set(); // one variant per family name
|
|
589
|
+
const seenFileUrls = new Set();
|
|
590
|
+
|
|
591
|
+
for (const href of googleFontsHrefs) {
|
|
592
|
+
let css;
|
|
593
|
+
try {
|
|
594
|
+
css = (await fetchBuf(href)).toString('utf8');
|
|
595
|
+
} catch (e) {
|
|
596
|
+
console.warn(`[fonts] Could not fetch ${href}:`, e.message);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const faceRe = /@font-face\s*\{([\s\S]*?)\}/g;
|
|
601
|
+
let m;
|
|
602
|
+
while ((m = faceRe.exec(css)) !== null) {
|
|
603
|
+
const block = m[1];
|
|
604
|
+
const nameMatch = block.match(/font-family\s*:\s*['"]?([^;'"]+)['"]?/);
|
|
605
|
+
const urlMatch = block.match(/url\(['"]?(https?:\/\/fonts\.gstatic\.com[^'")\s]+)['"]?\)/);
|
|
606
|
+
if (!nameMatch || !urlMatch) continue;
|
|
607
|
+
|
|
608
|
+
const name = nameMatch[1].trim();
|
|
609
|
+
const fontUrl = urlMatch[1];
|
|
610
|
+
|
|
611
|
+
// One face per family keeps the PPTX embeddedFontLst clean.
|
|
612
|
+
if (seenFonts.has(name) || seenFileUrls.has(fontUrl)) continue;
|
|
613
|
+
seenFonts.add(name);
|
|
614
|
+
seenFileUrls.add(fontUrl);
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const buf = await fetchBuf(fontUrl);
|
|
618
|
+
fonts.push({ name, url: `data:font/ttf;base64,${buf.toString('base64')}` });
|
|
619
|
+
console.log(`[fonts] "${name}" — ${Math.round(buf.length / 1024)} KB`);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
console.warn(`[fonts] Could not download ${fontUrl}:`, e.message);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log(`[fonts] ${fonts.length} font(s) ready for embedding`);
|
|
627
|
+
return fonts;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
631
|
+
|
|
632
|
+
async function main() {
|
|
633
|
+
const args = process.argv.slice(2);
|
|
634
|
+
let outputPath = '';
|
|
635
|
+
let layoutName = 'LAYOUT_16x9_1280';
|
|
636
|
+
let tmpDir = os.tmpdir();
|
|
637
|
+
let htmlFiles = [];
|
|
638
|
+
|
|
639
|
+
for (let i = 0; i < args.length; i++) {
|
|
640
|
+
if (args[i] === '--output') outputPath = args[++i];
|
|
641
|
+
else if (args[i] === '--layout') layoutName = args[++i];
|
|
642
|
+
else if (args[i] === '--tmp-dir') tmpDir = args[++i];
|
|
643
|
+
else if (args[i] === '--html2pptx') ++i; // backwards compat, unused
|
|
644
|
+
else if (args[i] === '--') { htmlFiles = args.slice(i + 1); break; }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!outputPath || !htmlFiles.length) {
|
|
648
|
+
console.error(
|
|
649
|
+
'Usage: node html2pptx_runner.js --output out.pptx ' +
|
|
650
|
+
'--layout LAYOUT_16x9_1280 [--tmp-dir /tmp] -- slide1.html ...'
|
|
651
|
+
);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const layout = LAYOUTS[layoutName];
|
|
656
|
+
if (!layout) {
|
|
657
|
+
console.error(`Unknown layout "${layoutName}". Valid: ${Object.keys(LAYOUTS).join(', ')}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const domToPptxBundle = path.resolve(
|
|
662
|
+
__dirname, '..', '..', 'node_modules', 'dom-to-pptx', 'dist', 'dom-to-pptx.bundle.js'
|
|
663
|
+
);
|
|
664
|
+
if (!fs.existsSync(domToPptxBundle)) {
|
|
665
|
+
console.error(`dom-to-pptx bundle not found at: ${domToPptxBundle}`);
|
|
666
|
+
console.error('Run: npm install dom-to-pptx');
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const slides = htmlFiles.map(filePath => ({
|
|
671
|
+
...parseSlideHtml(materializePseudoBackgrounds(fs.readFileSync(filePath, 'utf8'))),
|
|
672
|
+
dir: path.dirname(path.resolve(filePath)),
|
|
673
|
+
}));
|
|
674
|
+
const slideDir = slides[0].dir;
|
|
675
|
+
const orchFile = path.join(slideDir, `._orchestrator_${crypto.randomBytes(6).toString('hex')}.html`);
|
|
676
|
+
|
|
677
|
+
let embeddedFonts = [];
|
|
678
|
+
|
|
679
|
+
// Pre-download Google Fonts as TTF in Node.js (no CORS) so the browser
|
|
680
|
+
// page can embed them via fetch('data:font/ttf;base64,...') without errors.
|
|
681
|
+
const googleFontsHrefs = collectGoogleFontsHrefs(slides);
|
|
682
|
+
try {
|
|
683
|
+
embeddedFonts = await downloadGoogleFonts(googleFontsHrefs);
|
|
684
|
+
} catch (e) {
|
|
685
|
+
console.warn('[fonts] Pre-download failed, fonts will not be embedded:', e.message);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Font Awesome: download TTF files and materialize <i> icon elements into
|
|
689
|
+
// real <span> nodes so dom-to-pptx can see them (it cannot read ::before).
|
|
690
|
+
const faHrefs = collectFontAwesomeHrefs(slides);
|
|
691
|
+
if (faHrefs.length) {
|
|
692
|
+
try {
|
|
693
|
+
const { fonts: faFonts, iconMap } = await downloadFontAwesomeFonts(faHrefs);
|
|
694
|
+
embeddedFonts = [...embeddedFonts, ...faFonts];
|
|
695
|
+
if (Object.keys(iconMap).length) {
|
|
696
|
+
slides.forEach(slide => {
|
|
697
|
+
slide.body = materializeFontAwesomeIcons(slide.body, iconMap);
|
|
698
|
+
});
|
|
699
|
+
console.log('[fa] Icons materialized in slide bodies');
|
|
700
|
+
}
|
|
701
|
+
} catch (e) {
|
|
702
|
+
console.warn('[fa] Font Awesome processing failed:', e.message);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
fs.writeFileSync(orchFile, buildOrchestratorHtml(slides, layout, domToPptxBundle), 'utf8');
|
|
707
|
+
|
|
708
|
+
const { chromium } = require('playwright');
|
|
709
|
+
const browser = await chromium.launch();
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const context = await browser.newContext({
|
|
713
|
+
viewport: { width: layout.viewportW, height: layout.viewportH * htmlFiles.length },
|
|
714
|
+
acceptDownloads: true,
|
|
715
|
+
});
|
|
716
|
+
const page = await context.newPage();
|
|
717
|
+
page.on('pageerror', err => console.error('[page error]', err.message));
|
|
718
|
+
page.on('console', msg => { if (msg.type() === 'error') console.error('[page console]', msg.text()); });
|
|
719
|
+
|
|
720
|
+
await page.goto(toFileUrl(orchFile), { waitUntil: 'load', timeout: 60_000 });
|
|
721
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
722
|
+
|
|
723
|
+
// dom-to-pptx exports descendant nodes of `.slide-host`, but it does not
|
|
724
|
+
// carry over the orchestrator page background. When local theme CSS sets
|
|
725
|
+
// the slide base via `html/body` (and pseudo-elements like `body::before`)
|
|
726
|
+
// the slide ends up exporting onto a default white PPT background, which
|
|
727
|
+
// washes out every semi-transparent shape. We scope local CSS to the host
|
|
728
|
+
// above, then rasterize ONLY the host's own background/pseudo layer into a
|
|
729
|
+
// real child image. Content descendants remain as native PPT text/shapes.
|
|
730
|
+
await page.addStyleTag({
|
|
731
|
+
content: [
|
|
732
|
+
'.slide-host[data-rasterizing-bg="1"] * { visibility: hidden !important; }',
|
|
733
|
+
'.slide-host[data-raster-bg="1"]::before, .slide-host[data-raster-bg="1"]::after {',
|
|
734
|
+
' content: none !important;',
|
|
735
|
+
' display: none !important;',
|
|
736
|
+
' background: none !important;',
|
|
737
|
+
'}',
|
|
738
|
+
].join('\n'),
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const hostCount = await page.locator('.slide-host').count();
|
|
742
|
+
for (let i = 0; i < hostCount; i++) {
|
|
743
|
+
const host = page.locator('.slide-host').nth(i);
|
|
744
|
+
const needsRasterBg = await host.evaluate((el) => {
|
|
745
|
+
function hasPaint(style) {
|
|
746
|
+
if (!style) return false;
|
|
747
|
+
const bgImage = style.backgroundImage && style.backgroundImage !== 'none';
|
|
748
|
+
const bgColor = style.backgroundColor &&
|
|
749
|
+
style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
|
|
750
|
+
style.backgroundColor !== 'transparent';
|
|
751
|
+
return bgImage || bgColor;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return hasPaint(window.getComputedStyle(el)) ||
|
|
755
|
+
hasPaint(window.getComputedStyle(el, '::before')) ||
|
|
756
|
+
hasPaint(window.getComputedStyle(el, '::after'));
|
|
757
|
+
});
|
|
758
|
+
if (!needsRasterBg) continue;
|
|
759
|
+
|
|
760
|
+
await host.evaluate(el => el.setAttribute('data-rasterizing-bg', '1'));
|
|
761
|
+
const bgBuffer = await host.screenshot({ type: 'png' });
|
|
762
|
+
await host.evaluate(el => el.removeAttribute('data-rasterizing-bg'));
|
|
763
|
+
|
|
764
|
+
await host.evaluate((el, dataUrl) => {
|
|
765
|
+
const bg = document.createElement('img');
|
|
766
|
+
bg.src = dataUrl;
|
|
767
|
+
bg.alt = '';
|
|
768
|
+
bg.setAttribute('data-raster-bg-layer', '1');
|
|
769
|
+
bg.style.position = 'absolute';
|
|
770
|
+
bg.style.inset = '0';
|
|
771
|
+
bg.style.width = '100%';
|
|
772
|
+
bg.style.height = '100%';
|
|
773
|
+
bg.style.pointerEvents = 'none';
|
|
774
|
+
bg.style.zIndex = '0';
|
|
775
|
+
|
|
776
|
+
el.insertBefore(bg, el.firstChild);
|
|
777
|
+
el.style.background = 'none';
|
|
778
|
+
el.style.backgroundImage = 'none';
|
|
779
|
+
el.style.backgroundColor = 'transparent';
|
|
780
|
+
el.setAttribute('data-raster-bg', '1');
|
|
781
|
+
}, `data:image/png;base64,${bgBuffer.toString('base64')}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Preserve Chromium's actual wrapped-line geometry for large rich-text
|
|
785
|
+
// headings. If we export a multi-line heading as one reflowable PPT text
|
|
786
|
+
// box, PowerPoint chooses its own wrap/line-height and the title can drift
|
|
787
|
+
// into nearby pills/cards. We keep the source HTML untouched and only
|
|
788
|
+
// rewrite the temporary export DOM into one absolutely positioned text box
|
|
789
|
+
// per rendered browser line.
|
|
790
|
+
await page.evaluate(() => {
|
|
791
|
+
const HEADING_SELECTOR = '.slide-host h1, .slide-host h2, .slide-host h3, .slide-host .display, .slide-host .title, .slide-host .slide-title';
|
|
792
|
+
const INLINE_TAGS = new Set(['SPAN', 'B', 'STRONG', 'I', 'EM', 'U', 'BR']);
|
|
793
|
+
const LINE_GROUP_EPSILON_PX = 2;
|
|
794
|
+
const WIDTH_BUFFER_PX = 16;
|
|
795
|
+
const HEIGHT_BUFFER_PX = 4;
|
|
796
|
+
|
|
797
|
+
function applyTextTransform(text, transform) {
|
|
798
|
+
if (transform === 'uppercase') return text.toUpperCase();
|
|
799
|
+
if (transform === 'lowercase') return text.toLowerCase();
|
|
800
|
+
if (transform === 'capitalize') return text.replace(/\b\w/g, c => c.toUpperCase());
|
|
801
|
+
return text;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function snapshotTextStyle(el) {
|
|
805
|
+
const cs = window.getComputedStyle(el);
|
|
806
|
+
return {
|
|
807
|
+
color: cs.color,
|
|
808
|
+
fontFamily: cs.fontFamily,
|
|
809
|
+
fontSize: cs.fontSize,
|
|
810
|
+
fontWeight: cs.fontWeight,
|
|
811
|
+
fontStyle: cs.fontStyle,
|
|
812
|
+
letterSpacing: cs.letterSpacing,
|
|
813
|
+
lineHeight: cs.lineHeight,
|
|
814
|
+
textDecorationLine: cs.textDecorationLine,
|
|
815
|
+
textDecorationStyle: cs.textDecorationStyle,
|
|
816
|
+
textDecorationColor: cs.textDecorationColor,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function sameTextStyle(a, b) {
|
|
821
|
+
return a.color === b.color &&
|
|
822
|
+
a.fontFamily === b.fontFamily &&
|
|
823
|
+
a.fontSize === b.fontSize &&
|
|
824
|
+
a.fontWeight === b.fontWeight &&
|
|
825
|
+
a.fontStyle === b.fontStyle &&
|
|
826
|
+
a.letterSpacing === b.letterSpacing &&
|
|
827
|
+
a.lineHeight === b.lineHeight &&
|
|
828
|
+
a.textDecorationLine === b.textDecorationLine &&
|
|
829
|
+
a.textDecorationStyle === b.textDecorationStyle &&
|
|
830
|
+
a.textDecorationColor === b.textDecorationColor;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function applyTextStyle(target, style) {
|
|
834
|
+
target.style.color = style.color;
|
|
835
|
+
target.style.fontFamily = style.fontFamily;
|
|
836
|
+
target.style.fontSize = style.fontSize;
|
|
837
|
+
target.style.fontWeight = style.fontWeight;
|
|
838
|
+
target.style.fontStyle = style.fontStyle;
|
|
839
|
+
target.style.letterSpacing = style.letterSpacing;
|
|
840
|
+
target.style.lineHeight = style.lineHeight;
|
|
841
|
+
target.style.textTransform = 'none';
|
|
842
|
+
if (style.textDecorationLine && style.textDecorationLine !== 'none') {
|
|
843
|
+
target.style.textDecorationLine = style.textDecorationLine;
|
|
844
|
+
target.style.textDecorationStyle = style.textDecorationStyle;
|
|
845
|
+
target.style.textDecorationColor = style.textDecorationColor;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function getOrCreateLine(lines, rect) {
|
|
850
|
+
const midY = rect.top + rect.height / 2;
|
|
851
|
+
let line = lines.find(item =>
|
|
852
|
+
midY >= item.top - LINE_GROUP_EPSILON_PX &&
|
|
853
|
+
midY <= item.bottom + LINE_GROUP_EPSILON_PX
|
|
854
|
+
);
|
|
855
|
+
if (line) return line;
|
|
856
|
+
|
|
857
|
+
line = {
|
|
858
|
+
top: rect.top,
|
|
859
|
+
bottom: rect.top + rect.height,
|
|
860
|
+
left: rect.left,
|
|
861
|
+
right: rect.right,
|
|
862
|
+
fragments: [],
|
|
863
|
+
};
|
|
864
|
+
lines.push(line);
|
|
865
|
+
return line;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function collectRenderedLines(el) {
|
|
869
|
+
const lines = [];
|
|
870
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
871
|
+
let order = 0;
|
|
872
|
+
|
|
873
|
+
while (walker.nextNode()) {
|
|
874
|
+
const textNode = walker.currentNode;
|
|
875
|
+
const rawText = textNode.nodeValue || '';
|
|
876
|
+
if (!rawText.length) continue;
|
|
877
|
+
|
|
878
|
+
const styleSource = textNode.parentElement || el;
|
|
879
|
+
const style = snapshotTextStyle(styleSource);
|
|
880
|
+
const displayText = applyTextTransform(rawText, window.getComputedStyle(styleSource).textTransform);
|
|
881
|
+
|
|
882
|
+
for (let i = 0; i < rawText.length; i++) {
|
|
883
|
+
const range = document.createRange();
|
|
884
|
+
range.setStart(textNode, i);
|
|
885
|
+
range.setEnd(textNode, i + 1);
|
|
886
|
+
const rect = Array.from(range.getClientRects()).find(r => r.width > 0.1 && r.height > 0.1);
|
|
887
|
+
if (!rect) continue;
|
|
888
|
+
|
|
889
|
+
const line = getOrCreateLine(lines, rect);
|
|
890
|
+
line.top = Math.min(line.top, rect.top);
|
|
891
|
+
line.bottom = Math.max(line.bottom, rect.top + rect.height);
|
|
892
|
+
line.left = Math.min(line.left, rect.left);
|
|
893
|
+
line.right = Math.max(line.right, rect.right);
|
|
894
|
+
line.fragments.push({
|
|
895
|
+
text: displayText.slice(i, i + 1),
|
|
896
|
+
style,
|
|
897
|
+
order,
|
|
898
|
+
});
|
|
899
|
+
order += 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
lines.sort((a, b) => a.top - b.top || a.left - b.left);
|
|
904
|
+
|
|
905
|
+
return lines.map(line => {
|
|
906
|
+
const merged = [];
|
|
907
|
+
line.fragments.sort((a, b) => a.order - b.order);
|
|
908
|
+
|
|
909
|
+
for (const fragment of line.fragments) {
|
|
910
|
+
const last = merged[merged.length - 1];
|
|
911
|
+
if (last && sameTextStyle(last.style, fragment.style) && fragment.order === last.orderEnd + 1) {
|
|
912
|
+
last.text += fragment.text;
|
|
913
|
+
last.orderEnd = fragment.order;
|
|
914
|
+
} else {
|
|
915
|
+
merged.push({ ...fragment, orderEnd: fragment.order });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
top: line.top,
|
|
921
|
+
left: line.left,
|
|
922
|
+
width: Math.max(0, line.right - line.left),
|
|
923
|
+
height: Math.max(0, line.bottom - line.top),
|
|
924
|
+
fragments: merged
|
|
925
|
+
.map(({ orderEnd, ...fragment }) => fragment)
|
|
926
|
+
.filter(fragment => fragment.text.length > 0),
|
|
927
|
+
};
|
|
928
|
+
}).filter(line =>
|
|
929
|
+
line.width > 0.5 &&
|
|
930
|
+
line.height > 0.5 &&
|
|
931
|
+
line.fragments.some(fragment => fragment.text.trim().length > 0)
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
for (const el of document.querySelectorAll(HEADING_SELECTOR)) {
|
|
936
|
+
const cs = window.getComputedStyle(el);
|
|
937
|
+
const fontSize = parseFloat(cs.fontSize) || 0;
|
|
938
|
+
if (fontSize < 24) continue;
|
|
939
|
+
|
|
940
|
+
const hasInlineChildren = Array.from(el.children).some(child => INLINE_TAGS.has(child.tagName));
|
|
941
|
+
if (!hasInlineChildren) continue;
|
|
942
|
+
|
|
943
|
+
const lines = collectRenderedLines(el);
|
|
944
|
+
if (lines.length < 2) continue;
|
|
945
|
+
|
|
946
|
+
const parent = el.parentElement;
|
|
947
|
+
if (!parent) continue;
|
|
948
|
+
if (window.getComputedStyle(parent).position === 'static') {
|
|
949
|
+
parent.style.position = 'relative';
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const parentRect = parent.getBoundingClientRect();
|
|
953
|
+
const baseStyle = snapshotTextStyle(el);
|
|
954
|
+
|
|
955
|
+
for (const line of lines) {
|
|
956
|
+
const lineNode = document.createElement('div');
|
|
957
|
+
lineNode.setAttribute('data-pptx-preserved-line', '1');
|
|
958
|
+
applyTextStyle(lineNode, baseStyle);
|
|
959
|
+
lineNode.style.position = 'absolute';
|
|
960
|
+
lineNode.style.left = (line.left - parentRect.left) + 'px';
|
|
961
|
+
lineNode.style.top = (line.top - parentRect.top) + 'px';
|
|
962
|
+
lineNode.style.width = (line.width + WIDTH_BUFFER_PX) + 'px';
|
|
963
|
+
lineNode.style.height = (line.height + HEIGHT_BUFFER_PX) + 'px';
|
|
964
|
+
lineNode.style.margin = '0';
|
|
965
|
+
lineNode.style.padding = '0';
|
|
966
|
+
lineNode.style.border = '0';
|
|
967
|
+
lineNode.style.background = 'none';
|
|
968
|
+
lineNode.style.overflow = 'visible';
|
|
969
|
+
lineNode.style.whiteSpace = 'pre';
|
|
970
|
+
lineNode.style.pointerEvents = 'none';
|
|
971
|
+
lineNode.style.textAlign = cs.textAlign;
|
|
972
|
+
lineNode.style.zIndex = cs.zIndex !== 'auto' ? cs.zIndex : '0';
|
|
973
|
+
|
|
974
|
+
for (const fragment of line.fragments) {
|
|
975
|
+
if (!fragment.text) continue;
|
|
976
|
+
if (sameTextStyle(fragment.style, baseStyle)) {
|
|
977
|
+
lineNode.appendChild(document.createTextNode(fragment.text));
|
|
978
|
+
} else {
|
|
979
|
+
const span = document.createElement('span');
|
|
980
|
+
applyTextStyle(span, fragment.style);
|
|
981
|
+
span.textContent = fragment.text;
|
|
982
|
+
lineNode.appendChild(span);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
parent.appendChild(lineNode);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
el.style.visibility = 'hidden';
|
|
990
|
+
el.setAttribute('data-pptx-preserve-lines-source', '1');
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// Materialise CSS `filter` and `opacity` on <img> elements so that
|
|
995
|
+
// dom-to-pptx (which uses getComputedStyle + getBoundingClientRect but
|
|
996
|
+
// does NOT translate CSS filter/opacity into PPTX image effects) receives
|
|
997
|
+
// a pre-processed image with the visual appearance already baked in.
|
|
998
|
+
//
|
|
999
|
+
// For each <img> that has a non-trivial filter or opacity:
|
|
1000
|
+
// 1. Draw the image to a canvas with ctx.filter + ctx.globalAlpha so
|
|
1001
|
+
// both effects are baked into the PNG pixel data (alpha channel).
|
|
1002
|
+
// 2. Replace img.src with the canvas data-URL.
|
|
1003
|
+
// 3. Reset the CSS filter and opacity to neutral so dom-to-pptx sees
|
|
1004
|
+
// a plain image and does not double-apply any effect.
|
|
1005
|
+
await page.evaluate(() => {
|
|
1006
|
+
/**
|
|
1007
|
+
* Walk up the DOM from `el` and multiply together all `opacity`
|
|
1008
|
+
* values set on ancestors (including el itself), stopping at the
|
|
1009
|
+
* document root. This gives the effective visual opacity.
|
|
1010
|
+
*/
|
|
1011
|
+
function effectiveOpacity(el) {
|
|
1012
|
+
let opacity = 1;
|
|
1013
|
+
let node = el;
|
|
1014
|
+
while (node && node !== document.documentElement) {
|
|
1015
|
+
const o = parseFloat(window.getComputedStyle(node).opacity);
|
|
1016
|
+
if (!isNaN(o)) opacity *= o;
|
|
1017
|
+
node = node.parentElement;
|
|
1018
|
+
}
|
|
1019
|
+
return opacity;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Bake CSS filter + opacity into a canvas and return a data URL.
|
|
1024
|
+
* The canvas alpha channel carries the opacity so no separate PPTX
|
|
1025
|
+
* transparency attribute is needed.
|
|
1026
|
+
*/
|
|
1027
|
+
function bakeImage(img, filterValue, opacity) {
|
|
1028
|
+
const w = img.naturalWidth || img.width || 1;
|
|
1029
|
+
const h = img.naturalHeight || img.height || 1;
|
|
1030
|
+
const canvas = document.createElement('canvas');
|
|
1031
|
+
canvas.width = w;
|
|
1032
|
+
canvas.height = h;
|
|
1033
|
+
const ctx = canvas.getContext('2d');
|
|
1034
|
+
if (filterValue !== 'none') ctx.filter = filterValue;
|
|
1035
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, opacity));
|
|
1036
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
1037
|
+
return canvas.toDataURL('image/png');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const img of document.querySelectorAll('img')) {
|
|
1041
|
+
const cs = window.getComputedStyle(img);
|
|
1042
|
+
const filter = cs.filter || cs.webkitFilter || 'none';
|
|
1043
|
+
const opacity = effectiveOpacity(img);
|
|
1044
|
+
|
|
1045
|
+
if (filter === 'none' && opacity >= 1) continue;
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
img.src = bakeImage(img, filter, opacity);
|
|
1049
|
+
img.style.filter = 'none';
|
|
1050
|
+
img.style.opacity = '1';
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
// tainted canvas (cross-origin image) — skip
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Bake rgba text colours to solid (PPTX text boxes have no alpha-channel
|
|
1058
|
+
// colour support) and freeze non-default flex layouts to explicit absolute
|
|
1059
|
+
// coordinates so dom-to-pptx gets concrete positions rather than having
|
|
1060
|
+
// to re-implement justify-content / align-items logic.
|
|
1061
|
+
await page.evaluate(() => {
|
|
1062
|
+
// ── helpers ───────────────────────────────────────────────────────
|
|
1063
|
+
function parseRgba(str) {
|
|
1064
|
+
const m = str.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
1065
|
+
return m ? { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 } : null;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Walk up the DOM to find the first ancestor with a non-transparent bg.
|
|
1069
|
+
function effectiveBg(el) {
|
|
1070
|
+
for (let n = el.parentElement; n && n !== document.documentElement; n = n.parentElement) {
|
|
1071
|
+
const bg = parseRgba(window.getComputedStyle(n).backgroundColor);
|
|
1072
|
+
if (bg && bg.a > 0.01) return bg;
|
|
1073
|
+
}
|
|
1074
|
+
return { r: 0, g: 0, b: 0, a: 1 }; // fall back to black
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ── 1. Solid-ify semi-transparent text colours ────────────────────
|
|
1078
|
+
for (const el of document.querySelectorAll('*')) {
|
|
1079
|
+
const cs = window.getComputedStyle(el);
|
|
1080
|
+
const fg = parseRgba(cs.color);
|
|
1081
|
+
if (!fg || fg.a >= 1) continue;
|
|
1082
|
+
const bg = effectiveBg(el);
|
|
1083
|
+
el.style.color = `rgb(${
|
|
1084
|
+
Math.round(bg.r * (1 - fg.a) + fg.r * fg.a)},${
|
|
1085
|
+
Math.round(bg.g * (1 - fg.a) + fg.g * fg.a)},${
|
|
1086
|
+
Math.round(bg.b * (1 - fg.a) + fg.b * fg.a)})`;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ── 2. Re-parent space-between/around/evenly flex children ──────────
|
|
1090
|
+
// dom-to-pptx treats the flex container as one text box and collapses
|
|
1091
|
+
// all children into it, ignoring individual child positions. The only
|
|
1092
|
+
// reliable fix is to move each child OUT of the container and attach it
|
|
1093
|
+
// directly to the nearest positioned ancestor (the slide root), with
|
|
1094
|
+
// explicit absolute coordinates derived from the browser's own layout.
|
|
1095
|
+
//
|
|
1096
|
+
// Two-phase: collect ALL position snapshots first (zero DOM mutations),
|
|
1097
|
+
// then re-parent in a second pass so earlier moves don't corrupt later
|
|
1098
|
+
// getBoundingClientRect reads.
|
|
1099
|
+
const DISTRIBUTION_JUSTIFY = new Set(['space-between', 'space-around', 'space-evenly']);
|
|
1100
|
+
|
|
1101
|
+
// Phase 1: snapshot — zero DOM mutations.
|
|
1102
|
+
const freezeOps = [];
|
|
1103
|
+
for (const container of document.querySelectorAll('*')) {
|
|
1104
|
+
const cs = window.getComputedStyle(container);
|
|
1105
|
+
if (cs.display !== 'flex' && cs.display !== 'inline-flex') continue;
|
|
1106
|
+
if (!DISTRIBUTION_JUSTIFY.has(cs.justifyContent)) continue;
|
|
1107
|
+
|
|
1108
|
+
// Walk up to find the nearest positioned ancestor — this becomes
|
|
1109
|
+
// the new containing block for the re-parented children.
|
|
1110
|
+
let newParent = container.parentElement;
|
|
1111
|
+
while (newParent && newParent !== document.body) {
|
|
1112
|
+
if (window.getComputedStyle(newParent).position !== 'static') break;
|
|
1113
|
+
newParent = newParent.parentElement;
|
|
1114
|
+
}
|
|
1115
|
+
if (!newParent) continue;
|
|
1116
|
+
|
|
1117
|
+
const containerRect = container.getBoundingClientRect();
|
|
1118
|
+
const parentRect = newParent.getBoundingClientRect();
|
|
1119
|
+
const children = Array.from(container.children);
|
|
1120
|
+
const childRects = children.map(c => c.getBoundingClientRect());
|
|
1121
|
+
|
|
1122
|
+
freezeOps.push({ container, containerRect, newParent, parentRect, children, childRects });
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Phase 2: re-parent — all positions already captured.
|
|
1126
|
+
for (const { container, containerRect, newParent, parentRect, children, childRects } of freezeOps) {
|
|
1127
|
+
// Keep the container's original slot in normal flow. Without this,
|
|
1128
|
+
// moving its children out can make the empty flex box collapse and
|
|
1129
|
+
// pull later sections (like the main diagram) upward.
|
|
1130
|
+
container.style.width = containerRect.width + 'px';
|
|
1131
|
+
container.style.height = containerRect.height + 'px';
|
|
1132
|
+
container.style.minWidth = containerRect.width + 'px';
|
|
1133
|
+
container.style.minHeight = containerRect.height + 'px';
|
|
1134
|
+
container.style.maxWidth = containerRect.width + 'px';
|
|
1135
|
+
container.style.maxHeight = containerRect.height + 'px';
|
|
1136
|
+
container.style.flex = '0 0 auto';
|
|
1137
|
+
|
|
1138
|
+
children.forEach((child, i) => {
|
|
1139
|
+
const r = childRects[i];
|
|
1140
|
+
child.style.position = 'absolute';
|
|
1141
|
+
child.style.left = (r.left - parentRect.left) + 'px';
|
|
1142
|
+
child.style.top = (r.top - parentRect.top) + 'px';
|
|
1143
|
+
child.style.width = r.width + 'px';
|
|
1144
|
+
newParent.appendChild(child); // lifts child out of the flex container
|
|
1145
|
+
});
|
|
1146
|
+
// Hide the now-empty container so dom-to-pptx skips it.
|
|
1147
|
+
container.style.visibility = 'hidden';
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const [download] = await Promise.all([
|
|
1152
|
+
page.waitForEvent('download', { timeout: EXPORT_TIMEOUT_MS }),
|
|
1153
|
+
page.evaluate(({ fileName, fonts }) => {
|
|
1154
|
+
const hosts = Array.from(document.querySelectorAll('.slide-host'));
|
|
1155
|
+
return window.domToPptx.exportToPptx(hosts, {
|
|
1156
|
+
fileName,
|
|
1157
|
+
// svgAsVector: false — rasterise SVGs; the vector path (v1.1.5) can
|
|
1158
|
+
// produce malformed XML that causes PowerPoint's "Repair" dialog.
|
|
1159
|
+
svgAsVector: false,
|
|
1160
|
+
// autoEmbedFonts: false — we supply pre-downloaded TTF fonts via the
|
|
1161
|
+
// `fonts` option instead. Each data: URI is fetched by the browser
|
|
1162
|
+
// without CORS issues, and TTF is the format PowerPoint can embed.
|
|
1163
|
+
autoEmbedFonts: false,
|
|
1164
|
+
fonts,
|
|
1165
|
+
});
|
|
1166
|
+
}, { fileName: path.basename(outputPath), fonts: embeddedFonts }),
|
|
1167
|
+
]);
|
|
1168
|
+
|
|
1169
|
+
await download.saveAs(outputPath);
|
|
1170
|
+
console.log(`Saved: ${outputPath}`);
|
|
1171
|
+
console.log(`Converted ${htmlFiles.length} slide(s)`);
|
|
1172
|
+
|
|
1173
|
+
await context.close();
|
|
1174
|
+
} finally {
|
|
1175
|
+
await browser.close();
|
|
1176
|
+
try { fs.unlinkSync(orchFile); } catch (_) {}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
main().catch(err => {
|
|
1181
|
+
console.error(err.message || String(err));
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
});
|