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.
Files changed (625) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +478 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +402 -0
  51. package/src/archiveops.js +697 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +129 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/transfer.js +669 -0
  82. package/src/trash-record.js +41 -0
  83. package/src/trash.js +210 -0
  84. package/src/trashbrowse.js +326 -0
  85. package/src/uistate.js +393 -0
  86. package/src/uistore.js +168 -0
  87. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  88. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  89. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  90. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  91. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  92. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  93. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  94. package/src/vendor/pele.js +145 -0
  95. package/src/vendor/temml.mjs +14708 -0
  96. package/src/watch.js +98 -0
  97. package/src/wayland/client.js +109 -0
  98. package/src/wayland/protocol.js +55 -0
  99. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  100. package/src/wayland/protocols/wayland.xml +3248 -0
  101. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  102. package/src/wayland/resource-io.js +6 -0
  103. package/src/wayland/socket-discovery.js +26 -0
  104. package/src/wayland/transport-posix.js +120 -0
  105. package/src/wayland/transport-win32.js +11 -0
  106. package/src/wayland/transport.js +3 -0
  107. package/src/wayland/win32-resources.js +80 -0
  108. package/src/wayland/win32-wire.js +62 -0
  109. package/test/archive.test.js +80 -0
  110. package/test/bench-markdown-scroll.mjs +190 -0
  111. package/test/clip-host.test.js +113 -0
  112. package/test/code-preview.test.js +35 -0
  113. package/test/compare-ui-paths.test.js +24 -0
  114. package/test/compare.test.js +54 -0
  115. package/test/delete-trash.test.js +67 -0
  116. package/test/dirsize.test.js +138 -0
  117. package/test/file-ops.test.js +241 -0
  118. package/test/find-contents.test.js +161 -0
  119. package/test/find-input.test.js +66 -0
  120. package/test/fixtures/pele-parity.json +1006 -0
  121. package/test/highlight-launcher.test.js +35 -0
  122. package/test/html-preview.test.js +22 -0
  123. package/test/input.test.js +15 -0
  124. package/test/keymap.test.js +26 -0
  125. package/test/language-icons.test.js +36 -0
  126. package/test/launcher.test.js +51 -0
  127. package/test/manual-code-scroll.mjs +28 -0
  128. package/test/manual-column-scroll.mjs +24 -0
  129. package/test/manual-compare-ui.mjs +20 -0
  130. package/test/manual-find-status.mjs +37 -0
  131. package/test/manual-html-security.mjs +23 -0
  132. package/test/manual-html-ui.mjs +30 -0
  133. package/test/manual-syntax-preview.mjs +26 -0
  134. package/test/manual-trash-ui.mjs +86 -0
  135. package/test/markdown-diagrams.test.js +93 -0
  136. package/test/markdown-html.test.js +105 -0
  137. package/test/mathml.test.js +92 -0
  138. package/test/menu-transfer.test.js +57 -0
  139. package/test/office-preview.test.js +154 -0
  140. package/test/pele-parity.test.js +28 -0
  141. package/test/places.test.js +134 -0
  142. package/test/preview-keys.test.js +60 -0
  143. package/test/preview-language.test.js +22 -0
  144. package/test/status-filename.test.js +20 -0
  145. package/test/transfer-replace-merge.test.js +187 -0
  146. package/test/trash-address.test.js +23 -0
  147. package/test/trash-view.test.js +37 -0
  148. package/test/trashbrowse.test.js +135 -0
  149. package/ui/AboutFacts.qml +78 -0
  150. package/ui/ArchiveCards.qml +68 -0
  151. package/ui/ArchiveCompressDialog.qml +378 -0
  152. package/ui/ArchivePasswordDialog.qml +257 -0
  153. package/ui/Backend.qml +771 -0
  154. package/ui/BulkOldNames.qml +47 -0
  155. package/ui/BulkRenameDialog.qml +380 -0
  156. package/ui/CardFrame.qml +67 -0
  157. package/ui/CardScroll.qml +79 -0
  158. package/ui/CheckBox.qml +73 -0
  159. package/ui/ChromeBar.qml +360 -0
  160. package/ui/ChromeButton.qml +131 -0
  161. package/ui/ChromeModes.qml +43 -0
  162. package/ui/CodeLines.qml +256 -0
  163. package/ui/CodeRefusal.qml +52 -0
  164. package/ui/CollideConfirm.qml +147 -0
  165. package/ui/CollideHost.qml +167 -0
  166. package/ui/ColumnPane.qml +523 -0
  167. package/ui/ColumnRow.qml +380 -0
  168. package/ui/ColumnsArea.qml +594 -0
  169. package/ui/ColumnsLanding.qml +147 -0
  170. package/ui/CompareView.qml +321 -0
  171. package/ui/ContextMenu.qml +743 -0
  172. package/ui/ConvertDialog.qml +456 -0
  173. package/ui/CountedWorker.qml +7 -0
  174. package/ui/Crumb.qml +64 -0
  175. package/ui/CrumbDrop.qml +25 -0
  176. package/ui/CsvTable.qml +394 -0
  177. package/ui/CursorMark.qml +41 -0
  178. package/ui/DefaultClaim.qml +130 -0
  179. package/ui/DeviceLock.qml +47 -0
  180. package/ui/DeviceMounts.qml +540 -0
  181. package/ui/DialogButton.qml +150 -0
  182. package/ui/DialogField.qml +147 -0
  183. package/ui/DialogHeader.qml +31 -0
  184. package/ui/DialogScrim.qml +20 -0
  185. package/ui/DialogTitle.qml +37 -0
  186. package/ui/Divider.qml +9 -0
  187. package/ui/DropInto.qml +126 -0
  188. package/ui/DropTarget.qml +35 -0
  189. package/ui/DropboxMark.qml +60 -0
  190. package/ui/EmptyState.qml +150 -0
  191. package/ui/EscapeHint.qml +11 -0
  192. package/ui/FactsTable.qml +54 -0
  193. package/ui/FastScrollHandler.qml +476 -0
  194. package/ui/FatNamesConfirm.qml +157 -0
  195. package/ui/Favourites.qml +143 -0
  196. package/ui/FigureService.qml +120 -0
  197. package/ui/FileDrag.qml +267 -0
  198. package/ui/FilterStrip.qml +133 -0
  199. package/ui/FitMetrics.qml +22 -0
  200. package/ui/FlashDriveRows.qml +137 -0
  201. package/ui/FlashHost.qml +72 -0
  202. package/ui/FlashImageRow.qml +104 -0
  203. package/ui/FlashRunner.qml +119 -0
  204. package/ui/FlashStepRow.qml +172 -0
  205. package/ui/FlashStrip.qml +38 -0
  206. package/ui/FlashView.qml +397 -0
  207. package/ui/FleaMark.qml +88 -0
  208. package/ui/FlyoutFade.qml +23 -0
  209. package/ui/FontPane.qml +170 -0
  210. package/ui/Glass.qml +114 -0
  211. package/ui/GlassTagger.qml +59 -0
  212. package/ui/Glyph.qml +64 -0
  213. package/ui/GlyphSolid.qml +22 -0
  214. package/ui/GridArea.qml +664 -0
  215. package/ui/GridTile.qml +325 -0
  216. package/ui/GvfsBridge.qml +164 -0
  217. package/ui/Header.qml +433 -0
  218. package/ui/HoverTimers.qml +39 -0
  219. package/ui/HtmlClip.qml +30 -0
  220. package/ui/HtmlPane.qml +63 -0
  221. package/ui/HyprlandMark.qml +45 -0
  222. package/ui/Ipc.qml +1005 -0
  223. package/ui/IpcControls.qml +7 -0
  224. package/ui/IpcMenuState.qml +48 -0
  225. package/ui/IpcPreviewState.qml +185 -0
  226. package/ui/JumpPath.qml +51 -0
  227. package/ui/KeymapSheet.qml +561 -0
  228. package/ui/KeymapSheetResult.qml +89 -0
  229. package/ui/KindStack.qml +40 -0
  230. package/ui/LangMark.qml +51 -0
  231. package/ui/LangMarkLoader.qml +26 -0
  232. package/ui/List.qml +480 -0
  233. package/ui/LoadingState.qml +54 -0
  234. package/ui/LocalSend.qml +56 -0
  235. package/ui/LocalSendMark.qml +73 -0
  236. package/ui/MarkdownBlockView.qml +311 -0
  237. package/ui/MarkdownBlocks.qml +35 -0
  238. package/ui/MarkdownFigure.qml +254 -0
  239. package/ui/MarkdownImages.qml +72 -0
  240. package/ui/MarkdownList.qml +81 -0
  241. package/ui/MarkdownMathsText.qml +97 -0
  242. package/ui/MarkdownPane.qml +83 -0
  243. package/ui/MarkdownPictures.qml +41 -0
  244. package/ui/MarkdownQuote.qml +52 -0
  245. package/ui/MarkdownRemote.qml +82 -0
  246. package/ui/MarkdownSource.qml +155 -0
  247. package/ui/MarkdownTable.qml +222 -0
  248. package/ui/MarkdownTableScroll.qml +64 -0
  249. package/ui/MarkdownTableWheel.qml +126 -0
  250. package/ui/MarkdownText.qml +87 -0
  251. package/ui/MarkdownWorker.js +132 -0
  252. package/ui/MatchText.qml +99 -0
  253. package/ui/MediaSession.qml +38 -0
  254. package/ui/MediaSound.qml +26 -0
  255. package/ui/MediaStrip.qml +191 -0
  256. package/ui/MediaTrack.qml +36 -0
  257. package/ui/MediaVolume.qml +54 -0
  258. package/ui/MenuActionDialog.qml +396 -0
  259. package/ui/MenuEdgeFade.qml +24 -0
  260. package/ui/MenuHost.qml +227 -0
  261. package/ui/MenuRow.qml +351 -0
  262. package/ui/MountListing.qml +100 -0
  263. package/ui/NetworkDialog.qml +435 -0
  264. package/ui/NetworkForm.qml +426 -0
  265. package/ui/NetworkMounts.qml +913 -0
  266. package/ui/NetworkPlaces.qml +127 -0
  267. package/ui/OpenWithDialog.qml +613 -0
  268. package/ui/Opener.qml +115 -0
  269. package/ui/Pane.qml +1296 -0
  270. package/ui/PaneMenuActions.qml +527 -0
  271. package/ui/PanePath.qml +90 -0
  272. package/ui/PaneRail.qml +239 -0
  273. package/ui/PaneStates.qml +88 -0
  274. package/ui/PaneSwap.qml +339 -0
  275. package/ui/PaneWire.qml +605 -0
  276. package/ui/ParkedWait.qml +9 -0
  277. package/ui/PathEntry.qml +186 -0
  278. package/ui/PathJump.qml +400 -0
  279. package/ui/PdfViewer.qml +297 -0
  280. package/ui/PermissionsDialog.qml +658 -0
  281. package/ui/PermissionsHeadings.qml +31 -0
  282. package/ui/PhoneMounts.qml +67 -0
  283. package/ui/PickerAnswers.qml +75 -0
  284. package/ui/PickerBox.qml +26 -0
  285. package/ui/PickerChrome.qml +295 -0
  286. package/ui/PickerFilter.qml +97 -0
  287. package/ui/PickerGrid.qml +308 -0
  288. package/ui/PickerHeader.qml +28 -0
  289. package/ui/PickerLifecycle.qml +27 -0
  290. package/ui/PickerList.qml +218 -0
  291. package/ui/PickerListing.qml +149 -0
  292. package/ui/PickerPathBar.qml +127 -0
  293. package/ui/PickerPlaces.qml +189 -0
  294. package/ui/PickerRecent.qml +92 -0
  295. package/ui/PickerSave.qml +101 -0
  296. package/ui/PickerSelection.qml +155 -0
  297. package/ui/PickerWindow.qml +846 -0
  298. package/ui/PowerSectorsReader.qml +10 -0
  299. package/ui/PrebuildDriver.qml +146 -0
  300. package/ui/Preview.qml +1004 -0
  301. package/ui/PreviewAnimation.qml +24 -0
  302. package/ui/PreviewArchive.qml +66 -0
  303. package/ui/PreviewColumn.qml +551 -0
  304. package/ui/PreviewEdge.qml +63 -0
  305. package/ui/PreviewFacts.qml +30 -0
  306. package/ui/PreviewFactsLine.qml +34 -0
  307. package/ui/PreviewHtml.qml +233 -0
  308. package/ui/PreviewImage.qml +253 -0
  309. package/ui/PreviewImagePan.qml +13 -0
  310. package/ui/PreviewImageSharp.qml +32 -0
  311. package/ui/PreviewLines.qml +106 -0
  312. package/ui/PreviewMarkdown.qml +706 -0
  313. package/ui/PreviewMedia.qml +250 -0
  314. package/ui/PreviewPdf.qml +482 -0
  315. package/ui/PreviewScroll.qml +201 -0
  316. package/ui/PreviewSwap.qml +335 -0
  317. package/ui/PreviewText.qml +149 -0
  318. package/ui/ProtocolChip.qml +18 -0
  319. package/ui/QuickLookArchive.qml +38 -0
  320. package/ui/QuickLookBar.qml +103 -0
  321. package/ui/QuickLookPrepare.qml +355 -0
  322. package/ui/QuickLookSwap.qml +10 -0
  323. package/ui/QuickLookWarm.qml +65 -0
  324. package/ui/RailDrop.qml +155 -0
  325. package/ui/RailHeading.qml +19 -0
  326. package/ui/RailPlaces.qml +38 -0
  327. package/ui/RailTrashRow.qml +67 -0
  328. package/ui/RenameField.qml +353 -0
  329. package/ui/RendererRetry.qml +11 -0
  330. package/ui/ResizeHandle.qml +52 -0
  331. package/ui/Reveal.qml +54 -0
  332. package/ui/Row.qml +476 -0
  333. package/ui/RowDrag.qml +213 -0
  334. package/ui/RowDropTarget.qml +27 -0
  335. package/ui/RunCard.qml +286 -0
  336. package/ui/Scripts.qml +65 -0
  337. package/ui/SearchStrip.qml +232 -0
  338. package/ui/SelectionBand.qml +140 -0
  339. package/ui/SelectionPreview.qml +370 -0
  340. package/ui/SettingsFavourite.qml +122 -0
  341. package/ui/SettingsGroup.qml +96 -0
  342. package/ui/SettingsHint.qml +12 -0
  343. package/ui/SettingsMetrics.qml +29 -0
  344. package/ui/SettingsPane.qml +178 -0
  345. package/ui/SettingsPanel.qml +547 -0
  346. package/ui/SettingsRail.qml +128 -0
  347. package/ui/SettingsRow.qml +409 -0
  348. package/ui/SettingsRuler.qml +85 -0
  349. package/ui/SettingsSegment.qml +85 -0
  350. package/ui/ShareBrowser.qml +113 -0
  351. package/ui/ShareLink.qml +67 -0
  352. package/ui/ShelfPins.qml +101 -0
  353. package/ui/Sidebar.qml +732 -0
  354. package/ui/SidebarRow.qml +358 -0
  355. package/ui/Spinner.qml +60 -0
  356. package/ui/StableModel.qml +89 -0
  357. package/ui/StateMessage.qml +84 -0
  358. package/ui/StatusBar.qml +512 -0
  359. package/ui/StatusFileName.qml +27 -0
  360. package/ui/TabBar.qml +673 -0
  361. package/ui/TabDragGeometry.qml +60 -0
  362. package/ui/TagMenuMark.qml +52 -0
  363. package/ui/TagRailSection.qml +53 -0
  364. package/ui/TagSlot.qml +40 -0
  365. package/ui/TagState.qml +150 -0
  366. package/ui/TagSwatch.qml +28 -0
  367. package/ui/Taildrop.qml +86 -0
  368. package/ui/TailscaleMark.qml +50 -0
  369. package/ui/Theme.qml +460 -0
  370. package/ui/ThemeColors.qml +43 -0
  371. package/ui/TransferCard.qml +239 -0
  372. package/ui/TrashConfirm.qml +129 -0
  373. package/ui/TrashDropHandler.qml +11 -0
  374. package/ui/TrashHost.qml +202 -0
  375. package/ui/TrashMonitor.qml +30 -0
  376. package/ui/TrashToolbar.qml +19 -0
  377. package/ui/TrashView.qml +492 -0
  378. package/ui/UnlockDialog.qml +450 -0
  379. package/ui/UnlockHost.qml +206 -0
  380. package/ui/UpdateCheck.qml +97 -0
  381. package/ui/UserDirs.qml +25 -0
  382. package/ui/ViewState.qml +565 -0
  383. package/ui/ViewportScrollBar.qml +195 -0
  384. package/ui/ViewportScrollBars.qml +25 -0
  385. package/ui/WebPreview.qml +33 -0
  386. package/ui/WebPreviewView.qml +26 -0
  387. package/ui/WindowBody.qml +827 -0
  388. package/ui/WindowDrag.qml +35 -0
  389. package/ui/boot/Commons/AudioNodes.qml +50 -0
  390. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  391. package/ui/boot/Commons/Border.qml +243 -0
  392. package/ui/boot/Commons/BorderGeometry.js +373 -0
  393. package/ui/boot/Commons/Color.qml +258 -0
  394. package/ui/boot/Commons/I18n.qml +118 -0
  395. package/ui/boot/Commons/InputDevices.qml +112 -0
  396. package/ui/boot/Commons/InputMethodState.qml +38 -0
  397. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  398. package/ui/boot/Commons/Numbers.js +19 -0
  399. package/ui/boot/Commons/Plural.js +91 -0
  400. package/ui/boot/Commons/ShellIpc.qml +12 -0
  401. package/ui/boot/Commons/Style.qml +561 -0
  402. package/ui/boot/Commons/Translations.qml +100 -0
  403. package/ui/boot/Commons/UntypedInput.qml +152 -0
  404. package/ui/boot/Commons/Util.qml +188 -0
  405. package/ui/boot/Commons/qmldir +13 -0
  406. package/ui/boot/LICENSE-omarchy +20 -0
  407. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  408. package/ui/boot/Ui/BarIconButton.qml +106 -0
  409. package/ui/boot/Ui/BarIconContent.qml +38 -0
  410. package/ui/boot/Ui/BarIndicator.qml +53 -0
  411. package/ui/boot/Ui/BarWidget.qml +50 -0
  412. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  413. package/ui/boot/Ui/BorderSurface.qml +40 -0
  414. package/ui/boot/Ui/Button.qml +224 -0
  415. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  416. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  417. package/ui/boot/Ui/CursorSurface.qml +42 -0
  418. package/ui/boot/Ui/Dropdown.qml +245 -0
  419. package/ui/boot/Ui/FilterInput.qml +26 -0
  420. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  421. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  422. package/ui/boot/Ui/MultiSelect.qml +624 -0
  423. package/ui/boot/Ui/NumberField.qml +86 -0
  424. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  425. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  426. package/ui/boot/Ui/Panel.qml +60 -0
  427. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  428. package/ui/boot/Ui/PanelController.qml +16 -0
  429. package/ui/boot/Ui/PanelHero.qml +112 -0
  430. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  431. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  432. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  433. package/ui/boot/Ui/PanelSlider.qml +156 -0
  434. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  435. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  436. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  437. package/ui/boot/Ui/PopupCard.qml +181 -0
  438. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  439. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  440. package/ui/boot/Ui/SizedImage.qml +125 -0
  441. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  442. package/ui/boot/Ui/TextField.qml +58 -0
  443. package/ui/boot/Ui/Toggle.qml +118 -0
  444. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  445. package/ui/boot/Ui/WidgetButton.qml +172 -0
  446. package/ui/boot/Ui/qmldir +41 -0
  447. package/ui/boot/fleatab.qml +50 -0
  448. package/ui/boot/picker.qml +39 -0
  449. package/ui/boot/shell.qml +122 -0
  450. package/ui/boot/tabtearoff.qml +51 -0
  451. package/ui/js/Anchor.js +379 -0
  452. package/ui/js/AnchorHold.js +162 -0
  453. package/ui/js/Archive.js +157 -0
  454. package/ui/js/ArchiveCard.js +44 -0
  455. package/ui/js/ArchiveOps.js +44 -0
  456. package/ui/js/BackReturn.js +118 -0
  457. package/ui/js/BulkRename.js +100 -0
  458. package/ui/js/Buttons.js +54 -0
  459. package/ui/js/ChromeKeys.js +116 -0
  460. package/ui/js/ClipMarks.js +73 -0
  461. package/ui/js/ClipSelection.js +67 -0
  462. package/ui/js/Clipboard.js +179 -0
  463. package/ui/js/Cloud.js +97 -0
  464. package/ui/js/CodeRuns.js +72 -0
  465. package/ui/js/Collide.js +212 -0
  466. package/ui/js/ColumnFit.js +28 -0
  467. package/ui/js/ColumnMenu.js +111 -0
  468. package/ui/js/Columns.js +298 -0
  469. package/ui/js/ColumnsCarry.js +103 -0
  470. package/ui/js/Compare.js +74 -0
  471. package/ui/js/Contrast.js +157 -0
  472. package/ui/js/ControlReader.js +42 -0
  473. package/ui/js/Convert.js +52 -0
  474. package/ui/js/CopyAs.js +52 -0
  475. package/ui/js/Crumbs.js +178 -0
  476. package/ui/js/Csv.js +231 -0
  477. package/ui/js/Density.js +44 -0
  478. package/ui/js/Devices.js +440 -0
  479. package/ui/js/DirSizes.js +180 -0
  480. package/ui/js/Drag.js +364 -0
  481. package/ui/js/DragOut.js +166 -0
  482. package/ui/js/DropDev.js +52 -0
  483. package/ui/js/Dropbox.js +41 -0
  484. package/ui/js/Eject.js +217 -0
  485. package/ui/js/Errors.js +188 -0
  486. package/ui/js/ExtThumbs.js +115 -0
  487. package/ui/js/Facts.js +402 -0
  488. package/ui/js/FigureDate.js +14 -0
  489. package/ui/js/Filter.js +285 -0
  490. package/ui/js/Find.js +46 -0
  491. package/ui/js/Flash.js +184 -0
  492. package/ui/js/FlashCard.js +79 -0
  493. package/ui/js/FlashEndings.js +125 -0
  494. package/ui/js/FlashPane.js +116 -0
  495. package/ui/js/FleaBin.js +12 -0
  496. package/ui/js/Focus.js +502 -0
  497. package/ui/js/FolderSorts.js +108 -0
  498. package/ui/js/FolderViews.js +44 -0
  499. package/ui/js/Format.js +305 -0
  500. package/ui/js/Fuzzy.js +141 -0
  501. package/ui/js/GlassTag.js +72 -0
  502. package/ui/js/Grid.js +33 -0
  503. package/ui/js/GridGeometry.js +90 -0
  504. package/ui/js/GridNames.js +153 -0
  505. package/ui/js/GvfsBridge.js +200 -0
  506. package/ui/js/HostClipboard.js +29 -0
  507. package/ui/js/Icons.js +218 -0
  508. package/ui/js/Input.js +50 -0
  509. package/ui/js/Jump.js +200 -0
  510. package/ui/js/KeyName.js +64 -0
  511. package/ui/js/Keymap.js +401 -0
  512. package/ui/js/Kinds.js +164 -0
  513. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  514. package/ui/js/LangMarkPaths.js +47 -0
  515. package/ui/js/LangMarks.js +120 -0
  516. package/ui/js/Lists.js +11 -0
  517. package/ui/js/LocalSend.js +39 -0
  518. package/ui/js/LockedMenu.js +63 -0
  519. package/ui/js/MakeDefault.js +186 -0
  520. package/ui/js/MarkBox.js +7 -0
  521. package/ui/js/Markdown.js +119 -0
  522. package/ui/js/MarkdownLists.js +86 -0
  523. package/ui/js/MarkdownMaths.js +58 -0
  524. package/ui/js/MarkdownPictures.js +32 -0
  525. package/ui/js/MarkdownPrepared.js +139 -0
  526. package/ui/js/MarkdownTableFit.js +34 -0
  527. package/ui/js/Marks.js +148 -0
  528. package/ui/js/Marquee.js +96 -0
  529. package/ui/js/Match.js +93 -0
  530. package/ui/js/Material.js +109 -0
  531. package/ui/js/MdBlocks.js +437 -0
  532. package/ui/js/MdBreak.js +71 -0
  533. package/ui/js/MdChunks.js +70 -0
  534. package/ui/js/MdContainer.js +137 -0
  535. package/ui/js/MdDocument.js +337 -0
  536. package/ui/js/MdEmph.js +97 -0
  537. package/ui/js/MdEntity.js +108 -0
  538. package/ui/js/MdEntityTable.js +223 -0
  539. package/ui/js/MdEscape.js +59 -0
  540. package/ui/js/MdFront.js +37 -0
  541. package/ui/js/MdHold.js +32 -0
  542. package/ui/js/MdHtml.js +307 -0
  543. package/ui/js/MdHtmlBlock.js +290 -0
  544. package/ui/js/MdHtmlHeading.js +53 -0
  545. package/ui/js/MdHtmlImage.js +179 -0
  546. package/ui/js/MdInline.js +154 -0
  547. package/ui/js/MdItems.js +212 -0
  548. package/ui/js/MdLeaf.js +309 -0
  549. package/ui/js/MdLink.js +196 -0
  550. package/ui/js/MdMath.js +67 -0
  551. package/ui/js/MdParagraphs.js +138 -0
  552. package/ui/js/MdRefs.js +232 -0
  553. package/ui/js/MdResolve.js +226 -0
  554. package/ui/js/MdRun.js +300 -0
  555. package/ui/js/MdUrl.js +226 -0
  556. package/ui/js/Menu.js +514 -0
  557. package/ui/js/MenuContext.js +85 -0
  558. package/ui/js/MenuFit.js +13 -0
  559. package/ui/js/MenuRefresh.js +74 -0
  560. package/ui/js/MenuWheel.js +35 -0
  561. package/ui/js/Messages.js +186 -0
  562. package/ui/js/Motion.js +16 -0
  563. package/ui/js/Mounts.js +320 -0
  564. package/ui/js/MouseNav.js +13 -0
  565. package/ui/js/Nav.js +329 -0
  566. package/ui/js/NetFs.js +26 -0
  567. package/ui/js/OpenWith.js +83 -0
  568. package/ui/js/Ops.js +585 -0
  569. package/ui/js/Palette.js +33 -0
  570. package/ui/js/PathBar.js +260 -0
  571. package/ui/js/Permissions.js +179 -0
  572. package/ui/js/Phones.js +112 -0
  573. package/ui/js/Picker.js +353 -0
  574. package/ui/js/PlaceMenu.js +96 -0
  575. package/ui/js/Places.js +265 -0
  576. package/ui/js/PreviewAsk.js +21 -0
  577. package/ui/js/PreviewKeys.js +155 -0
  578. package/ui/js/PreviewSettle.js +29 -0
  579. package/ui/js/PreviewSwap.js +101 -0
  580. package/ui/js/PropertyFacts.js +144 -0
  581. package/ui/js/Protocols.js +296 -0
  582. package/ui/js/QuickLookKinds.js +27 -0
  583. package/ui/js/QuickLookRect.js +24 -0
  584. package/ui/js/RailDrop.js +51 -0
  585. package/ui/js/RailKeys.js +87 -0
  586. package/ui/js/RailMenu.js +133 -0
  587. package/ui/js/Recent.js +135 -0
  588. package/ui/js/RecentMode.js +156 -0
  589. package/ui/js/Reload.js +60 -0
  590. package/ui/js/Renderer.js +9 -0
  591. package/ui/js/RunFile.js +143 -0
  592. package/ui/js/Scripts.js +28 -0
  593. package/ui/js/Scroll.js +304 -0
  594. package/ui/js/ScrollOff.js +69 -0
  595. package/ui/js/Search.js +478 -0
  596. package/ui/js/Selection.js +97 -0
  597. package/ui/js/Settings.js +476 -0
  598. package/ui/js/SettingsAbout.js +97 -0
  599. package/ui/js/SettingsShelf.js +52 -0
  600. package/ui/js/SheetKeys.js +42 -0
  601. package/ui/js/SheetQuery.js +281 -0
  602. package/ui/js/ShelfPile.js +27 -0
  603. package/ui/js/SlowClick.js +53 -0
  604. package/ui/js/SlowOp.js +6 -0
  605. package/ui/js/Sort.js +134 -0
  606. package/ui/js/Startup.js +63 -0
  607. package/ui/js/StateWords.js +17 -0
  608. package/ui/js/Status.js +238 -0
  609. package/ui/js/Swap.js +166 -0
  610. package/ui/js/TabMove.js +39 -0
  611. package/ui/js/Tabs.js +736 -0
  612. package/ui/js/Tags.js +134 -0
  613. package/ui/js/Taildrop.js +128 -0
  614. package/ui/js/Tap.js +174 -0
  615. package/ui/js/TextSize.js +107 -0
  616. package/ui/js/Thumbs.js +158 -0
  617. package/ui/js/Transfer.js +172 -0
  618. package/ui/js/Trash.js +81 -0
  619. package/ui/js/TrashDates.js +47 -0
  620. package/ui/js/TrashDrop.js +64 -0
  621. package/ui/js/UiState.js +438 -0
  622. package/ui/js/Unlock.js +152 -0
  623. package/ui/js/Update.js +130 -0
  624. package/ui/js/UserDirs.js +56 -0
  625. package/ui/qmldir +228 -0
package/src/listing.js ADDED
@@ -0,0 +1,130 @@
1
+ // One directory's rows: scanned for names first, stat'd only for the windows a client reads
2
+ // (docs/protocol.md "list", "window", "rows"). Upstream: src/backend/listing.rs.
3
+ import { accessSync, constants, lstatSync, readdirSync, readlinkSync, statSync } from "node:fs";
4
+ import { join, basename } from "node:path";
5
+ import { describeKind } from "./kinds.js";
6
+ import { NEEDS_STAT, sortRows } from "./order.js";
7
+ import { keptSize } from "./dirsize.js";
8
+
9
+ const S_IFMT = 0o170000,
10
+ S_IFLNK = 0o120000;
11
+
12
+ export class Listing {
13
+ constructor(path, rows) {
14
+ this.path = path;
15
+ this.rows = rows;
16
+ }
17
+
18
+ /** Names and the directory bit only; dotfiles never enter the listing unless `hidden`. */
19
+ static scan(path, hidden) {
20
+ const rows = [];
21
+ for (const entry of readdirSync(path || ".", { withFileTypes: true })) {
22
+ if (!hidden && entry.name.startsWith(".")) continue;
23
+ // `d` is the link's own type, so a symlink to a directory is not a directory row.
24
+ rows.push({ name: entry.name, dir: entry.isDirectory(), link: entry.isSymbolicLink() });
25
+ }
26
+ return new Listing(path, rows);
27
+ }
28
+
29
+ /** Reorders the rows, returning [read, sort] in milliseconds as `listed` reports them. */
30
+ sort(order) {
31
+ const started = performance.now();
32
+ if (NEEDS_STAT.has(order.by)) for (const row of this.rows) this.stat(row);
33
+ if (order.by === "size")
34
+ for (const row of this.rows) row.measured = row.dir && !row.trash ? keptSize(join(this.path || ".", row.name))?.bytes : undefined;
35
+ // Kind ordering compares the name's class alone, so it reads no file and pays no stat.
36
+ if (order.by === "kind" || order.groupByKind)
37
+ for (const row of this.rows) row.group ??= describeKind(row.trash ? basename(row.trash.original || row.name) : row.name, row.dir, 0, undefined).group;
38
+ const sorting = performance.now();
39
+ sortRows(this.rows, order);
40
+ return [sorting - started, performance.now() - sorting];
41
+ }
42
+
43
+ stat(row) {
44
+ if (row.st === undefined) {
45
+ try {
46
+ row.st = lstatSync((row.trash?.file ?? join(this.path, row.name)));
47
+ } catch {
48
+ row.st = null;
49
+ }
50
+ }
51
+ return row.st;
52
+ }
53
+
54
+ /** One `rows` line's body for rows start..start+count, clamped to the listing. */
55
+ window(start, count) {
56
+ const started = performance.now();
57
+ start = Math.min(Math.max(0, start | 0), this.rows.length);
58
+ const end = Math.min(this.rows.length, start + Math.max(0, count | 0));
59
+ const kinds = [],
60
+ kindIndex = new Map(),
61
+ out = [];
62
+ for (let index = start; index < end; index++) out.push(this.describe(this.rows[index], kinds, kindIndex));
63
+ return { start, rows: out, kinds, ms: performance.now() - started };
64
+ }
65
+
66
+ describe(row, kinds, kindIndex) {
67
+ const st = this.stat(row);
68
+ // A failed stat reports s, m and p all 0; p 0 is outside st_mode's domain, so it says so.
69
+ const mode = st?.mode ?? 0;
70
+ let link, target;
71
+ if ((mode & S_IFMT) === S_IFLNK) {
72
+ try {
73
+ link = readlinkSync((row.trash?.file ?? join(this.path, row.name)));
74
+ } catch {}
75
+ // Only a link pays the second stat: it tells a working link from a broken one.
76
+ try {
77
+ target = statSync((row.trash?.file ?? join(this.path, row.name)));
78
+ } catch {
79
+ target = null;
80
+ }
81
+ }
82
+ const { icon, kind } = describeKind(row.trash ? basename(row.trash.original || row.name) : row.name, row.dir, mode, target);
83
+ if (!kindIndex.has(kind)) {
84
+ kindIndex.set(kind, kinds.length);
85
+ kinds.push(kind);
86
+ }
87
+ const out = {
88
+ n: row.name,
89
+ d: row.dir,
90
+ s: st ? st.size : 0,
91
+ m: st ? Math.floor(st.mtimeMs / 1000) : 0,
92
+ p: mode,
93
+ i: icon,
94
+ t: false,
95
+ k: kindIndex.get(kind),
96
+ };
97
+ if (row.trash) out.trash = {name: basename(row.trash.original || row.name), original: row.trash.original, deleted: row.trash.deleted, identity: row.trash.identity};
98
+ if (link !== undefined) out.l = link;
99
+ // A content search's matched line, drawn under the name in its results.
100
+ if (row.match) out.g = row.match;
101
+ if (row.dir) out.v = st ? st.dev : 0;
102
+ // A folder the user already measured keeps its size on every later row: z, and zp for a floor.
103
+ if (row.dir && !row.trash) {
104
+ const kept = keptSize(join(this.path || ".", row.name));
105
+ if (kept) {
106
+ out.z = kept.bytes;
107
+ if (kept.partial) out.zp = true;
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ indexOfName(name) {
114
+ return this.rows.findIndex((row) => row.name === name);
115
+ }
116
+ }
117
+
118
+ /** The listed directory's own device id and whether this user may add or remove entries. */
119
+ export function directoryFacts(path) {
120
+ let v = 0,
121
+ w = false;
122
+ try {
123
+ v = statSync(path || ".").dev;
124
+ } catch {}
125
+ try {
126
+ accessSync(path || ".", constants.W_OK | constants.X_OK);
127
+ w = true;
128
+ } catch {}
129
+ return { v, w };
130
+ }
@@ -0,0 +1,317 @@
1
+ // Markdown to one passive HTML page, for a preview shown in a sandboxed web view
2
+ // (WebEngineView: buninu-browser's iframe, or Qt WebEngine). Bun.markdown.html writes the
3
+ // HTML; nothing it writes is used as it comes. Formulas are set aside first and set by
4
+ // src/mathml.js; the rest is parsed again and rebuilt from the same allowlist the QML
5
+ // preview applies to raw HTML (ui/js/MdHtml.js, ui/js/MdUrl.js): those tags, those
6
+ // attributes, http(s)/mailto/relative links, and only pictures inside the document's
7
+ // folder, which are embedded as data: URLs so the page loads nothing. A remote picture is
8
+ // a note naming its host. Mermaid fences are drawn by src/figure.js and embedded as SVG
9
+ // pictures (data: URLs in <img>, where an SVG runs nothing and loads nothing), sharing the
10
+ // pictures' budget. The page carries no script and no resource reference.
11
+ import { readFileSync, realpathSync, statSync } from "node:fs";
12
+ import { dirname, extname, resolve, sep } from "node:path";
13
+ import { texToMathML, cleanMathML } from "./mathml.js";
14
+ import { renderDiagram } from "./figure.js";
15
+
16
+ /** The largest Markdown source converted, as the QML preview's maxBytes default. */
17
+ export const MARKDOWN_LIMIT = 1 << 20;
18
+ // Qt WebEngine's loadHtml shows nothing past 2 MB (it loads a data: URL), so embedded
19
+ // pictures share 1 MB beside the at most 1 MB of text.
20
+ const IMAGE_LIMIT = 1 << 20;
21
+ const IMAGES_TOTAL = 1 << 20;
22
+ // Diagrams drawn for one page; past this a fence stays source, so a page of broken diagrams
23
+ // cannot hold the backend for a deadline each.
24
+ export const DIAGRAMS_PER_PAGE = 32;
25
+
26
+ // Tags re-emitted with the attributes below; any other tag is dropped and its content kept.
27
+ const ALLOWED = new Set(("a b strong i em u s del strike sub sup kbd code br small mark span p div h1 h2 h3 h4 h5 h6 hr " +
28
+ "img picture source details summary table thead tbody tr th td ul ol li blockquote pre font input").split(" "));
29
+ // Tags dropped with their content: active content, foreign namespaces (formulas come back
30
+ // through their own path), and the elements whose content the parser keeps as raw text,
31
+ // which would turn back into markup if only the tag were dropped.
32
+ const DROP_CONTENT = new Set(("script style iframe object embed template noscript svg math textarea title xmp " +
33
+ "plaintext noembed noframes select option head").split(" "));
34
+ const VOID = new Set(["br", "hr", "img", "source", "input"]);
35
+ const GLOBAL_ATTRIBUTES = new Set(["align", "alt", "width", "height", "title", "colspan", "rowspan"]);
36
+ const STRIKE_AS = { del: "s", strike: "s" };
37
+ const IMAGE_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp", ".avif": "image/avif" };
38
+
39
+ const escapeText = (text) => text.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
40
+ const escapeAttribute = (value) => String(value).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
41
+
42
+ // Entities, percent escapes and control characters undone until the text settles, as
43
+ // MdUrl.targetAllowed decodes before it judges a scheme; a target still changing after
44
+ // eight passes is refused.
45
+ function decodedTarget(raw) {
46
+ let seen = String(raw);
47
+ for (let pass = 0; pass < 8; pass++) {
48
+ let next = seen.replace(/&(#x[0-9a-f]+|#\d+|colon|tab|newline|amp|lt|gt|quot|apos);?/gi, (all, name) => {
49
+ const lower = name.toLowerCase();
50
+ if (lower === "colon") return ":";
51
+ if (lower === "tab") return "\t";
52
+ if (lower === "newline") return "\n";
53
+ if (lower[0] === "#") {
54
+ const code = lower[1] === "x" ? parseInt(lower.slice(2), 16) : parseInt(lower.slice(1), 10);
55
+ return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : "";
56
+ }
57
+ return { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[lower];
58
+ });
59
+ try { next = decodeURIComponent(next); } catch {}
60
+ next = next.replace(/[\u0000- \u007f-\u009f]/g, "");
61
+ if (next === seen) return next;
62
+ seen = next;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /** Whether a link target may stay: http(s), mailto, or no scheme (relative, #fragment). */
68
+ export function linkAllowed(raw) {
69
+ const target = decodedTarget(raw);
70
+ if (target === null) return false;
71
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(target)?.[1]?.toLowerCase();
72
+ return !scheme || scheme === "http" || scheme === "https" || scheme === "mailto";
73
+ }
74
+
75
+ const isRemote = (url) => /^(https?:)?\/\//i.test(url);
76
+ const hostOf = (url) => {
77
+ const rest = url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/^\/\//, "");
78
+ const host = rest.split(/[/?#]/)[0].split("@").pop();
79
+ return (host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0]) || url;
80
+ };
81
+
82
+ /**
83
+ * A picture's source: "remote" with its host, "local" with a data: URL for a file inside
84
+ * `folder` (symbolic links resolved, so none leads out), or "dropped". Every other scheme,
85
+ * data: included, is dropped.
86
+ */
87
+ export function classifyImage(raw, folder, budget) {
88
+ const url = decodedTarget(raw ?? "");
89
+ if (!url) return { kind: "dropped" };
90
+ if (isRemote(url)) return { kind: "remote", host: hostOf(url) };
91
+ let path;
92
+ if (/^file:/i.test(url)) path = url.replace(/^file:(\/\/)?/i, "");
93
+ else if (/^[a-z][a-z0-9+.-]*:/i.test(url)) return { kind: "dropped" };
94
+ else path = url.split(/[?#]/)[0];
95
+ if (path.includes("\\")) return { kind: "dropped" };
96
+ const type = IMAGE_TYPES[extname(path).toLowerCase()];
97
+ if (!type) return { kind: "dropped" };
98
+ try {
99
+ const root = realpathSync(folder);
100
+ const file = realpathSync(resolve(folder, path.startsWith("/") ? path : `./${path}`));
101
+ if (file !== root && !file.startsWith(root + sep)) return { kind: "dropped" };
102
+ const size = statSync(file).size;
103
+ if (size > IMAGE_LIMIT || size > budget.left) return { kind: "dropped" };
104
+ budget.left -= size;
105
+ return { kind: "local", url: `data:${type};base64,${readFileSync(file).toString("base64")}` };
106
+ } catch {
107
+ return { kind: "dropped" };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Rebuilds HTML from the allowlist: allowed tags with their allowed attributes, text
113
+ * escaped, everything else dropped (DROP_CONTENT with its content, other tags keeping
114
+ * theirs). Nothing of the source survives as markup except what is emitted here.
115
+ */
116
+ export function cleanHtml(source, folder, budget = { left: IMAGES_TOTAL }) {
117
+ const out = [];
118
+ let hidden = 0;
119
+ new HTMLRewriter().on("*", {
120
+ element(el) {
121
+ const tag = el.tagName.toLowerCase();
122
+ const foreign = el.namespaceURI !== "http://www.w3.org/1999/xhtml";
123
+ if (hidden || DROP_CONTENT.has(tag) || foreign) {
124
+ if (el.canHaveContent) { hidden++; el.onEndTag(() => { hidden--; }); }
125
+ return;
126
+ }
127
+ if (!ALLOWED.has(tag)) return;
128
+ const name = STRIKE_AS[tag] ?? (tag === "font" ? "span" : tag);
129
+ let attributes = "";
130
+ for (const [key, value] of el.attributes) {
131
+ const attribute = key.toLowerCase();
132
+ if (GLOBAL_ATTRIBUTES.has(attribute)) attributes += ` ${attribute}="${escapeAttribute(value)}"`;
133
+ else if (attribute === "href" && tag === "a" && linkAllowed(value)) attributes += ` href="${escapeAttribute(value)}"`;
134
+ // A task list's box: shown, never editable.
135
+ else if (tag === "input" && attribute === "checked") attributes += " checked";
136
+ }
137
+ if (tag === "input") {
138
+ if (String(el.getAttribute("type")).toLowerCase() !== "checkbox") return;
139
+ attributes += ' type="checkbox" disabled';
140
+ }
141
+ if (tag === "img" || tag === "source") {
142
+ if (tag === "source") return; // picture keeps its img; a source set is not followed
143
+ const picked = classifyImage(el.getAttribute("src"), folder, budget);
144
+ if (picked.kind === "remote") {
145
+ out.push(`<span class="remote">Remote image not loaded · ${escapeText(picked.host)}</span>`);
146
+ return;
147
+ }
148
+ if (picked.kind === "dropped") {
149
+ const alt = el.getAttribute("alt");
150
+ if (alt) out.push(`<span class="alt">${escapeText(alt)}</span>`);
151
+ return;
152
+ }
153
+ attributes += ` src="${picked.url}"`;
154
+ }
155
+ out.push(`<${name}${attributes}>`);
156
+ if (!VOID.has(tag) && el.canHaveContent) el.onEndTag(() => out.push(`</${name}>`));
157
+ },
158
+ }).onDocument({
159
+ text(chunk) {
160
+ if (!hidden) out.push(escapeText(chunk.text));
161
+ },
162
+ }).transform(String(source));
163
+ return out.join("");
164
+ }
165
+
166
+ // ---- formulas ----
167
+
168
+ // Private-use markers, which Markdown leaves alone (it turns NUL into U+FFFD).
169
+ const OPEN = "", CLOSE = "";
170
+
171
+ /**
172
+ * Sets formulas aside: $$…$$ and $…$ (TeX) and raw <math>…</math> (MathML), outside code
173
+ * fences and code spans; and Mermaid fences (```mermaid, as the block view picks them: the
174
+ * info string's first word, any case), each as a paragraph of its own. Returns the text with
175
+ * markers, the formulas they stand for, and the diagrams' sources.
176
+ */
177
+ export function holdFormulas(source) {
178
+ const formulas = [];
179
+ const diagrams = [];
180
+ const hold = (formula) => `${OPEN}${formulas.push(formula) - 1}${CLOSE}`;
181
+ const lines = String(source).replace(/[]/g, "�").split("\n");
182
+ const kept = [];
183
+ let fence = null;
184
+ let prose = [];
185
+ const flush = () => {
186
+ if (!prose.length) return;
187
+ let text = prose.join("\n");
188
+ prose = [];
189
+ // Code spans are kept whole: a run of backticks up to the same run.
190
+ const spans = [];
191
+ text = text.replace(/(`+)[\s\S]*?\1/g, (span) => `${spans.push(span) - 1}`);
192
+ text = text
193
+ .replace(/<math\b[\s\S]*?<\/math\s*>/gi, (markup) => hold({ markup }))
194
+ .replace(/\$\$([\s\S]+?)\$\$/g, (_, tex) => hold({ tex, display: true }))
195
+ // Inline TeX as pandoc reads it: no space just inside either $, and no digit right
196
+ // after the closing one, so "$5 and $10" stays prose.
197
+ .replace(/(^|[^\\$])\$(?=\S)([^$\n]*?\S)\$(?!\d)/g, (_, before, tex) => before + hold({ tex, display: false }));
198
+ kept.push(text.replace(/(\d+)/g, (_, index) => spans[Number(index)]));
199
+ };
200
+ // A diagram's lines, with the fence's own indent taken off each, as CommonMark reads code.
201
+ let diagram = null;
202
+ const holdDiagram = () => {
203
+ kept.push("", `${OPEN}d${diagrams.push(diagram.lines.join("\n")) - 1}${CLOSE}`, "");
204
+ diagram = null;
205
+ };
206
+ for (const line of lines) {
207
+ const opening = /^(\s{0,3})(`{3,}|~{3,})(.*)$/.exec(line);
208
+ const marker = opening?.[2];
209
+ if (diagram) {
210
+ if (marker && marker[0] === diagram.fence[0] && marker.length >= diagram.fence.length && !opening[3].trim()) holdDiagram();
211
+ else diagram.lines.push(line.replace(new RegExp(`^ {0,${diagram.indent}}`), ""));
212
+ } else if (fence) {
213
+ kept.push(line);
214
+ if (marker && marker[0] === fence[0] && marker.length >= fence.length && !line.trim().slice(marker.length).trim()) fence = null;
215
+ } else if (marker) {
216
+ flush();
217
+ // A backtick fence's info string holds no backtick (CommonMark), so ```mermaid` is not one.
218
+ const info = opening[3].trim();
219
+ if (/^mermaid(\s|$)/i.test(info) && !(marker[0] === "`" && info.includes("`")))
220
+ diagram = { fence: marker, indent: opening[1].length, lines: [] };
221
+ else {
222
+ fence = marker;
223
+ kept.push(line);
224
+ }
225
+ } else prose.push(line);
226
+ }
227
+ flush();
228
+ // An unclosed fence runs to the end, as an unclosed code block does.
229
+ if (diagram) holdDiagram();
230
+ return { text: kept.join("\n"), formulas, diagrams };
231
+ }
232
+
233
+ // A formula's MathML, or its source as escaped text when it is refused.
234
+ function formulaHtml({ tex, display, markup }) {
235
+ try {
236
+ return markup !== undefined ? cleanMathML(markup) : texToMathML(tex, display);
237
+ } catch {
238
+ const source = markup ?? (display ? `$$${tex}$$` : `$${tex}$`);
239
+ return `<code class="formula-source">${Bun.escapeHTML(source)}</code>`;
240
+ }
241
+ }
242
+
243
+ // ---- the page ----
244
+
245
+ const HEX = /^#[0-9a-f]{6}$/i;
246
+ const colour = (value, fallback) => (HEX.test(String(value)) ? String(value) : fallback);
247
+
248
+ /** The page's own stylesheet, from the preview's theme colours (each checked as #rrggbb). */
249
+ export function pageStyle(theme = {}) {
250
+ const fg = colour(theme.foreground, "#d8dee9"), bg = colour(theme.background, "#171b22");
251
+ const muted = colour(theme.muted, "#8a93a6"), surface = colour(theme.surface, "#232833"), accent = colour(theme.accent, "#88c0d0");
252
+ const body = Math.max(8, Math.min(40, Number(theme.bodyPx) || 14));
253
+ return `html{background:${bg}}body{margin:12px 16px;color:${fg};background:${bg};font:${body}px sans-serif;line-height:1.7;overflow-wrap:anywhere}` +
254
+ `a{color:${accent}}h1,h2{color:${fg}}h1{font-size:1.45em}h2{font-size:1.2em}h3,h4,h5,h6{font-size:1em}` +
255
+ `pre,code,kbd{font-family:monospace;background:${surface}}code,kbd{padding:0 4px;border-radius:3px}` +
256
+ `pre{padding:8px 12px;white-space:pre-wrap}pre code{padding:0;background:none}` +
257
+ `blockquote{margin:0;padding-left:12px;border-left:3px solid ${muted};color:${muted}}` +
258
+ `table{border-collapse:collapse}td,th{border:1px solid ${muted};padding:2px 8px}hr{border:0;border-top:1px solid ${muted}}` +
259
+ `.remote{display:inline-block;border:1px dashed ${muted};color:${muted};padding:2px 6px}.alt{color:${muted}}` +
260
+ `math[display=block]{display:block;margin:6px 0}.formula-source{color:${muted}}img{max-width:100%}` +
261
+ `.diagram{margin:8px 0}.diagram-error{margin:-4px 0 8px;color:${muted};font-size:.9em}`;
262
+ }
263
+
264
+ // A diagram as a picture, or its source as a code block with the reason it was not drawn.
265
+ // `drawn` is renderDiagram's answer; none (a page built without drawing) leaves the source.
266
+ function diagramHtml(source, drawn, budget) {
267
+ if (drawn?.svg !== undefined) {
268
+ const bytes = Buffer.byteLength(drawn.svg);
269
+ if (bytes <= budget.left) {
270
+ budget.left -= bytes;
271
+ return `<div class="diagram"><img alt="Mermaid diagram" src="data:image/svg+xml;base64,${Buffer.from(drawn.svg).toString("base64")}"></div>`;
272
+ }
273
+ drawn = { error: "over the page's picture budget" };
274
+ }
275
+ const code = `<pre><code>${escapeText(source.replaceAll("&", "&amp;"))}</code></pre>`;
276
+ return drawn?.error ? `${code}<p class="diagram-error">Diagram not drawn · ${escapeText(drawn.error.replaceAll("&", "&amp;"))}</p>` : code;
277
+ }
278
+
279
+ /**
280
+ * One complete, passive page for a Markdown source in `folder`. `drawn` holds renderDiagram's
281
+ * answer for each Mermaid fence in order (see markdownHtml); a fence without one stays source.
282
+ */
283
+ export function markdownPage(source, folder, theme = {}, drawn = []) {
284
+ if (Buffer.byteLength(String(source)) > MARKDOWN_LIMIT) throw Error("Markdown preview exceeds size limit");
285
+ const { text, formulas, diagrams } = holdFormulas(source);
286
+ const html = Bun.markdown.html(text, { tables: true, strikethrough: true, tasklists: true });
287
+ // Diagrams take their share of the picture budget first, in document order; pictures the rest.
288
+ const budget = { left: IMAGES_TOTAL };
289
+ const figures = diagrams.map((diagram, index) => diagramHtml(diagram, drawn[index], budget));
290
+ const body = cleanHtml(html, folder, budget)
291
+ .replace(new RegExp(`(?:<p>)?${OPEN}d(\\d+)${CLOSE}(?:</p>)?`, "g"), (_, index) => figures[Number(index)] ?? "")
292
+ .replace(new RegExp(`${OPEN}(\\d+)${CLOSE}`, "g"), (_, index) => formulas[Number(index)] ? formulaHtml(formulas[Number(index)]) : "");
293
+ return `<!doctype html><meta charset="utf-8"><style>${pageStyle(theme)}</style>${body}`;
294
+ }
295
+
296
+ /** Draws a source's Mermaid fences under `theme`, in order, the first DIAGRAMS_PER_PAGE of them. */
297
+ export async function drawDiagrams(source, theme = {}) {
298
+ const { diagrams } = holdFormulas(source);
299
+ return Promise.all(diagrams.map((diagram, index) => index < DIAGRAMS_PER_PAGE
300
+ ? renderDiagram(diagram, theme)
301
+ : Promise.resolve({ error: `more than ${DIAGRAMS_PER_PAGE} diagrams on the page` })));
302
+ }
303
+
304
+ /** The backend's answer to one `markdownhtml` request for the file at `request.path`. */
305
+ export async function markdownHtml(request) {
306
+ const answer = { t: "markdownhtml", slot: request.slot ?? "", token: request.token, state: "failed", html: "" };
307
+ try {
308
+ const path = String(request.path ?? "");
309
+ if (!path || statSync(path).size > MARKDOWN_LIMIT) return answer;
310
+ const source = await Bun.file(path).text();
311
+ const theme = request.theme ?? {};
312
+ const drawn = await drawDiagrams(source, theme);
313
+ return { ...answer, state: "ready", html: markdownPage(source, dirname(resolve(path)), theme, drawn) };
314
+ } catch {
315
+ return answer;
316
+ }
317
+ }
package/src/mathml.js ADDED
@@ -0,0 +1,186 @@
1
+ // TeX to MathML for the Markdown preview: Temml writes MathML, and the result is never
2
+ // used as it comes. It is parsed again and rebuilt from an allowlist, the way
3
+ // src/html.js rebuilds passive HTML: MathML Core elements only, each attribute checked
4
+ // against the values layout reads, and from style only colours, box spacing and a
5
+ // plain border. Nothing that loads, links or runs can reach the output.
6
+ import temml from "./vendor/temml.mjs";
7
+
8
+ /** The longest TeX source converted, as the figure helper's MATH_LIMIT. */
9
+ export const MATH_LIMIT = 4096;
10
+ const OUTPUT_LIMIT = 256 * 1024;
11
+ const DEPTH_LIMIT = 64;
12
+
13
+ // MathML Core elements (https://w3c.github.io/mathml-core/#mathml-elements-and-attributes)
14
+ // and menclose, which Temml writes for \boxed and \overline.
15
+ const ELEMENTS = new Set(("math mrow mi mn mo ms mtext mspace mfrac msqrt mroot msub msup msubsup munder mover " +
16
+ "munderover mmultiscripts mprescripts none mtable mtr mtd mstyle mpadded mphantom menclose merror semantics").split(" "));
17
+ // Dropped with everything inside them.
18
+ const DROP = new Set(["annotation", "annotation-xml"]);
19
+
20
+ const BOOLEAN = /^(?:true|false)$/i;
21
+ const NUMBER = "[+-]?(?:\\d{1,4}(?:\\.\\d{1,6})?|\\.\\d{1,6})";
22
+ const LENGTH = new RegExp(`^\\s*(${NUMBER})\\s*(em|ex|px|pt|%)?\\s*$`, "i");
23
+ // The largest length kept, per unit: a huge space or rule is a way to cover the pane.
24
+ const LENGTH_MAX = { em: 50, ex: 100, px: 1000, pt: 750, "%": 1000, "": 0 };
25
+ const ATTRIBUTES = {
26
+ display: (value) => /^(?:block|inline)$/i.test(value),
27
+ displaystyle: (value) => BOOLEAN.test(value),
28
+ scriptlevel: (value) => /^[+-]?\d{1,2}$/.test(value),
29
+ mathvariant: (value) => /^(?:normal|bold|italic|bold-italic)$/i.test(value),
30
+ form: (value) => /^(?:prefix|infix|postfix)$/i.test(value),
31
+ stretchy: (value) => BOOLEAN.test(value),
32
+ symmetric: (value) => BOOLEAN.test(value),
33
+ largeop: (value) => BOOLEAN.test(value),
34
+ movablelimits: (value) => BOOLEAN.test(value),
35
+ fence: (value) => BOOLEAN.test(value),
36
+ separator: (value) => BOOLEAN.test(value),
37
+ accent: (value) => BOOLEAN.test(value),
38
+ accentunder: (value) => BOOLEAN.test(value),
39
+ lspace: length, rspace: length, width: length, height: length, depth: length, voffset: length,
40
+ minsize: length, maxsize: length, linethickness: length,
41
+ };
42
+
43
+ /** A MathML Core length within the limits above (unitless only as zero). */
44
+ export function length(value) {
45
+ const match = LENGTH.exec(String(value));
46
+ if (!match) return false;
47
+ const unit = (match[2] ?? "").toLowerCase();
48
+ return Math.abs(Number(match[1])) <= LENGTH_MAX[unit];
49
+ }
50
+
51
+ const COLOR = /^(?:#[0-9a-f]{3,4}|#[0-9a-f]{6}|#[0-9a-f]{8}|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*(?:0|1|0?\.\d+)\s*)?\))$/i;
52
+ const BOX_LENGTHS = (value, negative) => {
53
+ const parts = value.trim().split(/\s+/);
54
+ return parts.length >= 1 && parts.length <= 4 && parts.every((part) => length(part) && (negative || !part.trim().startsWith("-")));
55
+ };
56
+ const STYLE = {
57
+ color: (value) => COLOR.test(value),
58
+ "background-color": (value) => COLOR.test(value),
59
+ padding: (value) => BOX_LENGTHS(value, false),
60
+ "padding-top": (value) => BOX_LENGTHS(value, false), "padding-right": (value) => BOX_LENGTHS(value, false),
61
+ "padding-bottom": (value) => BOX_LENGTHS(value, false), "padding-left": (value) => BOX_LENGTHS(value, false),
62
+ "margin-left": (value) => BOX_LENGTHS(value, true), "margin-right": (value) => BOX_LENGTHS(value, true),
63
+ border: (value) => /^\s*(\S+)\s+(?:solid|dashed|dotted)(?:\s+(\S+))?\s*$/i.test(value)
64
+ && length(value.trim().split(/\s+/)[0]) && (value.trim().split(/\s+/)[2] === undefined || COLOR.test(value.trim().split(/\s+/)[2])),
65
+ };
66
+
67
+ /** The allowed declarations of a style attribute, rebuilt; empty when none survive. */
68
+ export function cleanStyle(source) {
69
+ const kept = [];
70
+ for (const declaration of String(source).split(";")) {
71
+ const colon = declaration.indexOf(":");
72
+ if (colon < 0) continue;
73
+ const name = declaration.slice(0, colon).trim().toLowerCase();
74
+ const value = declaration.slice(colon + 1).trim();
75
+ // A comment, escape or function call other than rgb() never survives the value checks,
76
+ // but refuse them up front so the checks only ever see plain values.
77
+ if (!STYLE[name] || /[\\/*"'<>&{}]|url\s*\(|expression|@/i.test(value)) continue;
78
+ if (STYLE[name](value)) kept.push(`${name}:${value}`);
79
+ }
80
+ return kept.join(";");
81
+ }
82
+
83
+ const escapeAttribute = (value) => String(value).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
84
+
85
+ // Entity references a text run or attribute value may hold; anything else that starts
86
+ // with "&" is escaped, so it stays text.
87
+ const ENTITY = /&(?:amp|lt|gt|quot|apos|#\d{1,7}|#x[0-9a-f]{1,6});/iy;
88
+ const NAMED = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
89
+ const decode = (value) => value.replace(/&(amp|lt|gt|quot|apos|#\d{1,7}|#x[0-9a-f]{1,6});/gi, (all, name) => {
90
+ if (name[0] !== "#") return NAMED[name.toLowerCase()];
91
+ const code = name[1].toLowerCase() === "x" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
92
+ return code > 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff) ? String.fromCodePoint(code) : "\ufffd";
93
+ });
94
+ const escapeText = (value) => {
95
+ let out = "";
96
+ for (let index = 0; index < value.length; index++) {
97
+ const character = value[index];
98
+ if (character === "&") {
99
+ ENTITY.lastIndex = index;
100
+ out += ENTITY.test(value) ? "&" : "&amp;";
101
+ } else out += character === ">" ? "&gt;" : character;
102
+ }
103
+ return out;
104
+ };
105
+
106
+ // The markup Temml writes: start tags with double-quoted attributes, "/>" or end tags,
107
+ // and text without "<". Comments, CDATA, doctypes, processing instructions, unquoted
108
+ // or single-quoted attributes and unbalanced tags are refused, not repaired.
109
+ const TOKEN = /<\/([a-z][a-z0-9-]*)\s*>|<([a-z][a-z0-9-]*)((?:\s+[a-z_:][-a-z0-9_:.]*\s*=\s*"[^"<]*")*)\s*(\/?)>|([^<]+)/giy;
110
+ const ATTRIBUTE = /([a-z_:][-a-z0-9_:.]*)\s*=\s*"([^"<]*)"/gi;
111
+
112
+ /**
113
+ * Rebuilds MathML from the allowlist. Whatever is not an allowed MathML element (HTML,
114
+ * SVG, annotations, anything past the depth limit) is dropped with its content; an
115
+ * unknown MathML-looking element keeps its children as an mrow. Throws when the input
116
+ * is not strict markup, or the result is not one <math> element or is too large.
117
+ */
118
+ export function cleanMathML(source) {
119
+ const markup = String(source);
120
+ if (Buffer.byteLength(markup) > OUTPUT_LIMIT) throw Error("MathML exceeds size limit");
121
+ const out = [];
122
+ const stack = []; // {name, kept, output}
123
+ let hidden = 0, roots = 0;
124
+ TOKEN.lastIndex = 0;
125
+ while (TOKEN.lastIndex < markup.length) {
126
+ const at = TOKEN.lastIndex;
127
+ const match = TOKEN.exec(markup);
128
+ if (!match || match.index !== at) throw Error("MathML is not strict markup");
129
+ const [, endName, startName, attributeText, selfClosing, text] = match;
130
+ if (text !== undefined) {
131
+ if (!hidden && stack.length) out.push(escapeText(text));
132
+ continue;
133
+ }
134
+ if (endName !== undefined) {
135
+ const open = stack.pop();
136
+ if (!open || open.name !== endName.toLowerCase()) throw Error("MathML tags are unbalanced");
137
+ if (open.kept) out.push(`</${open.output}>`);
138
+ else if (open.hides) hidden--;
139
+ continue;
140
+ }
141
+ const tag = startName.toLowerCase();
142
+ const depth = stack.length;
143
+ // MathML only: a known element, or one of Temml's own kept as mrow; <math> only at the root.
144
+ const looksMathML = ELEMENTS.has(tag) || /^m[a-z]+$/.test(tag);
145
+ const drop = hidden > 0 || !looksMathML || DROP.has(tag) || depth >= DEPTH_LIMIT || (tag === "math") !== (depth === 0);
146
+ const entry = { name: tag, kept: !drop, hides: drop && !hidden, output: ELEMENTS.has(tag) ? tag : "mrow" };
147
+ if (drop) {
148
+ if (entry.hides) hidden++;
149
+ } else {
150
+ if (tag === "math") roots++;
151
+ let attributes = "";
152
+ for (const [, key, raw] of attributeText.matchAll(ATTRIBUTE)) {
153
+ const attribute = key.toLowerCase();
154
+ const value = decode(raw);
155
+ if (attribute === "style") {
156
+ const style = cleanStyle(value);
157
+ if (style) attributes += ` style="${escapeAttribute(style)}"`;
158
+ } else if (ATTRIBUTES[attribute]?.(value)) attributes += ` ${attribute}="${escapeAttribute(value.trim())}"`;
159
+ }
160
+ out.push(`<${entry.output}${attributes}>`);
161
+ }
162
+ if (selfClosing) {
163
+ if (entry.kept) out.push(`</${entry.output}>`);
164
+ else if (entry.hides) hidden--;
165
+ } else stack.push(entry);
166
+ }
167
+ if (stack.length) throw Error("MathML tags are unbalanced");
168
+ const html = out.join("");
169
+ if (roots !== 1 || !html.startsWith("<math") || !html.endsWith("</math>")) throw Error("not one MathML formula");
170
+ return html;
171
+ }
172
+
173
+ /**
174
+ * MathML for one TeX formula, cleaned. Temml runs with trust off (no \href, \url,
175
+ * \includegraphics or \html*), bounded macro expansion and sizes, and reports a TeX
176
+ * error as an <merror> inside the formula rather than throwing.
177
+ */
178
+ export function texToMathML(tex, display = false) {
179
+ const source = String(tex ?? "");
180
+ if (source.length > MATH_LIMIT) throw Error("formula exceeds size limit");
181
+ const markup = temml.renderToString(source, {
182
+ displayMode: Boolean(display), trust: false, throwOnError: false, maxExpand: 1000, maxSize: 20, strict: false,
183
+ annotate: false, xml: false, wrap: "none",
184
+ });
185
+ return cleanMathML(markup);
186
+ }