flea-qml 0.0.0-stage → 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 +22 -0
- package/README.md +478 -2
- package/THIRD-PARTY-NOTICES.md +29 -0
- package/demos/App.java +5 -0
- package/demos/Dockerfile +4 -0
- package/demos/Main.kt +3 -0
- package/demos/Program.cs +1 -0
- package/demos/README.md +12 -0
- package/demos/app.dart +3 -0
- package/demos/app.deb +0 -0
- package/demos/app.js +1 -0
- package/demos/backup.tar +0 -0
- package/demos/budget.xlsx +0 -0
- package/demos/build.sh +2 -0
- package/demos/bundle.zip +0 -0
- package/demos/clip.mp4 +0 -0
- package/demos/data.sqlite +0 -0
- package/demos/disk.iso +0 -0
- package/demos/engine.cpp +5 -0
- package/demos/firmware.bin +0 -0
- package/demos/flake.nix +4 -0
- package/demos/folder/inside.txt +1 -0
- package/demos/font.ttf +0 -0
- package/demos/index.html +5 -0
- package/demos/index.php +2 -0
- package/demos/init.lua +1 -0
- package/demos/letter.docx +0 -0
- package/demos/lib.rs +3 -0
- package/demos/main.c +6 -0
- package/demos/main.h +3 -0
- package/demos/math-and-diagrams.md +200 -0
- package/demos/notes.tar.gz +0 -0
- package/demos/notes.txt +3 -0
- package/demos/photo.png +0 -0
- package/demos/program +2 -0
- package/demos/report.pdf +32 -0
- package/demos/script.py +1 -0
- package/demos/server.go +7 -0
- package/demos/settings.json +4 -0
- package/demos/slides.pptx +0 -0
- package/demos/song.mp3 +0 -0
- package/demos/style.css +4 -0
- package/demos/tool.rb +1 -0
- package/demos/types.ts +2 -0
- package/docs/pele.md +23 -0
- package/docs/protocol.md +3420 -0
- package/docs/temml.md +63 -0
- package/index.js +107 -0
- package/package.json +34 -4
- package/src/archive.js +402 -0
- package/src/archiveops.js +697 -0
- package/src/backend.js +369 -0
- package/src/bulkrename.js +95 -0
- package/src/clip-host.js +76 -0
- package/src/clip.js +462 -0
- package/src/code.js +168 -0
- package/src/compare.js +74 -0
- package/src/create.js +128 -0
- package/src/dirsize.js +147 -0
- package/src/figure-svg.js +530 -0
- package/src/figure-thread.js +23 -0
- package/src/figure.js +132 -0
- package/src/find.js +55 -0
- package/src/flea.js +39 -0
- package/src/fsinfo.js +39 -0
- package/src/highlight.js +141 -0
- package/src/html.js +52 -0
- package/src/kinds.js +89 -0
- package/src/listing.js +130 -0
- package/src/markdown-html.js +317 -0
- package/src/mathml.js +186 -0
- package/src/menu.js +180 -0
- package/src/meta.js +129 -0
- package/src/office.js +810 -0
- package/src/order.js +100 -0
- package/src/peek.js +49 -0
- package/src/pele-wrapper.js +695 -0
- package/src/permissions.js +180 -0
- package/src/places.js +313 -0
- package/src/preview-language.js +10 -0
- package/src/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +326 -0
- package/src/uistate.js +393 -0
- package/src/uistore.js +168 -0
- package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
- package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
- package/src/vendor/LICENSES/MIT-katex.txt +21 -0
- package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
- package/src/vendor/LICENSES/MIT-pele.txt +40 -0
- package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
- package/src/vendor/LICENSES/MIT-temml.txt +21 -0
- package/src/vendor/pele.js +145 -0
- package/src/vendor/temml.mjs +14708 -0
- package/src/watch.js +98 -0
- package/src/wayland/client.js +109 -0
- package/src/wayland/protocol.js +55 -0
- package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
- package/src/wayland/protocols/wayland.xml +3248 -0
- package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
- package/src/wayland/resource-io.js +6 -0
- package/src/wayland/socket-discovery.js +26 -0
- package/src/wayland/transport-posix.js +120 -0
- package/src/wayland/transport-win32.js +11 -0
- package/src/wayland/transport.js +3 -0
- package/src/wayland/win32-resources.js +80 -0
- package/src/wayland/win32-wire.js +62 -0
- package/test/archive.test.js +80 -0
- package/test/bench-markdown-scroll.mjs +190 -0
- package/test/clip-host.test.js +113 -0
- package/test/code-preview.test.js +35 -0
- package/test/compare-ui-paths.test.js +24 -0
- package/test/compare.test.js +54 -0
- package/test/delete-trash.test.js +67 -0
- package/test/dirsize.test.js +138 -0
- package/test/file-ops.test.js +241 -0
- package/test/find-contents.test.js +161 -0
- package/test/find-input.test.js +66 -0
- package/test/fixtures/pele-parity.json +1006 -0
- package/test/highlight-launcher.test.js +35 -0
- package/test/html-preview.test.js +22 -0
- package/test/input.test.js +15 -0
- package/test/keymap.test.js +26 -0
- package/test/language-icons.test.js +36 -0
- package/test/launcher.test.js +51 -0
- package/test/manual-code-scroll.mjs +28 -0
- package/test/manual-column-scroll.mjs +24 -0
- package/test/manual-compare-ui.mjs +20 -0
- package/test/manual-find-status.mjs +37 -0
- package/test/manual-html-security.mjs +23 -0
- package/test/manual-html-ui.mjs +30 -0
- package/test/manual-syntax-preview.mjs +26 -0
- package/test/manual-trash-ui.mjs +86 -0
- package/test/markdown-diagrams.test.js +93 -0
- package/test/markdown-html.test.js +105 -0
- package/test/mathml.test.js +92 -0
- package/test/menu-transfer.test.js +57 -0
- package/test/office-preview.test.js +154 -0
- package/test/pele-parity.test.js +28 -0
- package/test/places.test.js +134 -0
- package/test/preview-keys.test.js +60 -0
- package/test/preview-language.test.js +22 -0
- package/test/status-filename.test.js +20 -0
- package/test/transfer-replace-merge.test.js +187 -0
- package/test/trash-address.test.js +23 -0
- package/test/trash-view.test.js +37 -0
- package/test/trashbrowse.test.js +135 -0
- package/ui/AboutFacts.qml +78 -0
- package/ui/ArchiveCards.qml +68 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +771 -0
- package/ui/BulkOldNames.qml +47 -0
- package/ui/BulkRenameDialog.qml +380 -0
- package/ui/CardFrame.qml +67 -0
- package/ui/CardScroll.qml +79 -0
- package/ui/CheckBox.qml +73 -0
- package/ui/ChromeBar.qml +360 -0
- package/ui/ChromeButton.qml +131 -0
- package/ui/ChromeModes.qml +43 -0
- package/ui/CodeLines.qml +256 -0
- package/ui/CodeRefusal.qml +52 -0
- package/ui/CollideConfirm.qml +147 -0
- package/ui/CollideHost.qml +167 -0
- package/ui/ColumnPane.qml +523 -0
- package/ui/ColumnRow.qml +380 -0
- package/ui/ColumnsArea.qml +594 -0
- package/ui/ColumnsLanding.qml +147 -0
- package/ui/CompareView.qml +321 -0
- package/ui/ContextMenu.qml +743 -0
- package/ui/ConvertDialog.qml +456 -0
- package/ui/CountedWorker.qml +7 -0
- package/ui/Crumb.qml +64 -0
- package/ui/CrumbDrop.qml +25 -0
- package/ui/CsvTable.qml +394 -0
- package/ui/CursorMark.qml +41 -0
- package/ui/DefaultClaim.qml +130 -0
- package/ui/DeviceLock.qml +47 -0
- package/ui/DeviceMounts.qml +540 -0
- package/ui/DialogButton.qml +150 -0
- package/ui/DialogField.qml +147 -0
- package/ui/DialogHeader.qml +31 -0
- package/ui/DialogScrim.qml +20 -0
- package/ui/DialogTitle.qml +37 -0
- package/ui/Divider.qml +9 -0
- package/ui/DropInto.qml +126 -0
- package/ui/DropTarget.qml +35 -0
- package/ui/DropboxMark.qml +60 -0
- package/ui/EmptyState.qml +150 -0
- package/ui/EscapeHint.qml +11 -0
- package/ui/FactsTable.qml +54 -0
- package/ui/FastScrollHandler.qml +476 -0
- package/ui/FatNamesConfirm.qml +157 -0
- package/ui/Favourites.qml +143 -0
- package/ui/FigureService.qml +120 -0
- package/ui/FileDrag.qml +267 -0
- package/ui/FilterStrip.qml +133 -0
- package/ui/FitMetrics.qml +22 -0
- package/ui/FlashDriveRows.qml +137 -0
- package/ui/FlashHost.qml +72 -0
- package/ui/FlashImageRow.qml +104 -0
- package/ui/FlashRunner.qml +119 -0
- package/ui/FlashStepRow.qml +172 -0
- package/ui/FlashStrip.qml +38 -0
- package/ui/FlashView.qml +397 -0
- package/ui/FleaMark.qml +88 -0
- package/ui/FlyoutFade.qml +23 -0
- package/ui/FontPane.qml +170 -0
- package/ui/Glass.qml +114 -0
- package/ui/GlassTagger.qml +59 -0
- package/ui/Glyph.qml +64 -0
- package/ui/GlyphSolid.qml +22 -0
- package/ui/GridArea.qml +664 -0
- package/ui/GridTile.qml +325 -0
- package/ui/GvfsBridge.qml +164 -0
- package/ui/Header.qml +433 -0
- package/ui/HoverTimers.qml +39 -0
- package/ui/HtmlClip.qml +30 -0
- package/ui/HtmlPane.qml +63 -0
- package/ui/HyprlandMark.qml +45 -0
- package/ui/Ipc.qml +1005 -0
- package/ui/IpcControls.qml +7 -0
- package/ui/IpcMenuState.qml +48 -0
- package/ui/IpcPreviewState.qml +185 -0
- package/ui/JumpPath.qml +51 -0
- package/ui/KeymapSheet.qml +561 -0
- package/ui/KeymapSheetResult.qml +89 -0
- package/ui/KindStack.qml +40 -0
- package/ui/LangMark.qml +51 -0
- package/ui/LangMarkLoader.qml +26 -0
- package/ui/List.qml +480 -0
- package/ui/LoadingState.qml +54 -0
- package/ui/LocalSend.qml +56 -0
- package/ui/LocalSendMark.qml +73 -0
- package/ui/MarkdownBlockView.qml +311 -0
- package/ui/MarkdownBlocks.qml +35 -0
- package/ui/MarkdownFigure.qml +254 -0
- package/ui/MarkdownImages.qml +72 -0
- package/ui/MarkdownList.qml +81 -0
- package/ui/MarkdownMathsText.qml +97 -0
- package/ui/MarkdownPane.qml +83 -0
- package/ui/MarkdownPictures.qml +41 -0
- package/ui/MarkdownQuote.qml +52 -0
- package/ui/MarkdownRemote.qml +82 -0
- package/ui/MarkdownSource.qml +155 -0
- package/ui/MarkdownTable.qml +222 -0
- package/ui/MarkdownTableScroll.qml +64 -0
- package/ui/MarkdownTableWheel.qml +126 -0
- package/ui/MarkdownText.qml +87 -0
- package/ui/MarkdownWorker.js +132 -0
- package/ui/MatchText.qml +99 -0
- package/ui/MediaSession.qml +38 -0
- package/ui/MediaSound.qml +26 -0
- package/ui/MediaStrip.qml +191 -0
- package/ui/MediaTrack.qml +36 -0
- package/ui/MediaVolume.qml +54 -0
- package/ui/MenuActionDialog.qml +396 -0
- package/ui/MenuEdgeFade.qml +24 -0
- package/ui/MenuHost.qml +227 -0
- package/ui/MenuRow.qml +351 -0
- package/ui/MountListing.qml +100 -0
- package/ui/NetworkDialog.qml +435 -0
- package/ui/NetworkForm.qml +426 -0
- package/ui/NetworkMounts.qml +913 -0
- package/ui/NetworkPlaces.qml +127 -0
- package/ui/OpenWithDialog.qml +613 -0
- package/ui/Opener.qml +115 -0
- package/ui/Pane.qml +1296 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +239 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +605 -0
- package/ui/ParkedWait.qml +9 -0
- package/ui/PathEntry.qml +186 -0
- package/ui/PathJump.qml +400 -0
- package/ui/PdfViewer.qml +297 -0
- package/ui/PermissionsDialog.qml +658 -0
- package/ui/PermissionsHeadings.qml +31 -0
- package/ui/PhoneMounts.qml +67 -0
- package/ui/PickerAnswers.qml +75 -0
- package/ui/PickerBox.qml +26 -0
- package/ui/PickerChrome.qml +295 -0
- package/ui/PickerFilter.qml +97 -0
- package/ui/PickerGrid.qml +308 -0
- package/ui/PickerHeader.qml +28 -0
- package/ui/PickerLifecycle.qml +27 -0
- package/ui/PickerList.qml +218 -0
- package/ui/PickerListing.qml +149 -0
- package/ui/PickerPathBar.qml +127 -0
- package/ui/PickerPlaces.qml +189 -0
- package/ui/PickerRecent.qml +92 -0
- package/ui/PickerSave.qml +101 -0
- package/ui/PickerSelection.qml +155 -0
- package/ui/PickerWindow.qml +846 -0
- package/ui/PowerSectorsReader.qml +10 -0
- package/ui/PrebuildDriver.qml +146 -0
- package/ui/Preview.qml +1004 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +66 -0
- package/ui/PreviewColumn.qml +551 -0
- package/ui/PreviewEdge.qml +63 -0
- package/ui/PreviewFacts.qml +30 -0
- package/ui/PreviewFactsLine.qml +34 -0
- package/ui/PreviewHtml.qml +233 -0
- package/ui/PreviewImage.qml +253 -0
- package/ui/PreviewImagePan.qml +13 -0
- package/ui/PreviewImageSharp.qml +32 -0
- package/ui/PreviewLines.qml +106 -0
- package/ui/PreviewMarkdown.qml +706 -0
- package/ui/PreviewMedia.qml +250 -0
- package/ui/PreviewPdf.qml +482 -0
- package/ui/PreviewScroll.qml +201 -0
- package/ui/PreviewSwap.qml +335 -0
- package/ui/PreviewText.qml +149 -0
- package/ui/ProtocolChip.qml +18 -0
- package/ui/QuickLookArchive.qml +38 -0
- package/ui/QuickLookBar.qml +103 -0
- package/ui/QuickLookPrepare.qml +355 -0
- package/ui/QuickLookSwap.qml +10 -0
- package/ui/QuickLookWarm.qml +65 -0
- package/ui/RailDrop.qml +155 -0
- package/ui/RailHeading.qml +19 -0
- package/ui/RailPlaces.qml +38 -0
- package/ui/RailTrashRow.qml +67 -0
- package/ui/RenameField.qml +353 -0
- package/ui/RendererRetry.qml +11 -0
- package/ui/ResizeHandle.qml +52 -0
- package/ui/Reveal.qml +54 -0
- package/ui/Row.qml +476 -0
- package/ui/RowDrag.qml +213 -0
- package/ui/RowDropTarget.qml +27 -0
- package/ui/RunCard.qml +286 -0
- package/ui/Scripts.qml +65 -0
- package/ui/SearchStrip.qml +232 -0
- package/ui/SelectionBand.qml +140 -0
- package/ui/SelectionPreview.qml +370 -0
- package/ui/SettingsFavourite.qml +122 -0
- package/ui/SettingsGroup.qml +96 -0
- package/ui/SettingsHint.qml +12 -0
- package/ui/SettingsMetrics.qml +29 -0
- package/ui/SettingsPane.qml +178 -0
- package/ui/SettingsPanel.qml +547 -0
- package/ui/SettingsRail.qml +128 -0
- package/ui/SettingsRow.qml +409 -0
- package/ui/SettingsRuler.qml +85 -0
- package/ui/SettingsSegment.qml +85 -0
- package/ui/ShareBrowser.qml +113 -0
- package/ui/ShareLink.qml +67 -0
- package/ui/ShelfPins.qml +101 -0
- package/ui/Sidebar.qml +732 -0
- package/ui/SidebarRow.qml +358 -0
- package/ui/Spinner.qml +60 -0
- package/ui/StableModel.qml +89 -0
- package/ui/StateMessage.qml +84 -0
- package/ui/StatusBar.qml +512 -0
- package/ui/StatusFileName.qml +27 -0
- package/ui/TabBar.qml +673 -0
- package/ui/TabDragGeometry.qml +60 -0
- package/ui/TagMenuMark.qml +52 -0
- package/ui/TagRailSection.qml +53 -0
- package/ui/TagSlot.qml +40 -0
- package/ui/TagState.qml +150 -0
- package/ui/TagSwatch.qml +28 -0
- package/ui/Taildrop.qml +86 -0
- package/ui/TailscaleMark.qml +50 -0
- package/ui/Theme.qml +460 -0
- package/ui/ThemeColors.qml +43 -0
- package/ui/TransferCard.qml +239 -0
- package/ui/TrashConfirm.qml +129 -0
- package/ui/TrashDropHandler.qml +11 -0
- package/ui/TrashHost.qml +202 -0
- package/ui/TrashMonitor.qml +30 -0
- package/ui/TrashToolbar.qml +19 -0
- package/ui/TrashView.qml +492 -0
- package/ui/UnlockDialog.qml +450 -0
- package/ui/UnlockHost.qml +206 -0
- package/ui/UpdateCheck.qml +97 -0
- package/ui/UserDirs.qml +25 -0
- package/ui/ViewState.qml +565 -0
- package/ui/ViewportScrollBar.qml +195 -0
- package/ui/ViewportScrollBars.qml +25 -0
- package/ui/WebPreview.qml +33 -0
- package/ui/WebPreviewView.qml +26 -0
- package/ui/WindowBody.qml +827 -0
- package/ui/WindowDrag.qml +35 -0
- package/ui/boot/Commons/AudioNodes.qml +50 -0
- package/ui/boot/Commons/AudioNodesModel.js +96 -0
- package/ui/boot/Commons/Border.qml +243 -0
- package/ui/boot/Commons/BorderGeometry.js +373 -0
- package/ui/boot/Commons/Color.qml +258 -0
- package/ui/boot/Commons/I18n.qml +118 -0
- package/ui/boot/Commons/InputDevices.qml +112 -0
- package/ui/boot/Commons/InputMethodState.qml +38 -0
- package/ui/boot/Commons/IpcRegistry.qml +77 -0
- package/ui/boot/Commons/Numbers.js +19 -0
- package/ui/boot/Commons/Plural.js +91 -0
- package/ui/boot/Commons/ShellIpc.qml +12 -0
- package/ui/boot/Commons/Style.qml +561 -0
- package/ui/boot/Commons/Translations.qml +100 -0
- package/ui/boot/Commons/UntypedInput.qml +152 -0
- package/ui/boot/Commons/Util.qml +188 -0
- package/ui/boot/Commons/qmldir +13 -0
- package/ui/boot/LICENSE-omarchy +20 -0
- package/ui/boot/Ui/BackgroundMedia.qml +49 -0
- package/ui/boot/Ui/BarIconButton.qml +106 -0
- package/ui/boot/Ui/BarIconContent.qml +38 -0
- package/ui/boot/Ui/BarIndicator.qml +53 -0
- package/ui/boot/Ui/BarWidget.qml +50 -0
- package/ui/boot/Ui/BorderOverlay.qml +54 -0
- package/ui/boot/Ui/BorderSurface.qml +40 -0
- package/ui/boot/Ui/Button.qml +224 -0
- package/ui/boot/Ui/ButtonGroup.qml +131 -0
- package/ui/boot/Ui/ConfirmDialog.qml +134 -0
- package/ui/boot/Ui/CursorSurface.qml +42 -0
- package/ui/boot/Ui/Dropdown.qml +245 -0
- package/ui/boot/Ui/FilterInput.qml +26 -0
- package/ui/boot/Ui/IconContentBounds.qml +141 -0
- package/ui/boot/Ui/KeyboardPanel.qml +453 -0
- package/ui/boot/Ui/MultiSelect.qml +624 -0
- package/ui/boot/Ui/NumberField.qml +86 -0
- package/ui/boot/Ui/OpticalGlyph.qml +68 -0
- package/ui/boot/Ui/OverlayWindow.qml +102 -0
- package/ui/boot/Ui/Panel.qml +60 -0
- package/ui/boot/Ui/PanelActionButton.qml +105 -0
- package/ui/boot/Ui/PanelController.qml +16 -0
- package/ui/boot/Ui/PanelHero.qml +112 -0
- package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
- package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
- package/ui/boot/Ui/PanelSeparator.qml +19 -0
- package/ui/boot/Ui/PanelSlider.qml +156 -0
- package/ui/boot/Ui/PanelToolTip.qml +50 -0
- package/ui/boot/Ui/PluginBarApi.qml +89 -0
- package/ui/boot/Ui/PointerMoveGate.qml +54 -0
- package/ui/boot/Ui/PopupCard.qml +181 -0
- package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
- package/ui/boot/Ui/SearchableDropdown.qml +354 -0
- package/ui/boot/Ui/SizedImage.qml +125 -0
- package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
- package/ui/boot/Ui/TextField.qml +58 -0
- package/ui/boot/Ui/Toggle.qml +118 -0
- package/ui/boot/Ui/ToggleSwitch.qml +115 -0
- package/ui/boot/Ui/WidgetButton.qml +172 -0
- package/ui/boot/Ui/qmldir +41 -0
- package/ui/boot/fleatab.qml +50 -0
- package/ui/boot/picker.qml +39 -0
- package/ui/boot/shell.qml +122 -0
- package/ui/boot/tabtearoff.qml +51 -0
- package/ui/js/Anchor.js +379 -0
- package/ui/js/AnchorHold.js +162 -0
- package/ui/js/Archive.js +157 -0
- package/ui/js/ArchiveCard.js +44 -0
- package/ui/js/ArchiveOps.js +44 -0
- package/ui/js/BackReturn.js +118 -0
- package/ui/js/BulkRename.js +100 -0
- package/ui/js/Buttons.js +54 -0
- package/ui/js/ChromeKeys.js +116 -0
- package/ui/js/ClipMarks.js +73 -0
- package/ui/js/ClipSelection.js +67 -0
- package/ui/js/Clipboard.js +179 -0
- package/ui/js/Cloud.js +97 -0
- package/ui/js/CodeRuns.js +72 -0
- package/ui/js/Collide.js +212 -0
- package/ui/js/ColumnFit.js +28 -0
- package/ui/js/ColumnMenu.js +111 -0
- package/ui/js/Columns.js +298 -0
- package/ui/js/ColumnsCarry.js +103 -0
- package/ui/js/Compare.js +74 -0
- package/ui/js/Contrast.js +157 -0
- package/ui/js/ControlReader.js +42 -0
- package/ui/js/Convert.js +52 -0
- package/ui/js/CopyAs.js +52 -0
- package/ui/js/Crumbs.js +178 -0
- package/ui/js/Csv.js +231 -0
- package/ui/js/Density.js +44 -0
- package/ui/js/Devices.js +440 -0
- package/ui/js/DirSizes.js +180 -0
- package/ui/js/Drag.js +364 -0
- package/ui/js/DragOut.js +166 -0
- package/ui/js/DropDev.js +52 -0
- package/ui/js/Dropbox.js +41 -0
- package/ui/js/Eject.js +217 -0
- package/ui/js/Errors.js +188 -0
- package/ui/js/ExtThumbs.js +115 -0
- package/ui/js/Facts.js +402 -0
- package/ui/js/FigureDate.js +14 -0
- package/ui/js/Filter.js +285 -0
- package/ui/js/Find.js +46 -0
- package/ui/js/Flash.js +184 -0
- package/ui/js/FlashCard.js +79 -0
- package/ui/js/FlashEndings.js +125 -0
- package/ui/js/FlashPane.js +116 -0
- package/ui/js/FleaBin.js +12 -0
- package/ui/js/Focus.js +502 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +305 -0
- package/ui/js/Fuzzy.js +141 -0
- package/ui/js/GlassTag.js +72 -0
- package/ui/js/Grid.js +33 -0
- package/ui/js/GridGeometry.js +90 -0
- package/ui/js/GridNames.js +153 -0
- package/ui/js/GvfsBridge.js +200 -0
- package/ui/js/HostClipboard.js +29 -0
- package/ui/js/Icons.js +218 -0
- package/ui/js/Input.js +50 -0
- package/ui/js/Jump.js +200 -0
- package/ui/js/KeyName.js +64 -0
- package/ui/js/Keymap.js +401 -0
- package/ui/js/Kinds.js +164 -0
- package/ui/js/LICENSES/ISC-lucide.txt +43 -0
- package/ui/js/LangMarkPaths.js +47 -0
- package/ui/js/LangMarks.js +120 -0
- package/ui/js/Lists.js +11 -0
- package/ui/js/LocalSend.js +39 -0
- package/ui/js/LockedMenu.js +63 -0
- package/ui/js/MakeDefault.js +186 -0
- package/ui/js/MarkBox.js +7 -0
- package/ui/js/Markdown.js +119 -0
- package/ui/js/MarkdownLists.js +86 -0
- package/ui/js/MarkdownMaths.js +58 -0
- package/ui/js/MarkdownPictures.js +32 -0
- package/ui/js/MarkdownPrepared.js +139 -0
- package/ui/js/MarkdownTableFit.js +34 -0
- package/ui/js/Marks.js +148 -0
- package/ui/js/Marquee.js +96 -0
- package/ui/js/Match.js +93 -0
- package/ui/js/Material.js +109 -0
- package/ui/js/MdBlocks.js +437 -0
- package/ui/js/MdBreak.js +71 -0
- package/ui/js/MdChunks.js +70 -0
- package/ui/js/MdContainer.js +137 -0
- package/ui/js/MdDocument.js +337 -0
- package/ui/js/MdEmph.js +97 -0
- package/ui/js/MdEntity.js +108 -0
- package/ui/js/MdEntityTable.js +223 -0
- package/ui/js/MdEscape.js +59 -0
- package/ui/js/MdFront.js +37 -0
- package/ui/js/MdHold.js +32 -0
- package/ui/js/MdHtml.js +307 -0
- package/ui/js/MdHtmlBlock.js +290 -0
- package/ui/js/MdHtmlHeading.js +53 -0
- package/ui/js/MdHtmlImage.js +179 -0
- package/ui/js/MdInline.js +154 -0
- package/ui/js/MdItems.js +212 -0
- package/ui/js/MdLeaf.js +309 -0
- package/ui/js/MdLink.js +196 -0
- package/ui/js/MdMath.js +67 -0
- package/ui/js/MdParagraphs.js +138 -0
- package/ui/js/MdRefs.js +232 -0
- package/ui/js/MdResolve.js +226 -0
- package/ui/js/MdRun.js +300 -0
- package/ui/js/MdUrl.js +226 -0
- package/ui/js/Menu.js +514 -0
- package/ui/js/MenuContext.js +85 -0
- package/ui/js/MenuFit.js +13 -0
- package/ui/js/MenuRefresh.js +74 -0
- package/ui/js/MenuWheel.js +35 -0
- package/ui/js/Messages.js +186 -0
- package/ui/js/Motion.js +16 -0
- package/ui/js/Mounts.js +320 -0
- package/ui/js/MouseNav.js +13 -0
- package/ui/js/Nav.js +329 -0
- package/ui/js/NetFs.js +26 -0
- package/ui/js/OpenWith.js +83 -0
- package/ui/js/Ops.js +585 -0
- package/ui/js/Palette.js +33 -0
- package/ui/js/PathBar.js +260 -0
- package/ui/js/Permissions.js +179 -0
- package/ui/js/Phones.js +112 -0
- package/ui/js/Picker.js +353 -0
- package/ui/js/PlaceMenu.js +96 -0
- package/ui/js/Places.js +265 -0
- package/ui/js/PreviewAsk.js +21 -0
- package/ui/js/PreviewKeys.js +155 -0
- package/ui/js/PreviewSettle.js +29 -0
- package/ui/js/PreviewSwap.js +101 -0
- package/ui/js/PropertyFacts.js +144 -0
- package/ui/js/Protocols.js +296 -0
- package/ui/js/QuickLookKinds.js +27 -0
- package/ui/js/QuickLookRect.js +24 -0
- package/ui/js/RailDrop.js +51 -0
- package/ui/js/RailKeys.js +87 -0
- package/ui/js/RailMenu.js +133 -0
- package/ui/js/Recent.js +135 -0
- package/ui/js/RecentMode.js +156 -0
- package/ui/js/Reload.js +60 -0
- package/ui/js/Renderer.js +9 -0
- package/ui/js/RunFile.js +143 -0
- package/ui/js/Scripts.js +28 -0
- package/ui/js/Scroll.js +304 -0
- package/ui/js/ScrollOff.js +69 -0
- package/ui/js/Search.js +478 -0
- package/ui/js/Selection.js +97 -0
- package/ui/js/Settings.js +476 -0
- package/ui/js/SettingsAbout.js +97 -0
- package/ui/js/SettingsShelf.js +52 -0
- package/ui/js/SheetKeys.js +42 -0
- package/ui/js/SheetQuery.js +281 -0
- package/ui/js/ShelfPile.js +27 -0
- package/ui/js/SlowClick.js +53 -0
- package/ui/js/SlowOp.js +6 -0
- package/ui/js/Sort.js +134 -0
- package/ui/js/Startup.js +63 -0
- package/ui/js/StateWords.js +17 -0
- package/ui/js/Status.js +238 -0
- package/ui/js/Swap.js +166 -0
- package/ui/js/TabMove.js +39 -0
- package/ui/js/Tabs.js +736 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +174 -0
- package/ui/js/TextSize.js +107 -0
- package/ui/js/Thumbs.js +158 -0
- package/ui/js/Transfer.js +172 -0
- package/ui/js/Trash.js +81 -0
- package/ui/js/TrashDates.js +47 -0
- package/ui/js/TrashDrop.js +64 -0
- package/ui/js/UiState.js +438 -0
- package/ui/js/Unlock.js +152 -0
- package/ui/js/Update.js +130 -0
- package/ui/js/UserDirs.js +56 -0
- package/ui/qmldir +228 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test, expect, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { markdownPage, cleanHtml, holdFormulas, linkAllowed, pageStyle, markdownHtml, MARKDOWN_LIMIT } from "../src/markdown-html.js";
|
|
6
|
+
|
|
7
|
+
const root = mkdtempSync(join(tmpdir(), "flea-md-html-"));
|
|
8
|
+
const docs = join(root, "docs");
|
|
9
|
+
mkdirSync(docs);
|
|
10
|
+
// A 1x1 PNG.
|
|
11
|
+
const PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", "base64");
|
|
12
|
+
writeFileSync(join(docs, "pic.png"), PNG);
|
|
13
|
+
writeFileSync(join(root, "secret.png"), PNG);
|
|
14
|
+
symlinkSync(join(root, "secret.png"), join(docs, "link.png"));
|
|
15
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
|
16
|
+
const body = (markdown) => markdownPage(markdown, docs).replace(/^[\s\S]*?<\/style>/, "");
|
|
17
|
+
|
|
18
|
+
test("Markdown becomes headings, lists, tables, code and task boxes", () => {
|
|
19
|
+
const html = body("# Title\n\n- **bold** and *it*\n- `code`\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n```js\nlet x = 1 < 2;\n```\n\n- [x] done\n- [ ] todo\n");
|
|
20
|
+
expect(html).toContain("<h1>Title</h1>");
|
|
21
|
+
expect(html).toContain("<strong>bold</strong>");
|
|
22
|
+
expect(html).toContain("<table>");
|
|
23
|
+
expect(html).toContain("let x = 1 < 2;");
|
|
24
|
+
expect(html).toContain('<input checked type="checkbox" disabled>');
|
|
25
|
+
expect(html).not.toMatch(/<script|<style(?![^]*<\/style>[^]*$)/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("TeX in $…$ and $$…$$ and raw MathML become formulas; code and prices stay text", () => {
|
|
29
|
+
const html = body("Inline $x^2$ and\n\n$$\\frac{a}{b}$$\n\n<math><mi>y</mi></math>\n\n`$not$` costs $5 and $10\n\n```\n$also not$\n```\n");
|
|
30
|
+
expect((html.match(/<math/g) ?? []).length).toBe(3);
|
|
31
|
+
expect(html).toContain('<math display="block">');
|
|
32
|
+
expect(html).toContain("<code>$not$</code>");
|
|
33
|
+
expect(html).toContain("costs $5 and $10");
|
|
34
|
+
expect(html).toContain("$also not$");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a formula that fails is shown as its source, escaped", () => {
|
|
38
|
+
const html = body("Bad $\\frac{1}{$ and <math><mi title=x>a</mi></math>");
|
|
39
|
+
expect(html).toContain('<code class="formula-source">$\\frac{1}{$</code>');
|
|
40
|
+
expect(html).toContain("<math><mi title=x>a</mi></math>");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("active content, raw-text elements and foreign markup never survive", () => {
|
|
44
|
+
const attacks = [
|
|
45
|
+
"<script>alert(1)</script>", "<style>body{background:url(http://x)}</style>", "<iframe src=x></iframe>",
|
|
46
|
+
"<textarea><img src=x onerror=alert(1)></textarea>", "<title><script>alert(1)</script></title>",
|
|
47
|
+
"<svg><script>alert(1)</script></svg>", "<object data=x></object>", "<embed src=x>",
|
|
48
|
+
"<noscript><img src=x onerror=alert(1)></noscript>", "<xmp><img src=x onerror=alert(1)></xmp>",
|
|
49
|
+
];
|
|
50
|
+
for (const attack of attacks) {
|
|
51
|
+
const html = body(`before\n\n${attack}\n\nafter`);
|
|
52
|
+
expect(html).not.toMatch(/<(script|style|iframe|object|embed|svg|img|textarea|title|xmp|noscript)[\s>]/i);
|
|
53
|
+
expect(html).toContain("after");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("only the allowed attributes stay; links keep http, https, mailto and relative targets", () => {
|
|
58
|
+
const html = body('<p align="center" onclick="alert(1)" style="color:red" class="c" id="i">x</p>\n\n' +
|
|
59
|
+
'[ok](https://example.com) [mail](mailto:a@b.c) [rel](other.md) [js](javascript:alert(1)) <a href="javascript:alert(1)">e</a> <a href="data:text/html,x">d</a>');
|
|
60
|
+
expect(html).toContain('<p align="center">x</p>');
|
|
61
|
+
expect(html).not.toMatch(/onclick|style=|class="c"|id="i"/);
|
|
62
|
+
expect(html).toContain('href="https://example.com"');
|
|
63
|
+
expect(html).toContain('href="mailto:a@b.c"');
|
|
64
|
+
expect(html).toContain('href="other.md"');
|
|
65
|
+
expect(html).not.toMatch(/javascript|jav&|data:text/i);
|
|
66
|
+
for (const [target, allowed] of [["https://x", true], ["#top", true], ["a/b.md", true], ["JaVaScRiPt:x", false],
|
|
67
|
+
["java\tscript:x", false], ["%6Aavascript:x", false], ["vbscript:x", false], ["file:///etc/passwd", false]])
|
|
68
|
+
expect(linkAllowed(target)).toBe(allowed);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("pictures: inside the folder embedded, remote ones named, escapes dropped", () => {
|
|
72
|
+
const html = body("     ");
|
|
73
|
+
expect(html).toMatch(/<img alt="a" src="data:image\/png;base64,[A-Za-z0-9+/=]+">/);
|
|
74
|
+
expect(html).toContain('<span class="remote">Remote image not loaded · img.shields.io</span>');
|
|
75
|
+
// Outside the folder (by .., by absolute path or through a symbolic link) and data: are not loaded.
|
|
76
|
+
expect((html.match(/<img/g) ?? []).length).toBe(1);
|
|
77
|
+
for (const alt of ["up", "abs", "ln", "d"]) expect(html).toContain(`<span class="alt">${alt}</span>`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the theme only accepts #rrggbb colours", () => {
|
|
81
|
+
const style = pageStyle({ foreground: "red;}</style><script>alert(1)</script>", background: "#101010", bodyPx: "99" });
|
|
82
|
+
expect(style).not.toContain("script");
|
|
83
|
+
expect(style).toContain("background:#101010");
|
|
84
|
+
expect(style).toContain("font:40px");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("formula markers in the source are neutralised and text never becomes markup", () => {
|
|
88
|
+
const { text, formulas } = holdFormulas("0 and $x$");
|
|
89
|
+
expect(formulas.length).toBe(1);
|
|
90
|
+
expect(text.startsWith("�0�")).toBe(true);
|
|
91
|
+
expect(cleanHtml("<p>a <b> c</p>", docs)).toBe("<p>a <b> c</p>");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("the backend answer reads the file and refuses one over the limit", async () => {
|
|
95
|
+
const path = join(docs, "note.md");
|
|
96
|
+
writeFileSync(path, "# Note\n\n$x$\n");
|
|
97
|
+
const ready = await markdownHtml({ path, slot: "look", token: 3, theme: {} });
|
|
98
|
+
expect(ready).toMatchObject({ t: "markdownhtml", slot: "look", token: 3, state: "ready" });
|
|
99
|
+
expect(ready.html.startsWith("<!doctype html>")).toBe(true);
|
|
100
|
+
expect(ready.html).toContain("<math>");
|
|
101
|
+
const big = join(docs, "big.md");
|
|
102
|
+
writeFileSync(big, "x".repeat(MARKDOWN_LIMIT + 1));
|
|
103
|
+
expect((await markdownHtml({ path: big, token: 4 })).state).toBe("failed");
|
|
104
|
+
expect((await markdownHtml({ path: join(docs, "missing.md"), token: 5 })).state).toBe("failed");
|
|
105
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { texToMathML, cleanMathML, cleanStyle, length, MATH_LIMIT } from "../src/mathml.js";
|
|
3
|
+
|
|
4
|
+
const ELEMENT = /<\/?([a-z][a-z0-9-]*)/g;
|
|
5
|
+
// Every element and attribute name in some cleaned markup.
|
|
6
|
+
const names = (html) => ({
|
|
7
|
+
elements: [...new Set([...html.matchAll(ELEMENT)].map((match) => match[1]))],
|
|
8
|
+
attributes: [...new Set([...html.matchAll(/\s([a-z-]+)="/g)].map((match) => match[1]))],
|
|
9
|
+
});
|
|
10
|
+
const ALLOWED_ELEMENTS = new Set(("math mrow mi mn mo ms mtext mspace mfrac msqrt mroot msub msup msubsup munder mover " +
|
|
11
|
+
"munderover mmultiscripts mprescripts none mtable mtr mtd mstyle mpadded mphantom menclose merror semantics").split(" "));
|
|
12
|
+
const ALLOWED_ATTRIBUTES = new Set(("display displaystyle scriptlevel mathvariant form stretchy symmetric largeop movablelimits " +
|
|
13
|
+
"fence separator accent accentunder lspace rspace width height depth voffset minsize maxsize linethickness style").split(" "));
|
|
14
|
+
const onlyAllowed = (html) => {
|
|
15
|
+
const found = names(html);
|
|
16
|
+
expect(found.elements.filter((name) => !ALLOWED_ELEMENTS.has(name))).toEqual([]);
|
|
17
|
+
expect(found.attributes.filter((name) => !ALLOWED_ATTRIBUTES.has(name))).toEqual([]);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
test("a formula becomes one math element of MathML Core elements", () => {
|
|
21
|
+
const html = texToMathML("x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}", true);
|
|
22
|
+
expect(html.startsWith('<math display="block">')).toBe(true);
|
|
23
|
+
expect(html.endsWith("</math>")).toBe(true);
|
|
24
|
+
expect(html).toContain("<mfrac>");
|
|
25
|
+
expect(html).toContain("<msqrt>");
|
|
26
|
+
onlyAllowed(html);
|
|
27
|
+
for (const tex of ["\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}", "\\sum_{i=1}^n i", "\\boxed{E=mc^2}", "\\overbrace{a+b}^{n}", "\\hat x\\vec v", "\\text{中文 and emoji 😀}"])
|
|
28
|
+
onlyAllowed(texToMathML(tex, true));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("trust is off: links, URLs, images and HTML commands stay text", () => {
|
|
32
|
+
for (const tex of ["\\href{javascript:alert(1)}{x}", "\\url{http://example.com}", "\\includegraphics{http://example.com/a.png}",
|
|
33
|
+
"\\htmlClass{x}{y}", "\\htmlStyle{background:url(http://x)}{y}", "\\htmlData{a=b}{y}", "\\htmlId{x}{y}"]) {
|
|
34
|
+
let html;
|
|
35
|
+
try { html = texToMathML(tex); } catch { continue; } // a refused command is an error, which is also safe
|
|
36
|
+
onlyAllowed(html);
|
|
37
|
+
// The command's name and argument may show as text; no attribute carries them.
|
|
38
|
+
expect(html).not.toMatch(/\s(?:href|src)="|url\(/i);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("markup typed into TeX never becomes markup", () => {
|
|
43
|
+
for (const tex of ["\\text{<script>alert(1)</script>}", "\\text{</math><img src=x onerror=alert(1)>}", "\\mathrm{<b>x</b>}", "x<y>z"]) {
|
|
44
|
+
let html;
|
|
45
|
+
try { html = texToMathML(tex); } catch { continue; }
|
|
46
|
+
onlyAllowed(html);
|
|
47
|
+
// Typed markup survives only as escaped text.
|
|
48
|
+
expect(html).not.toMatch(/<(?:script|img|b)[\s>]/i);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("runaway macros, huge sizes and long sources are refused", () => {
|
|
53
|
+
expect(() => texToMathML("\\def\\a{\\a\\a}\\a")).toThrow();
|
|
54
|
+
expect(() => texToMathML("x".repeat(MATH_LIMIT + 1))).toThrow();
|
|
55
|
+
const sized = texToMathML("\\rule{1000em}{1000em}\\kern{1000em}");
|
|
56
|
+
for (const [, value] of sized.matchAll(/(?:width|height|depth|lspace|rspace)="([^"]*)"/g)) expect(length(value)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a TeX error is not turned into markup: the caller shows the source instead", () => {
|
|
60
|
+
expect(() => texToMathML("\\frac{1}{")).toThrow();
|
|
61
|
+
expect(() => texToMathML("\\text{a & b}")).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("the allowlist drops foreign content, handlers, links and unknown attributes", () => {
|
|
65
|
+
const html = cleanMathML('<math><mi onclick="alert(1)" href="x" title="t" class="c">a</mi><mtext>t<img src="x" onerror="alert(1)">u</img></mtext>' +
|
|
66
|
+
'<svg><script>alert(1)</script></svg><annotation-xml encoding="text/html"><b>x</b></annotation-xml><mo stretchy="maybe" form="infix">+</mo></math>');
|
|
67
|
+
expect(html).toBe('<math><mi>a</mi><mtext>t</mtext><mo form="infix">+</mo></math>');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("style keeps colours and box spacing only", () => {
|
|
71
|
+
expect(cleanStyle("color:#ff0000;padding:3pt;border:1px solid;background:url(http://x);position:fixed;margin-left:-0.1667em")).toBe(
|
|
72
|
+
"color:#ff0000;padding:3pt;border:1px solid;margin-left:-0.1667em");
|
|
73
|
+
expect(cleanStyle("color:expression(alert(1));padding:1e9em;color:red")).toBe("");
|
|
74
|
+
expect(cleanStyle("padding:-1em;border:1px solid url(x)")).toBe("");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("only strict, balanced markup with exactly one math root is accepted", () => {
|
|
78
|
+
for (const bad of ["<math><mi>a<!-- x --></mi></math>", "<math><mi title=a>a</mi></math>", "<math><mi>a</mo></math>",
|
|
79
|
+
"<math><mi>a</mi>", "<math><mi>a</mi></math><math><mi>b</mi></math>", "<p><math><mi>x</mi></math></p>", "<math><![CDATA[x]]></math>"])
|
|
80
|
+
expect(() => cleanMathML(bad)).toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("text keeps entities as text and escapes stray ampersands and brackets", () => {
|
|
84
|
+
expect(cleanMathML("<math><mtext><b> &bogus; & x > y</mtext></math>")).toBe("<math><mtext><b> &bogus; & x > y</mtext></math>");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("nesting past the depth limit is dropped", () => {
|
|
88
|
+
const deep = "<math>" + "<mrow>".repeat(80) + "<mi>x</mi>" + "</mrow>".repeat(80) + "</math>";
|
|
89
|
+
const html = cleanMathML(deep);
|
|
90
|
+
expect((html.match(/<mrow>/g) ?? []).length).toBe(63);
|
|
91
|
+
expect(html).not.toContain("<mi>");
|
|
92
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { test, expect, afterAll } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, renameSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { menuaction, capturedPaths } from '../src/menu.js';
|
|
6
|
+
import { collisions, transfer } from '../src/transfer.js';
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-menu-'));afterAll(()=>rmSync(dir,{recursive:true,force:true}));
|
|
8
|
+
let serial=0;
|
|
9
|
+
function setup() {
|
|
10
|
+
const id=++serial, root=join(dir,String(id));mkdirSync(root);
|
|
11
|
+
const src=join(root,'source'),dest=join(root,'dest');mkdirSync(src);mkdirSync(dest);
|
|
12
|
+
const path=join(src,'hello');writeFileSync(path,'original');
|
|
13
|
+
const messages=[],send=m=>messages.push(m),listing={path:src,rows:[{name:'hello'}]};
|
|
14
|
+
menuaction({id,op:'snapshot',rows:[0],cursor:0},send,listing,1);
|
|
15
|
+
return {id,path,src,dest,messages,send,listing};
|
|
16
|
+
}
|
|
17
|
+
async function done(s, request) {
|
|
18
|
+
transfer(request,s.send,s.listing,1);
|
|
19
|
+
for(let i=0;i<200;i++) {const result=s.messages.find(m=>m.t==='transferdone');if(result)return result; await Bun.sleep(5);}
|
|
20
|
+
throw Error('transfer did not complete: '+JSON.stringify(s.messages));
|
|
21
|
+
}
|
|
22
|
+
test('Copy to survives menu close',async()=>{
|
|
23
|
+
const s=setup();collisions({id:20,menuId:s.id,dest:s.dest},s.send,s.listing,1);
|
|
24
|
+
menuaction({id:s.id,op:'close'},s.send,s.listing,1);
|
|
25
|
+
const result=await done(s,{menuId:s.id,collideId:20,dest:s.dest,op:'copy',collide:'refuse'});
|
|
26
|
+
expect(result.ok).toBe(1);expect(readFileSync(join(s.dest,'hello'),'utf8')).toBe('original');
|
|
27
|
+
});
|
|
28
|
+
test('reject source replaced after collision question',async()=>{
|
|
29
|
+
const s=setup();collisions({id:21,menuId:s.id,dest:s.dest},s.send,s.listing,1);
|
|
30
|
+
menuaction({id:s.id,op:'close'},s.send,s.listing,1);
|
|
31
|
+
renameSync(s.path,s.path+'.old');writeFileSync(s.path,'replacement');
|
|
32
|
+
expect((await done(s,{menuId:s.id,collideId:21,dest:s.dest,op:'copy'})).failed).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
test('question cannot authorize another destination or be reused',()=>{
|
|
35
|
+
const s=setup();collisions({id:22,menuId:s.id,dest:s.dest},s.send,s.listing,1);
|
|
36
|
+
menuaction({id:s.id,op:'close'},s.send,s.listing,1);
|
|
37
|
+
transfer({menuId:s.id,collideId:22,dest:s.src},s.send,s.listing,1);
|
|
38
|
+
expect(s.messages.at(-1).t).toBe('error');
|
|
39
|
+
transfer({menuId:s.id,collideId:22,dest:s.dest},s.send,s.listing,1);
|
|
40
|
+
expect(s.messages.at(-1).t).toBe('error');
|
|
41
|
+
});
|
|
42
|
+
test('invalid repeated snapshot clears old identity and refuses missing rows',()=>{
|
|
43
|
+
const s=setup();menuaction({id:s.id,op:'snapshot',rows:[0,99]},s.send,s.listing,1);
|
|
44
|
+
expect(s.messages.at(-1).ok).toBe(false);expect(capturedPaths(s.id)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
test('cursor outside selection also retains identity',()=>{
|
|
47
|
+
const s=setup(),cursor=join(s.src,'cursor');writeFileSync(cursor,'cursor');
|
|
48
|
+
menuaction({id:s.id,op:'snapshot',paths:[s.path],cursorPath:cursor},s.send,s.listing,1);
|
|
49
|
+
renameSync(cursor,cursor+'.old');writeFileSync(cursor,'replacement');
|
|
50
|
+
menuaction({id:s.id,op:'activate',action:'sharelink'},s.send,s.listing,1);
|
|
51
|
+
expect(s.messages.at(-1).ok).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
test('keyboard menuId zero copies without a menu snapshot',async()=>{
|
|
54
|
+
const s=setup();collisions({id:99,menuId:0,rows:[0],dest:s.dest},s.send,s.listing,1);
|
|
55
|
+
const result=await done(s,{menuId:0,collideId:99,rows:[0],dest:s.dest,op:'copy',collide:'refuse'});
|
|
56
|
+
expect(result.ok).toBe(1);expect(readFileSync(join(s.dest,'hello'),'utf8')).toBe('original');
|
|
57
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { test, expect, afterAll } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { deflateRawSync } from 'node:zlib';
|
|
6
|
+
import { officeKind, officePreview } from '../src/office.js';
|
|
7
|
+
import { html, previewHtml } from '../src/html.js';
|
|
8
|
+
import { code } from '../src/code.js';
|
|
9
|
+
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), 'flea-office-'));
|
|
11
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
12
|
+
|
|
13
|
+
// A zip of the given members, deflated unless `store`; CRCs are zero because nothing here checks them.
|
|
14
|
+
function zip(members, store = false) {
|
|
15
|
+
const locals = [], centrals = [];
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const [name, content] of Object.entries(members)) {
|
|
18
|
+
const raw = Buffer.from(content), data = store ? raw : deflateRawSync(raw), id = Buffer.from(name);
|
|
19
|
+
const local = Buffer.alloc(30);
|
|
20
|
+
local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(store ? 0 : 8, 8);
|
|
21
|
+
local.writeUInt32LE(data.length, 18); local.writeUInt32LE(raw.length, 22); local.writeUInt16LE(id.length, 26);
|
|
22
|
+
const central = Buffer.alloc(46);
|
|
23
|
+
central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6);
|
|
24
|
+
central.writeUInt16LE(store ? 0 : 8, 10); central.writeUInt32LE(data.length, 20); central.writeUInt32LE(raw.length, 24);
|
|
25
|
+
central.writeUInt16LE(id.length, 28); central.writeUInt32LE(offset, 42);
|
|
26
|
+
locals.push(local, id, data); centrals.push(central, id);
|
|
27
|
+
offset += 30 + id.length + data.length;
|
|
28
|
+
}
|
|
29
|
+
const directory = Buffer.concat(centrals), end = Buffer.alloc(22);
|
|
30
|
+
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(Object.keys(members).length, 8); end.writeUInt16LE(Object.keys(members).length, 10);
|
|
31
|
+
end.writeUInt32LE(directory.length, 12); end.writeUInt32LE(offset, 16);
|
|
32
|
+
return Buffer.concat([...locals, directory, end]);
|
|
33
|
+
}
|
|
34
|
+
const file = (name, bytes) => { const path = join(dir, name); writeFileSync(path, bytes); return path; };
|
|
35
|
+
|
|
36
|
+
const W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
|
|
37
|
+
const docx = {
|
|
38
|
+
'word/styles.xml': `<w:styles ${W}><w:style w:type="paragraph" w:styleId="Kop1"><w:name w:val="heading 1"/></w:style><w:style w:type="paragraph" w:styleId="T"><w:name w:val="Title"/></w:style></w:styles>`,
|
|
39
|
+
'word/numbering.xml': `<w:numbering ${W}><w:abstractNum w:abstractNumId="0"><w:lvl w:ilvl="0"><w:numFmt w:val="bullet"/></w:lvl></w:abstractNum><w:abstractNum w:abstractNumId="1"><w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/></w:lvl></w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num><w:num w:numId="2"><w:abstractNumId w:val="1"/></w:num></w:numbering>`,
|
|
40
|
+
'word/document.xml': `<?xml version="1.0"?><w:document ${W}><w:body>
|
|
41
|
+
<w:p><w:pPr><w:pStyle w:val="Kop1"/><w:rPr><w:b/></w:rPr></w:pPr><w:r><w:t>Report & plan</w:t></w:r></w:p>
|
|
42
|
+
<w:p><w:r><w:t xml:space="preserve">Plain </w:t></w:r><w:r><w:rPr><w:b/><w:i w:val="0"/></w:rPr><w:t>bold</w:t></w:r><w:r><w:rPr><w:i/><w:u w:val="single"/></w:rPr><w:t> a<b</w:t></w:r><w:r><w:rPr><w:vertAlign w:val="superscript"/></w:rPr><w:t>2</w:t></w:r></w:p>
|
|
43
|
+
<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr><w:r><w:t>one</w:t></w:r></w:p>
|
|
44
|
+
<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr><w:r><w:t>two</w:t></w:r></w:p>
|
|
45
|
+
<w:p><w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr></w:pPr><w:r><w:t>first</w:t></w:r></w:p>
|
|
46
|
+
<w:tbl><w:tr><w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p><w:p><w:r><w:t>line</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>B1</w:t></w:r></w:p></w:tc></w:tr></w:tbl>
|
|
47
|
+
<w:p><w:r><w:t>kept</w:t></w:r><w:del><w:r><w:delText>removed</w:delText></w:r></w:del><w:r><w:instrText>PAGE</w:instrText></w:r><w:r><w:br/><w:t>next</w:t><w:tab/><w:t>col</w:t></w:r></w:p>
|
|
48
|
+
<w:p/>
|
|
49
|
+
</w:body></w:document>`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
test('office names are told apart by extension', () => {
|
|
53
|
+
expect(['a.DOCX', 'b.odt', 'c.pptx', 'd.odp', 'e.xlsx', 'f.ods', 'g.doc', 'h.zip', 'docx'].map(officeKind))
|
|
54
|
+
.toEqual(['document', 'document', 'slides', 'slides', 'sheet', 'sheet', '', '', '']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('DOCX becomes headings, emphasis, lists and tables in the fixed tags', () => {
|
|
58
|
+
const preview = officePreview(file('rich.docx', zip(docx)));
|
|
59
|
+
expect(preview.listed).toBe(false);
|
|
60
|
+
expect(preview.html).toBe('<h1>Report & plan</h1><p>Plain <b>bold</b><u><i> a<b</i></u><sup>2</sup></p>'
|
|
61
|
+
+ '<ul><li>one</li><li>two</li></ul><ol><li>first</li></ol>'
|
|
62
|
+
+ '<table><tr><td>A1<br>line</td><td>B1</td></tr></table>'
|
|
63
|
+
+ '<p>kept<br>next col</p>');
|
|
64
|
+
// The fragment survives the HTML preview's own rebuild unchanged, so every tag is one it keeps.
|
|
65
|
+
expect(previewHtml(preview.html)).toBe(preview.html);
|
|
66
|
+
expect(preview.text).toBe('Report & plan\nPlain bold a<b2\none\ntwo\nfirst\nA1\nline\tB1\n\nkept\nnext col\n');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the html and code requests answer a DOCX from its text, stored or deflated', async () => {
|
|
70
|
+
const path = file('stored.docx', zip(docx, true));
|
|
71
|
+
expect(await html({ path, token: 3, slot: 'column' })).toMatchObject({ state: 'ready', token: 3, total: 9, html: officePreview(path).html });
|
|
72
|
+
const reply = await code({ path, first: 1, count: 2 });
|
|
73
|
+
expect(reply).toMatchObject({ state: 'plain', total: 10, lines: [[0, 'Report & plan'], [0, 'Plain bold a<b2']] });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('PPTX slides follow the presentation order, with titles and tables', () => {
|
|
77
|
+
const slide = (title, body) => `<p:sld><p:sp><p:nvSpPr><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr><p:txBody><a:p><a:r><a:t>${title}</a:t></a:r></a:p></p:txBody></p:sp>`
|
|
78
|
+
+ `<p:sp><p:nvSpPr><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr><p:txBody><a:p><a:r><a:rPr b="1"/><a:t>${body}</a:t></a:r><a:br/><a:r><a:t>more</a:t></a:r></a:p></p:txBody></p:sp></p:sld>`;
|
|
79
|
+
const preview = officePreview(file('deck.pptx', zip({
|
|
80
|
+
'ppt/presentation.xml': '<p:presentation><p:sldIdLst><p:sldId id="1" r:id="rId2"/><p:sldId id="2" r:id="rId1"/></p:sldIdLst></p:presentation>',
|
|
81
|
+
'ppt/_rels/presentation.xml.rels': '<Relationships><Relationship Id="rId1" Target="slides/slide1.xml"/><Relationship Id="rId2" Target="slides/slide2.xml"/></Relationships>',
|
|
82
|
+
'ppt/slides/slide1.xml': slide('Second', 'b'),
|
|
83
|
+
'ppt/slides/slide2.xml': slide('First', 'a'),
|
|
84
|
+
})));
|
|
85
|
+
expect(preview.html).toBe('<h2>Slide 1</h2><h3>First</h3><p><b>a</b><br>more</p><h2>Slide 2</h2><h3>Second</h3><p><b>b</b><br>more</p>');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('ODT reads headings, styled spans, lists and tables, without tracked changes or notes', () => {
|
|
89
|
+
const preview = officePreview(file('text.odt', zip({ 'content.xml': `<office:document-content>
|
|
90
|
+
<office:automatic-styles><style:style style:name="T1" style:family="text"><style:text-properties fo:font-weight="bold" fo:font-style="italic"/></style:style></office:automatic-styles>
|
|
91
|
+
<office:body><office:text><text:tracked-changes><text:p>gone</text:p></text:tracked-changes>
|
|
92
|
+
<text:h text:outline-level="2">Head</text:h>
|
|
93
|
+
<text:p>a<text:span text:style-name="T1">b</text:span><text:s text:c="2"/>c<text:note><text:p>foot</text:p></text:note><text:line-break/>d</text:p>
|
|
94
|
+
<text:list><text:list-item><text:p>x</text:p></text:list-item><text:list-item><text:p>y</text:p></text:list-item></text:list>
|
|
95
|
+
<table:table><table:table-row><table:table-cell><text:p>1</text:p></table:table-cell><table:table-cell><text:p>2</text:p></table:table-cell></table:table-row></table:table>
|
|
96
|
+
</office:text></office:body></office:document-content>` })));
|
|
97
|
+
expect(preview.html).toBe('<h2>Head</h2><p>a<i><b>b</b></i> c<br>d</p><ul><li>x</li><li>y</li></ul><table><tr><td>1</td><td>2</td></tr></table>');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('XLSX answers its first sheet as CSV: shared strings, dates, booleans, gaps and quoting', async () => {
|
|
101
|
+
const path = file('book.xlsx', zip({
|
|
102
|
+
'xl/workbook.xml': '<workbook><sheets><sheet name="Second" sheetId="2" r:id="rId2"/><sheet name="Other" sheetId="1" r:id="rId1"/></sheets></workbook>',
|
|
103
|
+
'xl/_rels/workbook.xml.rels': '<Relationships><Relationship Id="rId1" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Target="/xl/worksheets/sheet2.xml"/></Relationships>',
|
|
104
|
+
'xl/sharedStrings.xml': '<sst><si><t>Name</t></si><si><r><t>Say </t></r><r><t>"hi", ok</t></r><rPh><t>skip</t></rPh></si></sst>',
|
|
105
|
+
'xl/styles.xml': '<styleSheet><numFmts><numFmt numFmtId="164" formatCode="yyyy\\-mm\\-dd"/></numFmts><cellStyleXfs><xf numFmtId="14"/></cellStyleXfs><cellXfs><xf numFmtId="0"/><xf numFmtId="164"/><xf numFmtId="20"/></cellXfs></styleSheet>',
|
|
106
|
+
'xl/worksheets/sheet1.xml': '<worksheet><sheetData><row r="1"><c r="A1"><v>wrong sheet</v></c></row></sheetData></worksheet>',
|
|
107
|
+
'xl/worksheets/sheet2.xml': '<worksheet><sheetData>'
|
|
108
|
+
+ '<row r="1"><c r="A1" t="s"><v>0</v></c><c r="C1" t="inlineStr"><is><t>When</t></is></c></row>'
|
|
109
|
+
+ '<row r="2"><c r="A2" t="s"><v>1</v></c><c r="B2"><v>1.50</v></c><c r="C2" s="1"><v>45000</v></c><c r="D2" t="b"><v>1</v></c><c r="E2" s="2"><v>0.5</v></c></row>'
|
|
110
|
+
+ '<row r="4"><c r="B4" t="str"><f>A1</f><v>calc</v></c></row>'
|
|
111
|
+
+ '</sheetData></worksheet>',
|
|
112
|
+
}));
|
|
113
|
+
const preview = officePreview(path);
|
|
114
|
+
expect(preview.csv).toBe('Name,,When\n"Say ""hi"", ok",1.5,2023-03-15,TRUE,12:00:00\n\n,calc\n[Sheet 1 of 2: Second. The other sheets are not shown.]\n');
|
|
115
|
+
// The table asks by record; the quoted record and the count come back whole.
|
|
116
|
+
expect(await code({ path, raw: true, csv: ',', record: 2, count: 1 })).toMatchObject({ first: 2, records: 5, firstRecord: 2, lines: [[0, '"Say ""hi"", ok",1.5,2023-03-15,TRUE,12:00:00']] });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('ODS repeats filled cells and ignores the padding to the sheet edge', () => {
|
|
120
|
+
const preview = officePreview(file('sheet.ods', zip({ 'content.xml': `<office:document-content><office:body><office:spreadsheet>
|
|
121
|
+
<table:table table:name="S"><table:table-row><table:table-cell table:number-columns-repeated="2"><text:p>x</text:p></table:table-cell><table:table-cell table:number-columns-repeated="16382"/></table:table-row>
|
|
122
|
+
<table:table-row table:number-rows-repeated="2"><table:table-cell/></table:table-row>
|
|
123
|
+
<table:table-row><table:table-cell/><table:table-cell><text:p>a, b</text:p><text:p>c</text:p></table:table-cell></table:table-row>
|
|
124
|
+
<table:table-row table:number-rows-repeated="1048570"><table:table-cell table:number-columns-repeated="16384"/></table:table-row></table:table>
|
|
125
|
+
</office:spreadsheet></office:body></office:document-content>` })));
|
|
126
|
+
expect(preview.csv).toBe('x,x\n\n\n,"a, b\nc"\n');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('a container with no readable content lists its members, and a non-zip is refused', async () => {
|
|
130
|
+
const empty = officePreview(file('empty.docx', zip({ '[Content_Types].xml': '<Types/>', 'word/other.xml': '<x/>' })));
|
|
131
|
+
expect(empty).toMatchObject({ listed: true, html: '<p>No readable text in this document. It contains:</p><ul><li>[Content_Types].xml</li><li>word/other.xml</li></ul>' });
|
|
132
|
+
const sheet = officePreview(file('empty.xlsx', zip({ 'a&b.bin': 'xyz' })));
|
|
133
|
+
expect(sheet).toMatchObject({ listed: true, csv: 'Member,Bytes\na&b.bin,3\n' });
|
|
134
|
+
const legacy = file('legacy.docx', Buffer.from('\xd0\xcf\x11\xe0 not a zip', 'latin1'));
|
|
135
|
+
expect(() => officePreview(legacy)).toThrow();
|
|
136
|
+
expect((await html({ path: legacy, token: 1, slot: 'x' })).state).toBe('failed');
|
|
137
|
+
expect((await code({ path: legacy })).state).toBe('failed');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('hostile XML is neither expanded nor passed through', () => {
|
|
141
|
+
const bomb = officePreview(file('bomb.docx', zip({ 'word/document.xml': '<!DOCTYPE d [<!ENTITY a "aaaa">]><w:document><w:body><w:p><w:r><w:t>&a;</w:t></w:r></w:p></w:body></w:document>' })));
|
|
142
|
+
expect(bomb.listed).toBe(true);
|
|
143
|
+
const markup = officePreview(file('markup.docx', zip({ 'word/document.xml': '<w:document><w:body><w:p><w:r><w:t><script>x</script> <img> &amp;</w:t></w:r></w:p></w:body></w:document>' })));
|
|
144
|
+
expect(markup.html).toBe('<p><script>x</script> <img> &amp;</p>');
|
|
145
|
+
expect(previewHtml(markup.html)).toBe(markup.html);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a long document stops at the output limit with balanced tags', () => {
|
|
149
|
+
const paragraph = `<w:p><w:r><w:t>${'word '.repeat(200)}</w:t></w:r></w:p>`;
|
|
150
|
+
const preview = officePreview(file('long.docx', zip({ 'word/document.xml': `<w:document><w:body>${paragraph.repeat(2000)}</w:body></w:document>` })));
|
|
151
|
+
expect(preview.html.length).toBeLessThan(1 << 20);
|
|
152
|
+
expect(preview.html.endsWith('<p>[Preview truncated]</p>')).toBe(true);
|
|
153
|
+
expect(previewHtml(preview.html)).toBe(preview.html);
|
|
154
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test';
|
|
2
|
+
import {mermaidToSvg} from '../src/pele-wrapper.js';
|
|
3
|
+
import fixtures from './fixtures/pele-parity.json';
|
|
4
|
+
|
|
5
|
+
// Baseline: the former Flea bundle (SHA-256
|
|
6
|
+
// 6030688a4dc97db8eee282af325343a8de1ae300f862a6f5c9d9175d40c8b765).
|
|
7
|
+
// Store only inputs and output hashes, not a second copy of the renderer.
|
|
8
|
+
// 33 inputs come from Flea's tests/fixtures/markdown-mermaid at 5da918109:
|
|
9
|
+
// Flea/Pele/Mermaid MIT examples; see THIRD-PARTY-NOTICES.md.
|
|
10
|
+
for (const entry of fixtures.cases) {
|
|
11
|
+
test(`Pele preserves legacy output: ${entry.name}`, () => {
|
|
12
|
+
for (const [index, theme] of fixtures.themes.entries()) {
|
|
13
|
+
const expected = entry.results[index];
|
|
14
|
+
if (expected.error) {
|
|
15
|
+
let error;
|
|
16
|
+
try {mermaidToSvg(entry.source, theme);} catch (e) {error = e.message;}
|
|
17
|
+
expect(error).toBe(expected.error);
|
|
18
|
+
} else {
|
|
19
|
+
const svg = mermaidToSvg(entry.source, theme);
|
|
20
|
+
expect(new Bun.CryptoHasher('sha256').update(svg).digest('hex')).toBe(expected.sha256);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
test('Pele is the unmodified official 1.6.0 ESM distribution', async () => {
|
|
26
|
+
const bytes = await Bun.file(new URL('../src/vendor/pele.js', import.meta.url)).arrayBuffer();
|
|
27
|
+
expect(new Bun.CryptoHasher('sha256').update(bytes).digest('hex')).toBe('d1ebc1889f3069d7759fe5db34c4ad4b4823285edbee2b537d0ef3d505a76d8d');
|
|
28
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, posix, win32 } from "node:path";
|
|
5
|
+
import { appPackage, places, probePlaces, rootNames, userDirs } from "../src/places.js";
|
|
6
|
+
|
|
7
|
+
let root, home;
|
|
8
|
+
// Nothing of the machine running the tests is read: every source is named here.
|
|
9
|
+
const quiet = (extra = {}) => ({ home, env: {}, android: false, temps: [], roots: [], mounts: [], ...extra });
|
|
10
|
+
const folder = (...parts) => {
|
|
11
|
+
const path = join(root, ...parts);
|
|
12
|
+
mkdirSync(path, { recursive: true });
|
|
13
|
+
return path;
|
|
14
|
+
};
|
|
15
|
+
const file = (...parts) => {
|
|
16
|
+
folder(...parts.slice(0, -1));
|
|
17
|
+
writeFileSync(join(root, ...parts), "x");
|
|
18
|
+
};
|
|
19
|
+
const labels = (rows) => rows.map((row) => row.label);
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
root = mkdtempSync(join(tmpdir(), "flea-places-"));
|
|
23
|
+
home = folder("home");
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
26
|
+
|
|
27
|
+
test("user-dirs.dirs names a role's folder, the last valid line winning", () => {
|
|
28
|
+
const dirs = userDirs(
|
|
29
|
+
['XDG_DOWNLOAD_DIR="$HOME/Old"', ' XDG_DOWNLOAD_DIR = "$HOME/下載" # inbox', 'XDG_MUSIC_DIR="/srv/music/"', 'XDG_PICTURES_DIR="relative"', 'XDG_VIDEOS_DIR="$HOME/a\\"b"', "XDG_DESKTOP_DIR=$HOME/Desktop"].join("\n"),
|
|
30
|
+
"/home/a",
|
|
31
|
+
);
|
|
32
|
+
expect(Object.fromEntries(dirs)).toEqual({ DOWNLOAD: "/home/a/下載", MUSIC: "/srv/music", VIDEOS: '/home/a/a"b' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("an Android app is read off the temporary directory inside its private folder", () => {
|
|
36
|
+
expect(appPackage("/data/data/com.termux/files/usr/tmp")).toEqual({ name: "com.termux", dir: "/data/data/com.termux" });
|
|
37
|
+
expect(appPackage("/data/user/0/org.example.app_2/cache")).toEqual({ name: "org.example.app_2", dir: "/data/user/0/org.example.app_2" });
|
|
38
|
+
expect(appPackage("/data/user/10/a.b")).toEqual({ name: "a.b", dir: "/data/user/10/a.b" });
|
|
39
|
+
for (const other of ["/tmp", "/data/local/tmp", "/data/data/nodots/tmp", "/data/user/x/com.a.b/tmp", "/data/data/com.1a/tmp", "/sdcard/data/data/com.a.b", "", undefined])
|
|
40
|
+
expect(appPackage(other)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the roots are / where paths are POSIX and the drive letters where they are not", () => {
|
|
44
|
+
expect(rootNames(posix)).toEqual(["/"]);
|
|
45
|
+
const letters = rootNames(win32);
|
|
46
|
+
expect(letters).toContain("C:\\");
|
|
47
|
+
expect(letters.filter((name) => /^[A-Z]:\\$/.test(name)).length).toBe(26);
|
|
48
|
+
expect(letters).not.toContain("/");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a folder is a place only once it directly holds a regular file", async () => {
|
|
52
|
+
file("home", "Documents", "note.txt");
|
|
53
|
+
file("home", "Downloads", "setup.bin");
|
|
54
|
+
folder("home", "Music");
|
|
55
|
+
folder("home", "Pictures", "Album");
|
|
56
|
+
file("home", "Pictures", "Album", "deep.jpg");
|
|
57
|
+
folder("home", "Videos");
|
|
58
|
+
symlinkSync(join(home, "Documents", "note.txt"), join(home, "Videos", "link.txt"));
|
|
59
|
+
const found = await probePlaces(quiet());
|
|
60
|
+
expect(found.places).toEqual([
|
|
61
|
+
{ path: join(home, "Downloads"), label: "Downloads", glyph: "download" },
|
|
62
|
+
{ path: join(home, "Documents"), label: "Documents", glyph: "file-text" },
|
|
63
|
+
]);
|
|
64
|
+
expect(found.volumes).toEqual([]);
|
|
65
|
+
// The first file to land makes the row, and the folder's own change is what is noticed.
|
|
66
|
+
writeFileSync(join(home, "Music", "song.ogg"), "x");
|
|
67
|
+
expect(labels((await probePlaces(quiet())).places)).toEqual(["Downloads", "Documents", "Music"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("user-dirs.dirs is tried before the names, and one folder is one row", async () => {
|
|
71
|
+
file("home", "文件", "a.txt");
|
|
72
|
+
file("home", "Documents", "b.txt");
|
|
73
|
+
file("home", "Movies", "c.mkv");
|
|
74
|
+
file("home", "Shared", "d.txt");
|
|
75
|
+
symlinkSync(join(home, "Movies"), join(home, "Videos"));
|
|
76
|
+
folder("home", ".config");
|
|
77
|
+
writeFileSync(join(home, ".config", "user-dirs.dirs"), 'XDG_DOCUMENTS_DIR="$HOME/文件"\nXDG_PUBLICSHARE_DIR="$HOME/Shared"\nXDG_TEMPLATES_DIR="$HOME"\n');
|
|
78
|
+
const found = await probePlaces(quiet());
|
|
79
|
+
expect(found.places).toEqual([
|
|
80
|
+
{ path: join(home, "文件"), label: "文件", glyph: "file-text" },
|
|
81
|
+
{ path: join(home, "Videos"), label: "Videos", glyph: "film" },
|
|
82
|
+
{ path: join(home, "Shared"), label: "Shared", glyph: "folder" },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("temporary folders are probed and named once each", async () => {
|
|
87
|
+
const a = folder("tmp-a"),
|
|
88
|
+
b = folder("tmp-b");
|
|
89
|
+
symlinkSync(a, join(root, "tmp-link"));
|
|
90
|
+
const found = await probePlaces(quiet({ temps: [a, undefined, join(root, "tmp-link"), join(root, "missing"), "relative", b] }));
|
|
91
|
+
expect(found.places).toEqual([
|
|
92
|
+
{ path: a, label: "Temp", glyph: "folder" },
|
|
93
|
+
{ path: b, label: b, glyph: "folder" },
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("Android always has its shared storage and Download, and the app's own two folders", async () => {
|
|
98
|
+
const shared = join(root, "shared");
|
|
99
|
+
file("shared", "DCIM", "Camera", "IMG_1.jpg");
|
|
100
|
+
file("shared", "DCIM", "stray.jpg");
|
|
101
|
+
file("shared", "Pictures", "Screenshots", "s.png");
|
|
102
|
+
folder("shared", "Documents");
|
|
103
|
+
const app = folder("data", "data", "com.example.app");
|
|
104
|
+
const found = await probePlaces(quiet({ android: true, shared, temps: ["/data/user/0/com.example.app/cache"] }));
|
|
105
|
+
expect(found.places).toEqual([
|
|
106
|
+
{ path: shared, label: "Internal storage", glyph: "smartphone" },
|
|
107
|
+
{ path: join(shared, "Download"), label: "Download", glyph: "download" },
|
|
108
|
+
{ path: join(shared, "DCIM", "Camera"), label: "Camera", glyph: "camera" },
|
|
109
|
+
{ path: join(shared, "Pictures", "Screenshots"), label: "Screenshots", glyph: "image" },
|
|
110
|
+
{ path: join(shared, "Android", "media", "com.example.app"), label: "App media", glyph: "folder" },
|
|
111
|
+
]);
|
|
112
|
+
expect(existsSync(join(shared, "Android", "media", "com.example.app"))).toBe(true);
|
|
113
|
+
expect(existsSync(app)).toBe(true);
|
|
114
|
+
// Without a camera folder holding files, DCIM itself is the camera's row.
|
|
115
|
+
rmSync(join(shared, "DCIM", "Camera"), { recursive: true });
|
|
116
|
+
expect(labels((await probePlaces(quiet({ android: true, shared }))).places)).toEqual(["Internal storage", "Download", "DCIM", "Screenshots"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a root that can be listed is a volume with its size, and one that cannot is not", async () => {
|
|
120
|
+
const found = await probePlaces(quiet({ roots: [root, join(root, "gone"), root + "/"] }));
|
|
121
|
+
expect(found.volumes.length).toBe(1);
|
|
122
|
+
expect(found.volumes[0]).toMatchObject({ path: root, label: root, glyph: "drive" });
|
|
123
|
+
expect(found.volumes[0].size).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the places request answers when asked, and a watch is ended by its own word", async () => {
|
|
127
|
+
file("home", "Documents", "note.txt");
|
|
128
|
+
const lines = [];
|
|
129
|
+
await places({}, (line) => lines.push(line), quiet());
|
|
130
|
+
await places({ watch: true }, (line) => lines.push(line), quiet());
|
|
131
|
+
await places({ watch: false }, (line) => lines.push(line), quiet());
|
|
132
|
+
expect(lines.length).toBe(2);
|
|
133
|
+
expect(lines[0]).toEqual({ t: "places", places: [{ path: join(home, "Documents"), label: "Documents", glyph: "file-text" }], volumes: [] });
|
|
134
|
+
});
|