buninu-browser 0.3.0 → 0.5.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/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +130 -0
- package/frontends/wayland/input.js +75 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +95 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
package/lib/qml/fonts.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {readdir, realpath, stat} from 'node:fs/promises';
|
|
2
|
+
import {join, resolve} from 'node:path';
|
|
3
|
+
import {registerTypeface} from '../renderer/font-loading.js';
|
|
4
|
+
|
|
5
|
+
// Capture before a host-mode document can replace globals/environment.
|
|
6
|
+
const directory = process.env.QT_QPA_FONTDIR || '';
|
|
7
|
+
const cache = new WeakMap();
|
|
8
|
+
const MAX_FILE = 32 * 1024 * 1024;
|
|
9
|
+
const MAX_TOTAL = 128 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
// QT_QPA_FONTDIR is one directory, not a PATH-style list. Recursion is our
|
|
12
|
+
// extension; canonical paths prevent directory symlink cycles and duplicates.
|
|
13
|
+
export async function qmlFontFiles(root, warn = () => {}) {
|
|
14
|
+
const files = [], seen = new Set();
|
|
15
|
+
async function walk(path, depth = 0) {
|
|
16
|
+
if (depth > 16 || seen.size >= 4096) return;
|
|
17
|
+
try {
|
|
18
|
+
path = await realpath(path);
|
|
19
|
+
if (seen.has(path)) return;
|
|
20
|
+
seen.add(path);
|
|
21
|
+
const info = await stat(path);
|
|
22
|
+
if (info.isDirectory()) {
|
|
23
|
+
for (const name of (await readdir(path)).sort()) await walk(join(path, name), depth + 1);
|
|
24
|
+
} else if (info.isFile() && /\.(ttf|otf)$/i.test(path)) files.push({path, size:info.size});
|
|
25
|
+
} catch (error) { warn(`QML font: ${path}: ${error.message}`); }
|
|
26
|
+
}
|
|
27
|
+
if (root) await walk(resolve(root));
|
|
28
|
+
return files.sort((a,b)=>Number(!/regular/i.test(a.path))-Number(!/regular/i.test(b.path)) || a.path.localeCompare(b.path));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// One private collection per graphics instance, shared by its local QML pages.
|
|
32
|
+
// The ordinary HTML collection and font aliases are never mutated.
|
|
33
|
+
export function loadQmlFonts(base, warn = () => {}, root = directory) {
|
|
34
|
+
if (!root) return Promise.resolve(base);
|
|
35
|
+
let roots = cache.get(base);
|
|
36
|
+
if (!roots) cache.set(base, roots = new Map());
|
|
37
|
+
const key = resolve(root);
|
|
38
|
+
if (!roots.has(key)) roots.set(key, build(base, key, warn));
|
|
39
|
+
return roots.get(key);
|
|
40
|
+
}
|
|
41
|
+
async function build(base, root, warn) {
|
|
42
|
+
const {CanvasKit} = base;
|
|
43
|
+
const extra = [];
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const file of await qmlFontFiles(root, warn)) {
|
|
46
|
+
if (file.size > MAX_FILE || total + file.size > MAX_TOTAL || extra.length >= 128) {
|
|
47
|
+
warn(`QML font: skipped ${file.path}: font loading limit`);continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const bytes = await Bun.file(file.path).bytes();
|
|
51
|
+
if (bytes.length > MAX_FILE || total + bytes.length > MAX_TOTAL) continue;
|
|
52
|
+
const typeface = CanvasKit.Typeface.MakeTypefaceFromData(bytes);
|
|
53
|
+
if (!typeface) throw Error('unsupported or invalid font');
|
|
54
|
+
total += bytes.length;
|
|
55
|
+
extra.push({family:`Buninu QML Fallback ${extra.length}`, name:typeface.getFamilyName(), typeface});
|
|
56
|
+
} catch(error) {warn(`QML font: ${file.path}: ${error.message}`);}
|
|
57
|
+
}
|
|
58
|
+
if (!extra.length) return base;
|
|
59
|
+
const provider = CanvasKit.TypefaceFontProvider.Make();
|
|
60
|
+
const builtinTypefaces = [...base.builtinTypefaces, ...extra];
|
|
61
|
+
for (const face of builtinTypefaces) registerTypeface(CanvasKit, provider, face.typeface, face.family);
|
|
62
|
+
const fontCollection = CanvasKit.FontCollection.Make();
|
|
63
|
+
fontCollection.setDefaultFontManager(provider);
|
|
64
|
+
fontCollection.enableFontFallback();
|
|
65
|
+
const loadedFamilies = new Map(base.loadedFamilies);
|
|
66
|
+
for (const face of extra) if (!loadedFamilies.has(face.name.toLowerCase())) loadedFamilies.set(face.name.toLowerCase(), face.family);
|
|
67
|
+
return {...base, fontCollection, loadedFamilies, builtinTypefaces,
|
|
68
|
+
fontFamilies:[...base.fontFamilies, ...extra.map(face=>face.family)]};
|
|
69
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const QML_MIME = 'text/x-qml';
|
|
2
|
+
export function isQmlDocument(url, contentType = '') {
|
|
3
|
+
const mime = contentType.split(';')[0].trim().toLowerCase();
|
|
4
|
+
if (['text/x-qml', 'application/x-qml'].includes(mime)) return true;
|
|
5
|
+
// An explicit HTML response must retain HTML behavior, even at a .qml URL.
|
|
6
|
+
return ['', 'text/plain', 'application/octet-stream'].includes(mime)
|
|
7
|
+
&& /\.qml$/i.test(new URL(url, 'file:///').pathname);
|
|
8
|
+
}
|
|
9
|
+
|
package/lib/qml/host.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {readClipboard, writeClipboard} from '../clipboard.js';
|
|
2
|
+
import { createConnection } from 'node:net';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { loadQmlGraphSync } from './loader.js';
|
|
5
|
+
import { createQmlDocument } from './document.js';
|
|
6
|
+
// Local QML application capabilities. No imports from this module are exposed to HTML.
|
|
7
|
+
import {
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
unlinkSync,
|
|
12
|
+
statSync,
|
|
13
|
+
watch,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { resolve, delimiter } from 'node:path';
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
|
+
const spawn = Bun.spawn.bind(Bun);
|
|
18
|
+
const hostFetch = globalThis.fetch.bind(globalThis);
|
|
19
|
+
const hostTimeout = AbortSignal.timeout.bind(AbortSignal);
|
|
20
|
+
const environment = { ...process.env };
|
|
21
|
+
const active = new Set();
|
|
22
|
+
let installed = false;
|
|
23
|
+
export function createQmlHost(url) {
|
|
24
|
+
if (new URL(url).protocol !== 'file:')
|
|
25
|
+
throw Error('QML host requires a local document');
|
|
26
|
+
if (!installed) {
|
|
27
|
+
installed = true;
|
|
28
|
+
process.on('exit', () => {
|
|
29
|
+
for (const host of active) host.dispose();
|
|
30
|
+
});
|
|
31
|
+
process.on('SIGTERM', () => {
|
|
32
|
+
for (const host of active) host.dispose();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
let closed = false;
|
|
37
|
+
const resources = new Set(),
|
|
38
|
+
base = new URL('.', url);
|
|
39
|
+
const check = () => {
|
|
40
|
+
if (closed) throw Error('QML host is closed');
|
|
41
|
+
};
|
|
42
|
+
const path = (value) =>
|
|
43
|
+
String(value).startsWith('file:')
|
|
44
|
+
? fileURLToPath(value)
|
|
45
|
+
: resolve(fileURLToPath(base), String(value));
|
|
46
|
+
let clipboardEndpoint;
|
|
47
|
+
const clipboardRequest = async (operation, text) => {
|
|
48
|
+
check();
|
|
49
|
+
if (!clipboardEndpoint) return operation==='read' ? readClipboard() : writeClipboard(text);
|
|
50
|
+
const response=await hostFetch(clipboardEndpoint,{method:'POST',body:JSON.stringify({operation,text}),signal:hostTimeout(4000)});
|
|
51
|
+
if(!response.ok)throw Error('Wayland clipboard unavailable');
|
|
52
|
+
return (await response.json()).text;
|
|
53
|
+
};
|
|
54
|
+
const host = {
|
|
55
|
+
setClipboardEndpoint(endpoint) {
|
|
56
|
+
check(); const u=new URL(endpoint);
|
|
57
|
+
if(u.protocol!=='http:' || u.hostname!=='127.0.0.1')throw Error('Invalid clipboard endpoint');
|
|
58
|
+
clipboardEndpoint=u.href;
|
|
59
|
+
},
|
|
60
|
+
clipboardRead() {return clipboardRequest('read');},
|
|
61
|
+
clipboardWrite(text) {return clipboardRequest('write',String(text));},
|
|
62
|
+
worker(url, onMessage, onReady, onError) {
|
|
63
|
+
check();
|
|
64
|
+
if (new URL(url).protocol !== 'file:')
|
|
65
|
+
throw Error('WorkerScript requires a local source');
|
|
66
|
+
const worker = new Worker(
|
|
67
|
+
new URL('./worker-thread.js', import.meta.url),
|
|
68
|
+
{ workerData: { url } },
|
|
69
|
+
);
|
|
70
|
+
let stopped = false;
|
|
71
|
+
worker.on('message', (data) => {
|
|
72
|
+
if (stopped) return;
|
|
73
|
+
if (data.kind === 'ready') onReady();
|
|
74
|
+
else if (data.kind === 'message') onMessage(data.message);
|
|
75
|
+
else if (data.kind === 'error') onError(data.error);
|
|
76
|
+
});
|
|
77
|
+
worker.on('error', (error) => {
|
|
78
|
+
if (!stopped) onError(String(error.stack || error));
|
|
79
|
+
});
|
|
80
|
+
const stop = () => {
|
|
81
|
+
if (stopped) return;
|
|
82
|
+
stopped = true;
|
|
83
|
+
resources.delete(stop);
|
|
84
|
+
void worker.terminate();
|
|
85
|
+
};
|
|
86
|
+
resources.add(stop);
|
|
87
|
+
stop.send = (message) => {
|
|
88
|
+
if (stopped) throw Error('WorkerScript has stopped');
|
|
89
|
+
worker.postMessage(message);
|
|
90
|
+
};
|
|
91
|
+
return stop;
|
|
92
|
+
},
|
|
93
|
+
markdown: (source) => {
|
|
94
|
+
check();
|
|
95
|
+
return Bun.markdown.html(String(source));
|
|
96
|
+
},
|
|
97
|
+
shellDir: fileURLToPath(base).replace(/\/$/, ''),
|
|
98
|
+
processId: process.pid,
|
|
99
|
+
compile(href, externalSingletons = []) {
|
|
100
|
+
check();
|
|
101
|
+
if (new URL(href).protocol !== 'file:')
|
|
102
|
+
throw Error('Synchronous application components must be local files');
|
|
103
|
+
const source = readFileSync(fileURLToPath(href), 'utf8');
|
|
104
|
+
const fetchResource = (url) => {
|
|
105
|
+
if (new URL(url).protocol !== 'file:')
|
|
106
|
+
throw Error('Local component imports must be local files');
|
|
107
|
+
try {
|
|
108
|
+
return { status: 200, body: readFileSync(fileURLToPath(url)) };
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e.code === 'ENOENT' || e.code === 'ENOTDIR')
|
|
111
|
+
return { status: 404 };
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const graph = loadQmlGraphSync(source, href, fetchResource, {
|
|
116
|
+
importRoots: host.importRoots,
|
|
117
|
+
});
|
|
118
|
+
const { document } = createQmlDocument(source, href, {
|
|
119
|
+
graph,
|
|
120
|
+
component: true,
|
|
121
|
+
externalSingletons,
|
|
122
|
+
});
|
|
123
|
+
const program = document.querySelector('script')?.textContent;
|
|
124
|
+
if (!program)
|
|
125
|
+
throw Error(
|
|
126
|
+
document.querySelector('p')?.textContent ||
|
|
127
|
+
'Component compilation failed',
|
|
128
|
+
);
|
|
129
|
+
return program;
|
|
130
|
+
},
|
|
131
|
+
env(name) {
|
|
132
|
+
check();
|
|
133
|
+
return environment[String(name)] ?? null;
|
|
134
|
+
},
|
|
135
|
+
hyprEvents(callback) {
|
|
136
|
+
const signature = environment.HYPRLAND_INSTANCE_SIGNATURE;
|
|
137
|
+
if (!signature) return () => {};
|
|
138
|
+
const socketPath = resolve(
|
|
139
|
+
environment.XDG_RUNTIME_DIR || '/tmp',
|
|
140
|
+
'hypr',
|
|
141
|
+
signature,
|
|
142
|
+
'.socket2.sock',
|
|
143
|
+
);
|
|
144
|
+
let socket,
|
|
145
|
+
buffer = '',
|
|
146
|
+
stopped = false;
|
|
147
|
+
const stop = () => {
|
|
148
|
+
stopped = true;
|
|
149
|
+
socket?.end();
|
|
150
|
+
resources.delete(stop);
|
|
151
|
+
};
|
|
152
|
+
resources.add(stop);
|
|
153
|
+
Bun.connect({
|
|
154
|
+
unix: socketPath,
|
|
155
|
+
socket: {
|
|
156
|
+
open(s) {
|
|
157
|
+
socket = s;
|
|
158
|
+
if (stopped) s.end();
|
|
159
|
+
},
|
|
160
|
+
data(s, bytes) {
|
|
161
|
+
buffer += new TextDecoder().decode(bytes);
|
|
162
|
+
let end;
|
|
163
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
164
|
+
const line = buffer.slice(0, end);
|
|
165
|
+
buffer = buffer.slice(end + 1);
|
|
166
|
+
const at = line.indexOf('>>');
|
|
167
|
+
if (at >= 0 && !closed)
|
|
168
|
+
callback({ name: line.slice(0, at), data: line.slice(at + 2) });
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
error() {},
|
|
172
|
+
close() {
|
|
173
|
+
resources.delete(stop);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}).catch(() => resources.delete(stop));
|
|
177
|
+
return stop;
|
|
178
|
+
},
|
|
179
|
+
importRoots: (environment.QML_IMPORT_PATH || '')
|
|
180
|
+
.split(delimiter)
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.map((p) => pathToFileURL(resolve(p) + '/').href),
|
|
183
|
+
write(name, data, atomic = true) {
|
|
184
|
+
check();
|
|
185
|
+
const target = path(name);
|
|
186
|
+
let temporary;
|
|
187
|
+
try {
|
|
188
|
+
if (atomic) {
|
|
189
|
+
let mode = 0o600;
|
|
190
|
+
try {
|
|
191
|
+
mode = statSync(target).mode & 0o777;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error.code !== 'ENOENT') throw error;
|
|
194
|
+
}
|
|
195
|
+
temporary =
|
|
196
|
+
target + '.qml-write-' + process.pid + '-' + crypto.randomUUID();
|
|
197
|
+
writeFileSync(temporary, data, { flag: 'wx', mode });
|
|
198
|
+
renameSync(temporary, target);
|
|
199
|
+
temporary = null;
|
|
200
|
+
} else writeFileSync(target, data);
|
|
201
|
+
return { error: null };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return { error: error.code || String(error) };
|
|
204
|
+
} finally {
|
|
205
|
+
if (temporary)
|
|
206
|
+
try {
|
|
207
|
+
unlinkSync(temporary);
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
read(name) {
|
|
212
|
+
check();
|
|
213
|
+
try {
|
|
214
|
+
return { text: readFileSync(path(name), 'utf8'), error: null };
|
|
215
|
+
} catch (e) {
|
|
216
|
+
return { text: '', error: e.code || String(e) };
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
watch(name, callback) {
|
|
220
|
+
check();
|
|
221
|
+
const target = path(name);
|
|
222
|
+
let watcher;
|
|
223
|
+
try {
|
|
224
|
+
watcher = watch(target, () => {
|
|
225
|
+
if (!closed) callback();
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
return () => {};
|
|
229
|
+
}
|
|
230
|
+
const stop = () => {
|
|
231
|
+
watcher.close();
|
|
232
|
+
resources.delete(stop);
|
|
233
|
+
};
|
|
234
|
+
resources.add(stop);
|
|
235
|
+
return stop;
|
|
236
|
+
},
|
|
237
|
+
socket(name, handlers) {
|
|
238
|
+
check();
|
|
239
|
+
const socket = createConnection({path: path(name)});
|
|
240
|
+
let stopped = false;
|
|
241
|
+
socket.setEncoding('utf8');
|
|
242
|
+
socket.on('connect', () => { if (!stopped) handlers.open(); });
|
|
243
|
+
socket.on('data', chunk => { if (!stopped) handlers.data(chunk); });
|
|
244
|
+
socket.on('error', error => { if (!stopped) handlers.error(error.code || String(error)); });
|
|
245
|
+
socket.on('close', () => {
|
|
246
|
+
resources.delete(stop);
|
|
247
|
+
if (!stopped) { stopped = true; handlers.close(); }
|
|
248
|
+
});
|
|
249
|
+
const stop = () => {
|
|
250
|
+
stopped = true;
|
|
251
|
+
resources.delete(stop);
|
|
252
|
+
socket.destroy();
|
|
253
|
+
};
|
|
254
|
+
stop.write = text => { if (!stopped && !socket.connecting) socket.write(String(text)); };
|
|
255
|
+
stop.flush = () => {}; // node:net queues writes immediately, including backpressure.
|
|
256
|
+
resources.add(stop);
|
|
257
|
+
return stop;
|
|
258
|
+
},
|
|
259
|
+
run(command, callback, options = {}) {
|
|
260
|
+
check();
|
|
261
|
+
if (!Array.isArray(command) || !command.length)
|
|
262
|
+
throw Error('Process.command must be a nonempty array');
|
|
263
|
+
let child;
|
|
264
|
+
try {
|
|
265
|
+
child = spawn({
|
|
266
|
+
cmd: command.map(String),
|
|
267
|
+
cwd: options.cwd ? path(options.cwd) : fileURLToPath(base),
|
|
268
|
+
env: {
|
|
269
|
+
...(options.clearEnvironment ? {} : environment),
|
|
270
|
+
...(options.environment || {}),
|
|
271
|
+
},
|
|
272
|
+
stdin: options.stdin ? 'pipe' : 'ignore',
|
|
273
|
+
stdout: 'pipe',
|
|
274
|
+
stderr: 'pipe',
|
|
275
|
+
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
queueMicrotask(() => {
|
|
278
|
+
if (!closed)
|
|
279
|
+
callback({
|
|
280
|
+
exitCode: 127,
|
|
281
|
+
stdout: '',
|
|
282
|
+
stderr: String(error),
|
|
283
|
+
error: String(error),
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
return () => {};
|
|
287
|
+
}
|
|
288
|
+
const stop = () => {
|
|
289
|
+
child.kill();
|
|
290
|
+
resources.delete(stop);
|
|
291
|
+
};
|
|
292
|
+
stop.write = (text) => {
|
|
293
|
+
if (!options.stdin) throw Error('Process stdin is disabled');
|
|
294
|
+
child.stdin.write(String(text));
|
|
295
|
+
child.stdin.flush();
|
|
296
|
+
};
|
|
297
|
+
stop.closeWriteChannel = () => {
|
|
298
|
+
if (options.stdin) child.stdin.end();
|
|
299
|
+
};
|
|
300
|
+
stop.signal = (signal) => child.kill(Number(signal));
|
|
301
|
+
stop.processId = child.pid;
|
|
302
|
+
resources.add(stop);
|
|
303
|
+
const read = async (stream, onData) => {
|
|
304
|
+
let text = '';
|
|
305
|
+
const decoder = new TextDecoder();
|
|
306
|
+
for await (const bytes of stream) {
|
|
307
|
+
const chunk = decoder.decode(bytes, { stream: true });
|
|
308
|
+
text += chunk;
|
|
309
|
+
if (!closed) onData?.(chunk, bytes);
|
|
310
|
+
}
|
|
311
|
+
const tail = decoder.decode();
|
|
312
|
+
text += tail;
|
|
313
|
+
if (tail && !closed) onData?.(tail);
|
|
314
|
+
return text;
|
|
315
|
+
};
|
|
316
|
+
Promise.all([
|
|
317
|
+
read(child.stdout, options.stdout),
|
|
318
|
+
read(child.stderr, options.stderr),
|
|
319
|
+
child.exited,
|
|
320
|
+
])
|
|
321
|
+
.then(([stdout, stderr, exitCode]) => {
|
|
322
|
+
resources.delete(stop);
|
|
323
|
+
if (!closed) callback({ stdout, stderr, exitCode });
|
|
324
|
+
})
|
|
325
|
+
.catch((error) => {
|
|
326
|
+
resources.delete(stop);
|
|
327
|
+
if (!closed)
|
|
328
|
+
callback({
|
|
329
|
+
exitCode: 1,
|
|
330
|
+
stdout: '',
|
|
331
|
+
stderr: String(error),
|
|
332
|
+
error: String(error),
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
return stop;
|
|
336
|
+
},
|
|
337
|
+
trackResource(dispose) {
|
|
338
|
+
check();let stopped=false;
|
|
339
|
+
const stop=()=>{if(stopped)return;stopped=true;resources.delete(stop);dispose();};
|
|
340
|
+
resources.add(stop);return stop;
|
|
341
|
+
},
|
|
342
|
+
dispose() {
|
|
343
|
+
if (closed) return;
|
|
344
|
+
closed = true;
|
|
345
|
+
for (const stop of [...resources]) stop();
|
|
346
|
+
resources.clear();
|
|
347
|
+
active.delete(host);
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
active.add(host);
|
|
351
|
+
return host;
|
|
352
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// QML Image uses existing raster decoding or an inert inline SVG DOM. The SVG
|
|
2
|
+
// subset matches the browser's renderer; it does not rasterize/re-encode SVG.
|
|
3
|
+
export function createQmlImages(api) {
|
|
4
|
+
const { document, effect, host, cleanup } = api;
|
|
5
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
6
|
+
function svg(source) {
|
|
7
|
+
if (source.length > 4 * 1024 * 1024) throw Error('QML SVG exceeds 4 MiB');
|
|
8
|
+
const parsed = new DOMParser().parseFromString(source, 'image/svg+xml');
|
|
9
|
+
const root = parsed.documentElement;
|
|
10
|
+
if (root?.localName !== 'svg' || parsed.querySelector('parsererror'))
|
|
11
|
+
throw Error('Invalid SVG image');
|
|
12
|
+
const supported = new Set([
|
|
13
|
+
'svg',
|
|
14
|
+
'g',
|
|
15
|
+
'path',
|
|
16
|
+
'rect',
|
|
17
|
+
'circle',
|
|
18
|
+
'ellipse',
|
|
19
|
+
'line',
|
|
20
|
+
'polyline',
|
|
21
|
+
'polygon',
|
|
22
|
+
]);
|
|
23
|
+
const ignored = new Set(['title', 'desc', 'metadata']);
|
|
24
|
+
let nodes = 0;
|
|
25
|
+
function copy(from) {
|
|
26
|
+
if (++nodes > 10000) throw Error('QML SVG element limit');
|
|
27
|
+
if (!supported.has(from.localName))
|
|
28
|
+
throw Error('Unsupported QML SVG element: ' + from.localName);
|
|
29
|
+
const to = document.createElementNS(ns, from.localName);
|
|
30
|
+
for (const attr of from.attributes) {
|
|
31
|
+
if (
|
|
32
|
+
/^on/i.test(attr.name) ||
|
|
33
|
+
['href', 'xlink:href'].includes(attr.name) ||
|
|
34
|
+
/url\s*\(/i.test(attr.value)
|
|
35
|
+
)
|
|
36
|
+
throw Error('Unsupported active/external QML SVG attribute');
|
|
37
|
+
to.setAttribute(attr.name, attr.value);
|
|
38
|
+
}
|
|
39
|
+
for (const child of from.children) {
|
|
40
|
+
if (!ignored.has(child.localName)) to.appendChild(copy(child));
|
|
41
|
+
}
|
|
42
|
+
return to;
|
|
43
|
+
}
|
|
44
|
+
const element = copy(root),
|
|
45
|
+
box = (root.getAttribute('viewBox') || '')
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/[\s,]+/)
|
|
48
|
+
.map(Number);
|
|
49
|
+
const width = parseFloat(root.getAttribute('width')) || box[2],
|
|
50
|
+
height = parseFloat(root.getAttribute('height')) || box[3];
|
|
51
|
+
if (!(width > 0 && height > 0)) throw Error('SVG image has no dimensions');
|
|
52
|
+
if (!element.hasAttribute('viewBox'))
|
|
53
|
+
element.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
54
|
+
return { element, width, height };
|
|
55
|
+
}
|
|
56
|
+
return (v, element, url) => {
|
|
57
|
+
let generation = 0,
|
|
58
|
+
current,
|
|
59
|
+
node = null,
|
|
60
|
+
pendingImage = null;
|
|
61
|
+
const remove = () => {
|
|
62
|
+
node?.remove();
|
|
63
|
+
node = null;
|
|
64
|
+
};
|
|
65
|
+
const fit = () => {
|
|
66
|
+
v.status;
|
|
67
|
+
if (!node) return;
|
|
68
|
+
node.style.position = 'absolute';
|
|
69
|
+
node.style.pointerEvents = 'none';
|
|
70
|
+
node.style.width = v.width + 'px';
|
|
71
|
+
node.style.height = v.height + 'px';
|
|
72
|
+
node.style.left = '0';
|
|
73
|
+
node.style.top = '0';
|
|
74
|
+
node.style.objectFit =
|
|
75
|
+
v.fillMode === 1
|
|
76
|
+
? 'contain'
|
|
77
|
+
: v.fillMode === 2
|
|
78
|
+
? 'cover'
|
|
79
|
+
: v.fillMode === 6
|
|
80
|
+
? 'none'
|
|
81
|
+
: 'fill';
|
|
82
|
+
node.style.imageRendering = v.smooth ? 'auto' : 'pixelated';
|
|
83
|
+
node.style.transform = v.mirror ? 'scaleX(-1)' : '';
|
|
84
|
+
const ratio =
|
|
85
|
+
v.fillMode === 6
|
|
86
|
+
? 1
|
|
87
|
+
: (v.fillMode === 2 ? Math.max : Math.min)(
|
|
88
|
+
v.width / (v.implicitWidth || 1),
|
|
89
|
+
v.height / (v.implicitHeight || 1),
|
|
90
|
+
);
|
|
91
|
+
v.paintedWidth = [1, 2, 6].includes(v.fillMode)
|
|
92
|
+
? v.implicitWidth * ratio
|
|
93
|
+
: v.width;
|
|
94
|
+
v.paintedHeight = [1, 2, 6].includes(v.fillMode)
|
|
95
|
+
? v.implicitHeight * ratio
|
|
96
|
+
: v.height;
|
|
97
|
+
if (node.localName === 'svg') {
|
|
98
|
+
// The inline SVG renderer always preserves its viewBox ratio. Fit by
|
|
99
|
+
// sizing the DOM box, then let the QML wrapper clip crop/pad overflow.
|
|
100
|
+
node.style.width = v.paintedWidth + 'px';
|
|
101
|
+
node.style.height = v.paintedHeight + 'px';
|
|
102
|
+
node.style.left = (v.width - v.paintedWidth) / 2 + 'px';
|
|
103
|
+
node.style.top = (v.height - v.paintedHeight) / 2 + 'px';
|
|
104
|
+
}
|
|
105
|
+
element.style.overflow = 'hidden';
|
|
106
|
+
};
|
|
107
|
+
effect(() => {
|
|
108
|
+
if (current === v.source) return;
|
|
109
|
+
current = v.source;
|
|
110
|
+
const token = ++generation;
|
|
111
|
+
pendingImage?.remove();
|
|
112
|
+
pendingImage = null;
|
|
113
|
+
if (!current) {
|
|
114
|
+
remove();
|
|
115
|
+
v.status = 0;
|
|
116
|
+
v.progress = 0;
|
|
117
|
+
v.implicitWidth = 0;
|
|
118
|
+
v.implicitHeight = 0;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!v.retainWhileLoading) remove();
|
|
122
|
+
v.status = 2;
|
|
123
|
+
v.progress = 0;
|
|
124
|
+
const source = new URL(current, url).href;
|
|
125
|
+
const ready = (next, width, height) => {
|
|
126
|
+
if (token !== generation) {
|
|
127
|
+
next.remove();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
remove();
|
|
131
|
+
node = next;
|
|
132
|
+
element.appendChild(node);
|
|
133
|
+
v.implicitWidth = width;
|
|
134
|
+
v.implicitHeight = height;
|
|
135
|
+
v.status = 1;
|
|
136
|
+
v.progress = 1;
|
|
137
|
+
fit();
|
|
138
|
+
};
|
|
139
|
+
const failed = () => {
|
|
140
|
+
if (token === generation) {
|
|
141
|
+
remove();
|
|
142
|
+
v.status = 3;
|
|
143
|
+
v.progress = 0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (
|
|
147
|
+
/\.svg(?:[?#]|$)/i.test(source) ||
|
|
148
|
+
/^data:image\/svg\+xml/i.test(source)
|
|
149
|
+
) {
|
|
150
|
+
(async () => {
|
|
151
|
+
let text;
|
|
152
|
+
if (source.startsWith('file:')) {
|
|
153
|
+
if (!host) throw Error('Local SVG requires local QML');
|
|
154
|
+
const result = host.read(source);
|
|
155
|
+
if (result.error) throw Error(result.error);
|
|
156
|
+
text = result.text;
|
|
157
|
+
} else {
|
|
158
|
+
const response = await fetch(source);
|
|
159
|
+
if (!response.ok) throw Error('SVG HTTP ' + response.status);
|
|
160
|
+
text = await response.text();
|
|
161
|
+
}
|
|
162
|
+
const result = svg(text);
|
|
163
|
+
ready(result.element, result.width, result.height);
|
|
164
|
+
})().catch(failed);
|
|
165
|
+
} else {
|
|
166
|
+
const image = document.createElement('img');
|
|
167
|
+
pendingImage = image;
|
|
168
|
+
image.style.display = 'none';
|
|
169
|
+
element.appendChild(image);
|
|
170
|
+
image.addEventListener(
|
|
171
|
+
'load',
|
|
172
|
+
() => {
|
|
173
|
+
if (pendingImage === image) pendingImage = null;
|
|
174
|
+
image.style.display = 'block';
|
|
175
|
+
ready(image, image.naturalWidth, image.naturalHeight);
|
|
176
|
+
},
|
|
177
|
+
{ once: true },
|
|
178
|
+
);
|
|
179
|
+
image.addEventListener(
|
|
180
|
+
'error',
|
|
181
|
+
() => {
|
|
182
|
+
if (pendingImage === image) pendingImage = null;
|
|
183
|
+
image.remove();
|
|
184
|
+
failed();
|
|
185
|
+
},
|
|
186
|
+
{ once: true },
|
|
187
|
+
);
|
|
188
|
+
image.src = source;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
effect(fit);
|
|
192
|
+
cleanup(() => {
|
|
193
|
+
generation++;
|
|
194
|
+
pendingImage?.remove();
|
|
195
|
+
pendingImage = null;
|
|
196
|
+
remove();
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
}
|