flea-qml 0.0.0-stage → 0.2.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 +511 -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 +407 -0
- package/src/archiveops.js +728 -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 +135 -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/terminal-server.js +95 -0
- package/src/terminal-shell.js +26 -0
- package/src/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +327 -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 +307 -0
- package/test/file-url.test.js +24 -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-archive-here-ui.mjs +47 -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-folder-focus.mjs +63 -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-terminal-lifecycle.mjs +42 -0
- package/test/manual-terminal-navigation.mjs +48 -0
- package/test/manual-terminal-ui.mjs +47 -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 +95 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchiveHereConfirm.qml +127 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +775 -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 +525 -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 +95 -0
- package/ui/Pane.qml +1320 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +268 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +601 -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 +1005 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +91 -0
- package/ui/PreviewColumn.qml +553 -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 +41 -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 +728 -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/TerminalService.qml +69 -0
- package/ui/TerminalTab.qml +48 -0
- package/ui/TerminalTabs.qml +27 -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 +829 -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 +131 -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 +360 -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 +504 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +314 -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 +751 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +180 -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 +84 -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 +230 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// bun test/manual-terminal-ui.mjs (requires the sibling buninu-browser checkout).
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-terminal-ui-'));
|
|
8
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
10
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
11
|
+
const file=join(repo,'ui/boot/.terminal-check-'+process.pid+'.qml');
|
|
12
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow { id: win; width:900; height:600; Loader {anchors.fill:parent; source:"../WindowBody.qml";onLoaded:{item.host=win;globalThis.testBody=item;globalThis.terminalService=Flea.TerminalService}} }`);
|
|
13
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
14
|
+
const run=s=>browser.context.evaluate(s);
|
|
15
|
+
const until=async(s)=>{for(let i=0;i<200;i++){if(await run(s))return;await Bun.sleep(100);}throw Error('UI timeout '+s+' '+await run('document.body.innerText.slice(-2500)'));};
|
|
16
|
+
try {
|
|
17
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
18
|
+
await until('!!globalThis.testBody && !testBody.currentPane.listInFlight');
|
|
19
|
+
await run('testBody.currentPane.openTerminal()');
|
|
20
|
+
await until('!!testBody.currentPane.terminalId');
|
|
21
|
+
await until('Object.values(terminalService.tabs).some(t=>t.url?.includes("reconnect-token"))');
|
|
22
|
+
const one=await run('Object.values(terminalService.tabs)[0].url');
|
|
23
|
+
await run('testBody.currentPane.openTerminal()');
|
|
24
|
+
await until('Object.values(terminalService.tabs).length===2 && Object.values(terminalService.tabs).every(t=>t.url?.includes("reconnect-token"))');
|
|
25
|
+
const urls=await run('Object.values(terminalService.tabs).map(t=>t.url)');
|
|
26
|
+
assert.equal(new URL(urls[0]).host,new URL(urls[1]).host);assert.notEqual(urls[0],urls[1]);
|
|
27
|
+
assert.equal(await run('testBody.currentPane.tabs.items.length'),3);
|
|
28
|
+
console.log('PASS two terminal tabs, shared listener, distinct sessions',new URL(one).port);
|
|
29
|
+
// Child frame text confirms scripts load; input/PTY tested separately by the wire test.
|
|
30
|
+
await Bun.sleep(12000);
|
|
31
|
+
const target=new URL(urls[1]);
|
|
32
|
+
const monitor=new WebSocket(target.origin+target.pathname.replace(/\/$/,'/ws').replace(/^http/,'ws'),'webtty');
|
|
33
|
+
let output='';
|
|
34
|
+
monitor.onopen=()=>monitor.send(JSON.stringify({AuthToken:'',ReadOnly:true,ReconnectToken:target.searchParams.get('reconnect-token')}));
|
|
35
|
+
monitor.onmessage=e=>{const data=String(e.data);if(data[0]==='1')output+=Buffer.from(data.slice(1),'base64').toString();};
|
|
36
|
+
await Bun.sleep(200);
|
|
37
|
+
assert.equal(await run('document.activeElement.tagName'),'TEXTAREA','new terminal must own keyboard without a click');
|
|
38
|
+
for(const key of "printf '%s%s\\n' FLEA_ QTWEB_OK")await browser.context.press(key);
|
|
39
|
+
await browser.context.press('Enter');
|
|
40
|
+
for(let i=0;i<100&&!output.includes('FLEA_QTWEB_OK');i++)await Bun.sleep(100);
|
|
41
|
+
monitor.close();
|
|
42
|
+
assert(output.includes('FLEA_QTWEB_OK'),'Keyboard did not reach terminal: '+output);
|
|
43
|
+
console.log('PASS keyboard traverses QML WebEngineView to its PTY');
|
|
44
|
+
await Bun.write('/tmp/flea-terminal-tabs.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
45
|
+
console.log('canvas',await run('[...document.querySelectorAll("canvas")].map(c=>({w:c.width,h:c.height,box:c.getBoundingClientRect().toJSON()}))'));
|
|
46
|
+
console.log('frames',await run('[...document.querySelectorAll("iframe")].map(f=>({src:f.src,text:f.contentDocument?.body?.innerText?.slice(-500)}))'));
|
|
47
|
+
}finally{await browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true});}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Run: bun test/manual-trash-ui.mjs (requires ../buninu-browser). Uses temporary fixtures only.
|
|
2
|
+
import {mkdtempSync,mkdirSync,writeFileSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
6
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-trash-ui-'));
|
|
7
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.HOME=dir;process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
8
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
9
|
+
const {trashOne}=await import('../src/trash.js');
|
|
10
|
+
writeFileSync(join(dir,'preview.txt'),'HELLO TRASH PREVIEW\nline two');trashOne(join(dir,'preview.txt'));
|
|
11
|
+
writeFileSync(join(dir,'other.txt'),'SECOND');trashOne(join(dir,'other.txt'));
|
|
12
|
+
mkdirSync(join(dir,'folder'));writeFileSync(join(dir,'folder','child'),'child');trashOne(join(dir,'folder'));
|
|
13
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
14
|
+
const file=join(repo,'ui/boot/.trash-check-'+process.pid+'.qml');
|
|
15
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow { id: win; width:900; height:600; Loader {id:body; anchors.fill:parent; Component.onCompleted:{globalThis.testLoader=body;setSource("${Bun.pathToFileURL(join(repo,"ui/WindowBody.qml")).href}")} onStatusChanged:console.log("TESTLOADER",status) ; onLoaded:{item.host=win;globalThis.testBody=item}} }`);
|
|
16
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
17
|
+
const run=s=>browser.context.evaluate(s);
|
|
18
|
+
try {
|
|
19
|
+
await browser.context.addInitScript(`globalThis.compileErrors=[];const compile=__qmlHost.compile;__qmlHost.compile=(...a)=>{try{return compile(...a)}catch(e){compileErrors.push(String(e));throw e}}`);
|
|
20
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
21
|
+
console.log('loaded',await run('document.body.innerText.slice(0,5000)'));
|
|
22
|
+
for(let i=0;i<30;i++){if(await run('!!globalThis.testBody && !testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
|
|
23
|
+
if(!await run('!!globalThis.testBody'))throw Error('testBody missing '+JSON.stringify(await run('({status:globalThis.testLoader?.status,source:globalThis.testLoader?.source,text:document.body.innerText,errors:globalThis.compileErrors})'))) ;
|
|
24
|
+
console.log('initial',await run('({text:document.body.innerText.slice(0,150),mode:testBody.currentPane.recentMode})'));
|
|
25
|
+
for(const address of ['trash:/','trash://','trash:///']) {
|
|
26
|
+
if(await run('testBody.currentPane.recentMode === "trash"')) {
|
|
27
|
+
await run('testBody.currentPane.closeRecent()');
|
|
28
|
+
for(let i=0;i<150;i++){if(await run('!testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
|
|
29
|
+
}
|
|
30
|
+
await run(`testBody.currentPane.chromeBar.startEdit();testBody.currentPane.chromeBar.pathField.text=${JSON.stringify(address)};testBody.currentPane.chromeBar.commitEdit()`);
|
|
31
|
+
for(let i=0;i<150;i++){if(await run('!testBody.currentPane.listInFlight'))break;await Bun.sleep(100)}
|
|
32
|
+
if(!await run('testBody.currentPane.recentMode === "trash" && testBody.currentPane.path === "/" && testBody.currentPane.chromeBar.path === "trash:///"'))throw Error('Trash address failed: '+address);
|
|
33
|
+
}
|
|
34
|
+
await run('testBody.currentPane.chromeBar.startEdit()');
|
|
35
|
+
if(await run('testBody.currentPane.chromeBar.editText')!=='trash:///')throw Error('Trash edit address acquired an extra slash');
|
|
36
|
+
await run('testBody.currentPane.chromeBar.closeEdit()');
|
|
37
|
+
console.log('PASS all Trash address aliases, display and unchanged backing path');
|
|
38
|
+
const paths=await run('testBody.currentPane.rows.map(r=>r.trash.original)');
|
|
39
|
+
if(paths.length!==3 || paths.some(p=>!p.startsWith(dir+'/')))throw Error('External trash detected; refusing mutation');
|
|
40
|
+
console.log('trash',await run('({text:document.body.innerText,mode:testBody.currentPane.recentMode,rows:testBody.currentPane.total,cursor:testBody.currentPane.cursorRow})'));
|
|
41
|
+
await run('testBody.currentPane.setCursor(testBody.currentPane.rows.findIndex(r=>r.n.endsWith("preview.txt")))');
|
|
42
|
+
if(await run('testBody.currentPane.statusBar.selectedName')!=='preview.txt')throw Error('Trash status filename is not the original name');
|
|
43
|
+
if(!await run('testBody.currentPane.statusBar.fileNameItem.visible && testBody.currentPane.statusBar.fileNameItem.width > 0'))throw Error('Status filename has no visible space');
|
|
44
|
+
console.log('PASS status filename uses the original Trash name');
|
|
45
|
+
// Exercise only the presentation component with a name wider than the window;
|
|
46
|
+
// keep the actual fixture paths and operation identities untouched.
|
|
47
|
+
await run('testBody.currentPane.statusBar.fileNameItem.name="完整檔案名稱-".repeat(40)+".txt"');
|
|
48
|
+
await Bun.sleep(100);
|
|
49
|
+
if(!await run('testBody.currentPane.statusBar.fileNameItem.contentWidth > testBody.currentPane.statusBar.fileNameItem.width'))throw Error('Long status filename cannot scroll');
|
|
50
|
+
await run('testBody.currentPane.statusBar.fileNameItem.contentX=testBody.currentPane.statusBar.fileNameItem.contentWidth-testBody.currentPane.statusBar.fileNameItem.width');
|
|
51
|
+
if(!await run('testBody.currentPane.statusBar.fileNameItem.contentX > 0'))throw Error('Long status filename did not scroll');
|
|
52
|
+
if(process.env.FLEA_STATUS_SCREENSHOT)await Bun.write(process.env.FLEA_STATUS_SCREENSHOT,Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
53
|
+
await run('testBody.currentPane.statusBar.fileNameItem.name=testBody.currentPane.statusBar.fileLabel');
|
|
54
|
+
if(await run('testBody.currentPane.statusBar.fileNameItem.contentX')!==0)throw Error('New filename did not reset scrolling');
|
|
55
|
+
console.log('PASS long status filename scrolling and reset');
|
|
56
|
+
await run('testBody.currentPane.act("preview")');await Bun.sleep(2000);
|
|
57
|
+
if(!await run('testBody.quickLookActive && document.body.innerText.includes("HELLO TRASH PREVIEW")'))throw Error('preview missing');
|
|
58
|
+
if(process.env.FLEA_TRASH_SCREENSHOT)await Bun.write(process.env.FLEA_TRASH_SCREENSHOT,Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
59
|
+
console.log('preview',await run('({active:testBody.quickLookActive,text:document.body.innerText.slice(-900)})'));
|
|
60
|
+
await run('testBody.currentPane.quickLook().close()');
|
|
61
|
+
await run('testBody.currentPane.act("filter");testBody.currentPane.filterQuery="preview"');
|
|
62
|
+
await Bun.sleep(150);
|
|
63
|
+
if(await run('testBody.currentPane.shown.length')!==1)throw Error('filter failed');
|
|
64
|
+
await run('testBody.currentPane.filterQuery="";testBody.currentPane.filterTyping=false');
|
|
65
|
+
const wait=async predicate=>{for(let i=0;i<200;i++){if(await run(predicate))return;await Bun.sleep(50)}throw Error('Timed out: '+predicate)};
|
|
66
|
+
await run('testBody.currentPane.open(testBody.currentPane.join("/",testBody.currentPane.rows.find(r=>r.d).n))');
|
|
67
|
+
await wait('!testBody.currentPane.listInFlight && testBody.currentPane.recentMode===""');
|
|
68
|
+
await run('testBody.currentPane.goBack()');
|
|
69
|
+
await wait('!testBody.currentPane.listInFlight && testBody.currentPane.recentMode==="trash"');
|
|
70
|
+
console.log('PASS filter and directory Back');
|
|
71
|
+
await run('testBody.currentPane.setCursor(testBody.currentPane.rows.findIndex(r=>r.n.endsWith("preview.txt")));testBody.currentPane.trash.action("deletePermanently")');
|
|
72
|
+
await wait('testBody.currentPane.trash.confirmationOpen');
|
|
73
|
+
await run('testBody.currentPane.trash.confirmation.cancel()');
|
|
74
|
+
await wait('!testBody.currentPane.trash.busy');
|
|
75
|
+
await run('testBody.currentPane.trash.action("restoreTrashSelection")');
|
|
76
|
+
await wait('!testBody.currentPane.trash.busy && !testBody.currentPane.listInFlight && testBody.currentPane.total===2');
|
|
77
|
+
if(!await Bun.file(join(dir,'preview.txt')).exists())throw Error('restore missing');
|
|
78
|
+
console.log('PASS cancel deletion and restore selected');
|
|
79
|
+
await run('testBody.currentPane.trash.action("emptyTrash")');
|
|
80
|
+
await wait('testBody.currentPane.trash.confirmationOpen');
|
|
81
|
+
// Opening the review is safe; never activate Empty Trash in a test, because
|
|
82
|
+
// mount-local trashes can exist outside the isolated HOME/XDG directories.
|
|
83
|
+
await run('testBody.currentPane.trash.confirmation.cancel()');
|
|
84
|
+
await wait('!testBody.currentPane.trash.busy');
|
|
85
|
+
console.log('PASS Empty Trash review and cancellation');
|
|
86
|
+
}finally{browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true})}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Mermaid diagrams in the Markdown previews: fences held out of the page and drawn by src/figure.js
|
|
2
|
+
// (Pele in a Bun Worker), embedded as SVG pictures, and their source kept with a reason when not.
|
|
3
|
+
import { afterAll, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { DIAGRAMS_PER_PAGE, drawDiagrams, holdFormulas, markdownHtml, markdownPage } from "../src/markdown-html.js";
|
|
8
|
+
import { figureTheme, renderDiagram, stopFigures } from "../src/figure.js";
|
|
9
|
+
|
|
10
|
+
const folder = mkdtempSync(join(tmpdir(), "flea-diagrams-"));
|
|
11
|
+
afterAll(() => { stopFigures(); rmSync(folder, { recursive: true, force: true }); });
|
|
12
|
+
|
|
13
|
+
const FLOW = "flowchart TD\n A[開始 😀] --> B[結束]";
|
|
14
|
+
const pictures = (html) => [...html.matchAll(/<div class="diagram"><img alt="Mermaid diagram" src="data:image\/svg\+xml;base64,([^"]+)"><\/div>/g)]
|
|
15
|
+
.map((match) => Buffer.from(match[1], "base64").toString());
|
|
16
|
+
|
|
17
|
+
test("Mermaid fences are held as the block view picks them, and other code stays code", () => {
|
|
18
|
+
const source = [
|
|
19
|
+
"```mermaid", "flowchart TD", " A --> B", "```",
|
|
20
|
+
"~~~ Mermaid title", "pie", "~~~",
|
|
21
|
+
" ```mermaid", " sequenceDiagram", " A->>B: hi", " ```",
|
|
22
|
+
"```js", "const x = 1;", "```",
|
|
23
|
+
"```mermaid`", "not a fence opener", "```",
|
|
24
|
+
"$x$ and `$y$`",
|
|
25
|
+
"```mermaid", "flowchart LR", " unclosed --> end",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const { text, diagrams, formulas } = holdFormulas(source);
|
|
28
|
+
expect(diagrams).toEqual(["flowchart TD\n A --> B", "pie", "sequenceDiagram\n A->>B: hi", "flowchart LR\n unclosed --> end"]);
|
|
29
|
+
expect(text).toContain("```js\nconst x = 1;\n```");
|
|
30
|
+
expect(text).not.toContain("flowchart");
|
|
31
|
+
expect(formulas).toEqual([{ tex: "x", display: false }]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a drawn diagram becomes a picture; a failed one keeps its source and says why", () => {
|
|
35
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><text>中文</text></svg>';
|
|
36
|
+
const source = "# T\n\n```mermaid\nA\n```\n\n```mermaid\nB & <b>\n```\n\n```mermaid\nC\n```\n";
|
|
37
|
+
const html = markdownPage(source, folder, {}, [{ svg }, { error: "Parse error <here>" }]);
|
|
38
|
+
expect(pictures(html)).toEqual([svg]);
|
|
39
|
+
expect(html).toContain("<pre><code>B & <b></code></pre><p class=\"diagram-error\">Diagram not drawn · Parse error <here></p>");
|
|
40
|
+
// A page built without drawing (no answer for the fence) shows the plain source.
|
|
41
|
+
expect(html).toContain("<pre><code>C</code></pre>");
|
|
42
|
+
expect(html).not.toMatch(/[]|<p><div|<script/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("diagrams share the pictures' budget", () => {
|
|
46
|
+
const big = `<svg xmlns="http://www.w3.org/2000/svg">${"x".repeat((1 << 20) + 1)}</svg>`;
|
|
47
|
+
const html = markdownPage("```mermaid\nA\n```\n", folder, {}, [{ svg: big }]);
|
|
48
|
+
expect(pictures(html)).toEqual([]);
|
|
49
|
+
expect(html).toContain("Diagram not drawn · over the page's picture budget");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the HTML preview draws Chinese and emoji labels, and the page stays passive", async () => {
|
|
53
|
+
const path = join(folder, "doc.md");
|
|
54
|
+
writeFileSync(path, `# 圖\n\n\`\`\`mermaid\n${FLOW}\n\`\`\`\n\n\`\`\`mermaid\nflowchart TD\n A -->\n\`\`\`\n`);
|
|
55
|
+
const answer = await markdownHtml({ path, token: 3, slot: "s", theme: { foreground: "#c0caf5", background: "#101315" } });
|
|
56
|
+
expect(answer).toMatchObject({ t: "markdownhtml", token: 3, slot: "s", state: "ready" });
|
|
57
|
+
const [svg] = pictures(answer.html);
|
|
58
|
+
expect(svg).toContain("開始 😀");
|
|
59
|
+
expect(svg).toContain("#c0caf5");
|
|
60
|
+
expect(svg).not.toMatch(/<script|<foreignObject|\son\w+=/i);
|
|
61
|
+
expect(answer.html).toMatch(/Diagram not drawn · Parse error on line 3: Expecting/);
|
|
62
|
+
expect(answer.html).not.toMatch(/<script/i);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test(`at most ${DIAGRAMS_PER_PAGE} diagrams are drawn for one page`, async () => {
|
|
66
|
+
const source = Array.from({ length: DIAGRAMS_PER_PAGE + 1 }, () => "```mermaid\nflowchart TD\n A --> B\n```").join("\n\n");
|
|
67
|
+
const drawn = await drawDiagrams(source);
|
|
68
|
+
expect(drawn.slice(0, DIAGRAMS_PER_PAGE).every((answer) => answer.svg)).toBe(true);
|
|
69
|
+
expect(drawn.at(-1)).toEqual({ error: `more than ${DIAGRAMS_PER_PAGE} diagrams on the page` });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("renderDiagram caches, reports errors, and recovers from a deadline", async () => {
|
|
73
|
+
const first = await renderDiagram(FLOW);
|
|
74
|
+
expect(first.svg).toContain("結束");
|
|
75
|
+
expect(await renderDiagram(FLOW)).toBe(first);
|
|
76
|
+
expect((await renderDiagram("flowchart TD\n A -->")).error).toMatch(/^Parse error on line 3: Expecting/);
|
|
77
|
+
expect(await renderDiagram(" ")).toEqual({ error: "empty diagram" });
|
|
78
|
+
expect(await renderDiagram("x".repeat(32769))).toEqual({ error: "diagram over 32 KiB" });
|
|
79
|
+
const slow = "flowchart LR\n" + Array.from({ length: 300 }, (_, i) => ` N${i} --> N${(i * 7 + 3) % 300}`).join("\n");
|
|
80
|
+
expect(await renderDiagram(slow, {}, { timeout: 1 })).toEqual({ error: "diagram took over 0.001 s" });
|
|
81
|
+
// The stuck Worker was terminated; the next diagram starts a fresh one.
|
|
82
|
+
expect((await renderDiagram("sequenceDiagram\n 甲->>乙: 你好")).svg).toContain("你好");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("figureTheme keeps colours, a plain font name and whole advance tables, and dates a Gantt chart", () => {
|
|
86
|
+
const advances = Array.from({ length: 95 }, () => 500);
|
|
87
|
+
const theme = figureTheme({ fg: '"><script>', background: "#101315", font: "Noto Sans CJK TC", advances, boldAdvances: [1, 2] },
|
|
88
|
+
"gantt\n title 計畫", new Date(2026, 10, 15, 23));
|
|
89
|
+
expect(theme).toMatchObject({ fg: "#d8dee9", bg: "#101315", font: "Noto Sans CJK TC", advances, boldAdvances: [] });
|
|
90
|
+
expect(theme.today).toBe(Date.UTC(2026, 10, 15, 12));
|
|
91
|
+
expect(figureTheme({ font: 'x" onload="y' }).font).toBe("sans-serif");
|
|
92
|
+
expect(figureTheme({}, "flowchart TD").today).toBeUndefined();
|
|
93
|
+
});
|
|
@@ -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
|
+
});
|