buninu-browser 0.3.0 → 0.4.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 (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +116 -0
  20. package/frontends/wayland/input.js +56 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,1176 @@
1
+ import { nonvisualTypes, typeProperties, createQt } from './builtins.js';
2
+ import { createInertWindow } from '../happy-dom/parser.js';
3
+ import { parseQml } from './parser.js';
4
+ import { createQmlTextures } from './textures.js';
5
+ import { createQmlMedia } from './media.js';
6
+ import { createQmlImages } from './images.js';
7
+ import { createQmlShapes } from './shapes.js';
8
+ import { createQmlText } from './text.js';
9
+ import { createQmlModels } from './models.js';
10
+ import { createReactive } from './reactive.js';
11
+ import { createQmlRuntime } from './runtime.js';
12
+ import { builtinTypes, loadQmlGraph } from './loader.js';
13
+
14
+ const types = builtinTypes;
15
+ const defaults = [
16
+ 'width',
17
+ 'height',
18
+ 'implicitWidth',
19
+ 'implicitHeight',
20
+ 'x',
21
+ 'y',
22
+ 'color',
23
+ 'radius',
24
+ 'containmentMask',
25
+ 'Layout',
26
+ 'Keys',
27
+ 'opacity',
28
+ 'visible',
29
+ 'enabled',
30
+ 'clip',
31
+ 'text',
32
+ 'title',
33
+ 'font',
34
+ 'border',
35
+ 'anchors',
36
+ 'horizontalAlignment',
37
+ 'textFormat',
38
+ 'spacing',
39
+ 'placeholderText',
40
+ 'readOnly',
41
+ 'selectByMouse',
42
+ 'Accessible',
43
+ 'objectName',
44
+ 'activeFocus',
45
+ 'focus',
46
+ 'activeFocusOnTab',
47
+ 'baseline',
48
+ 'horizontalCenter',
49
+ 'verticalCenter',
50
+ 'left',
51
+ 'right',
52
+ 'top',
53
+ 'bottom',
54
+ 'childrenRect',
55
+ 'children',
56
+ 'data',
57
+ 'parent',
58
+ 'Drag',
59
+ 'gradient',
60
+ 'scale',
61
+ 'rotation',
62
+ 'transform',
63
+ 'z',
64
+ 'baselineOffset',
65
+ 'topPadding',
66
+ 'bottomPadding',
67
+ 'leftPadding',
68
+ 'rightPadding',
69
+ 'linkColor',
70
+ 'hoveredLink',
71
+ 'truncated',
72
+ 'contentWidth',
73
+ 'contentHeight',
74
+ 'maximumLineCount',
75
+ 'lineCount',
76
+ 'renderType',
77
+ 'verticalAlignment',
78
+ 'wrapMode',
79
+ 'elide',
80
+ 'lineHeight',
81
+ 'lineHeightMode',
82
+ 'forceActiveFocus',
83
+ 'contains',
84
+ 'mapToItem',
85
+ 'mapFromItem',
86
+ ];
87
+ const specializedProperties = {
88
+ lineCount: ['Text', 'Label', 'TextEdit'],
89
+ text: ['Text', 'Label', 'TextField', 'TextInput', 'TextEdit', 'Button'],
90
+ font: ['Text', 'Label', 'TextField', 'TextInput', 'TextEdit', 'Button'],
91
+ color: [
92
+ 'Text',
93
+ 'Label',
94
+ 'TextField',
95
+ 'TextInput',
96
+ 'TextEdit',
97
+ 'Button',
98
+ 'Rectangle',
99
+ 'FloatingWindow',
100
+ ],
101
+ title: ['FloatingWindow'],
102
+ border: ['Rectangle'],
103
+ radius: ['Rectangle'],
104
+ textFormat: ['Text', 'Label', 'TextEdit'],
105
+ placeholderText: ['TextField'],
106
+ readOnly: ['TextField', 'TextInput', 'TextEdit'],
107
+ selectByMouse: ['TextField', 'TextInput', 'TextEdit'],
108
+ horizontalAlignment: [
109
+ 'Text',
110
+ 'Label',
111
+ 'TextField',
112
+ 'TextInput',
113
+ 'TextEdit',
114
+ 'Button',
115
+ ],
116
+ };
117
+ const itemProperties = (type) =>
118
+ defaults.filter(
119
+ (key) =>
120
+ !specializedProperties[key] || specializedProperties[key].includes(type),
121
+ );
122
+ const groups = new Set([
123
+ 'Layout.fillWidth',
124
+ 'Layout.fillHeight',
125
+ 'Layout.minimumWidth',
126
+ 'Layout.minimumHeight',
127
+ 'Layout.maximumWidth',
128
+ 'Layout.maximumHeight',
129
+ 'Layout.preferredWidth',
130
+ 'Layout.preferredHeight',
131
+ 'Layout.alignment',
132
+ 'Layout.margins',
133
+ 'Layout.leftMargin',
134
+ 'Layout.rightMargin',
135
+ 'Layout.topMargin',
136
+ 'Layout.bottomMargin',
137
+ 'Keys.enabled',
138
+ 'Keys.forwardTo',
139
+ 'font.pixelSize',
140
+ 'font.family',
141
+ 'font.bold',
142
+ 'border.width',
143
+ 'border.color',
144
+ 'font.capitalization',
145
+ 'font.letterSpacing',
146
+ 'font.italic',
147
+ 'font.features',
148
+ 'anchors.fill',
149
+ 'anchors.centerIn',
150
+ 'anchors.horizontalCenter',
151
+ 'anchors.verticalCenter',
152
+ 'anchors.left',
153
+ 'anchors.right',
154
+ 'anchors.baseline',
155
+ 'anchors.baselineOffset',
156
+ 'anchors.top',
157
+ 'anchors.bottom',
158
+ 'anchors.margins',
159
+ 'anchors.leftMargin',
160
+ 'anchors.rightMargin',
161
+ 'anchors.topMargin',
162
+ 'anchors.bottomMargin',
163
+ 'anchors.horizontalCenterOffset',
164
+ 'anchors.verticalCenterOffset',
165
+ 'Accessible.selected',
166
+ 'Accessible.checkable',
167
+ 'Accessible.name',
168
+ 'Accessible.role',
169
+ 'Accessible.checked',
170
+ 'Accessible.description',
171
+ 'Accessible.focusable',
172
+ 'Accessible.focused',
173
+ 'Accessible.ignored',
174
+ 'xAxis.enabled',
175
+ 'xAxis.minimum',
176
+ 'xAxis.maximum',
177
+ 'yAxis.enabled',
178
+ 'yAxis.minimum',
179
+ 'yAxis.maximum',
180
+ 'sourceSize.width',
181
+ 'sourceSize.height',
182
+ 'Drag.active',
183
+ 'Drag.source',
184
+ 'Drag.keys',
185
+ 'Drag.mimeData',
186
+ 'Drag.supportedActions',
187
+ 'Drag.proposedAction',
188
+ 'Drag.dragType',
189
+ 'Drag.imageSource',
190
+ 'Drag.hotSpot.x',
191
+ 'Drag.hotSpot.y',
192
+ 'origin.x',
193
+ 'origin.y',
194
+ 'easing.type',
195
+ 'easing.amplitude',
196
+ 'easing.period',
197
+ 'easing.overshoot',
198
+ ]);
199
+
200
+ // Rewrite only QML-resolved names; all other identifiers retain page JS meaning.
201
+ // Function/block-local names shadow QML properties. No eval in the host process.
202
+ function rewriteExpression(property, scope, runtimeRef) {
203
+ const edits = [];
204
+ function walk(node, parent, key, locals = new Set()) {
205
+ if (!node || typeof node !== 'object') return;
206
+ if (Array.isArray(node)) {
207
+ for (const child of node) walk(child, parent, key, locals);
208
+ return;
209
+ }
210
+ if (
211
+ [
212
+ 'FunctionExpression',
213
+ 'ArrowFunctionExpression',
214
+ 'FunctionDeclaration',
215
+ 'BlockStatement',
216
+ 'Program',
217
+ 'ForStatement',
218
+ 'ForInStatement',
219
+ 'ForOfStatement',
220
+ 'CatchClause',
221
+ ].includes(node.type)
222
+ ) {
223
+ locals = new Set(locals);
224
+ const addPattern = (n) => {
225
+ if (!n) return;
226
+ if (n.type === 'Identifier') locals.add(n.name);
227
+ else if (n.type === 'AssignmentPattern') addPattern(n.left);
228
+ else if (n.type === 'RestElement') addPattern(n.argument);
229
+ else if (n.type === 'ArrayPattern') n.elements.forEach(addPattern);
230
+ else if (n.type === 'ObjectPattern')
231
+ n.properties.forEach((p) =>
232
+ addPattern(p.type === 'RestElement' ? p : p.value),
233
+ );
234
+ };
235
+ const collectVars = (n) => {
236
+ if (!n || typeof n !== 'object') return;
237
+ if (Array.isArray(n)) {
238
+ n.forEach(collectVars);
239
+ return;
240
+ }
241
+ if (
242
+ [
243
+ 'FunctionExpression',
244
+ 'ArrowFunctionExpression',
245
+ 'FunctionDeclaration',
246
+ ].includes(n.type)
247
+ )
248
+ return;
249
+ if (n.type === 'VariableDeclaration' && n.kind === 'var')
250
+ n.declarations.forEach((d) => addPattern(d.id));
251
+ for (const [name, child] of Object.entries(n))
252
+ if (name !== 'type' && child && typeof child === 'object')
253
+ collectVars(child);
254
+ };
255
+ if (
256
+ node.type === 'Program' ||
257
+ node.type.includes('Function') ||
258
+ (node.type === 'BlockStatement' && !parent)
259
+ )
260
+ collectVars(node.body);
261
+ const loop = node.init ?? node.left;
262
+ if (loop?.type === 'VariableDeclaration')
263
+ loop.declarations.forEach((d) => addPattern(d.id));
264
+ if (node.type === 'CatchClause') addPattern(node.param);
265
+ node.params?.forEach(addPattern);
266
+ addPattern(node.id);
267
+ const statements = Array.isArray(node.body)
268
+ ? node.body
269
+ : node.body?.type === 'BlockStatement'
270
+ ? node.body.body
271
+ : [];
272
+ for (const statement of statements) {
273
+ if (statement.type === 'VariableDeclaration')
274
+ statement.declarations.forEach((d) => addPattern(d.id));
275
+ if (statement.type === 'FunctionDeclaration') addPattern(statement.id);
276
+ }
277
+ }
278
+ // Replacing a namespace identifier with a parenthesized object must not
279
+ // turn the following line into a call on the previous expression (ASI).
280
+ if (
281
+ [
282
+ 'ExpressionStatement',
283
+ 'ReturnStatement',
284
+ 'ThrowStatement',
285
+ 'BreakStatement',
286
+ 'ContinueStatement',
287
+ 'VariableDeclaration',
288
+ ].includes(node.type) &&
289
+ !(
290
+ ['ForStatement', 'ForInStatement', 'ForOfStatement'].includes(
291
+ parent?.type,
292
+ ) && ['init', 'left'].includes(key)
293
+ ) &&
294
+ property.code[node.end - 1] !== ';'
295
+ )
296
+ edits.push([node.end, node.end, ';']);
297
+ if (
298
+ node.type === 'BinaryExpression' &&
299
+ node.operator === 'in' &&
300
+ /\bas\b/.test(property.code.slice(node.left.end, node.right.start))
301
+ ) {
302
+ edits.push(
303
+ [node.start, node.start, runtimeRef + '.cast('],
304
+ [node.left.end, node.right.start, ','],
305
+ [
306
+ node.right.start,
307
+ node.right.end,
308
+ JSON.stringify(property.code.slice(node.right.start, node.right.end)),
309
+ ],
310
+ [node.end, node.end, ')'],
311
+ );
312
+ walk(node.left, node, 'left', locals);
313
+ return;
314
+ }
315
+ if (
316
+ node.type === 'MemberExpression' &&
317
+ !node.computed &&
318
+ !node.optional &&
319
+ node.property.name === 'arg' &&
320
+ parent?.type === 'CallExpression' &&
321
+ key === 'callee'
322
+ ) {
323
+ edits.push(
324
+ [node.start, node.start, runtimeRef + '.argFor('],
325
+ [node.object.end, node.end, ')'],
326
+ );
327
+ walk(node.object, node, 'object', locals);
328
+ return;
329
+ }
330
+ if (
331
+ node.type === 'Identifier' &&
332
+ scope.has(node.name) &&
333
+ !locals.has(node.name)
334
+ ) {
335
+ if (
336
+ (parent?.type === 'MemberExpression' &&
337
+ key === 'property' &&
338
+ !parent.computed) ||
339
+ (parent?.type === 'Property' && key === 'key') ||
340
+ (parent?.type === 'VariableDeclarator' && key === 'id')
341
+ )
342
+ return;
343
+ const replacement = scope.get(node.name);
344
+ edits.push([
345
+ node.start,
346
+ node.end,
347
+ parent?.type === 'Property' && parent.shorthand
348
+ ? `${node.name}: ${replacement}`
349
+ : replacement,
350
+ ]);
351
+ return;
352
+ }
353
+ for (const [k, v] of Object.entries(node))
354
+ if (k !== 'type' && v && typeof v === 'object') walk(v, node, k, locals);
355
+ }
356
+ walk(property.ast, null, null);
357
+ let code = property.code;
358
+ for (const [start, end, value] of edits.sort(
359
+ (a, b) => b[0] - a[0] || b[1] - a[1],
360
+ ))
361
+ code = code.slice(0, start) + value + code.slice(end);
362
+ return property.block ? `(()=>${code})()` : code;
363
+ }
364
+
365
+ /** Asynchronous import preparation uses the browser's existing resource bridge. */
366
+ export async function loadQmlDocument(source, url, options = {}) {
367
+ try {
368
+ if (options.scripting === false)
369
+ return createQmlDocument(source, url, options);
370
+ const graph = await loadQmlGraph(
371
+ source,
372
+ url,
373
+ options.fetchResource,
374
+ options,
375
+ );
376
+ return createQmlDocument(source, url, { ...options, graph });
377
+ } catch (error) {
378
+ return createQmlDocument(source, url, { ...options, error });
379
+ }
380
+ }
381
+
382
+ /** Build actual DOM nodes. Import ASTs are discarded after compiling the page. */
383
+ export function createQmlDocument(
384
+ source,
385
+ url,
386
+ {
387
+ scripting = true,
388
+ graph = null,
389
+ error = null,
390
+ component = false,
391
+ externalSingletons = [],
392
+ } = {},
393
+ ) {
394
+ const window = createInertWindow(url),
395
+ document = window.document;
396
+ document.body.style.cssText = 'margin:0;overflow:hidden';
397
+ try {
398
+ if (error) throw error;
399
+ if (!scripting)
400
+ throw Error('QML rendering requires page scripting; source shown below.');
401
+ if (!graph) {
402
+ const ast = parseQml(source);
403
+ for (const imp of ast.imports)
404
+ if (
405
+ !/^(QtQuick(?:\.Controls)?|Quickshell)(?:\s+\d+(?:\.\d+)?)?$/.test(
406
+ imp,
407
+ )
408
+ )
409
+ throw Error(`Unsupported QML import: ${imp}`);
410
+ graph = {
411
+ entry: url,
412
+ documents: new Map([
413
+ [
414
+ url,
415
+ {
416
+ url,
417
+ ast,
418
+ types: new Map(),
419
+ values: new Map(),
420
+ builtins: new Map(),
421
+ },
422
+ ],
423
+ ]),
424
+ scripts: new Map(),
425
+ };
426
+ }
427
+ // Generated references must not be captured by application locals such as
428
+ // `for (var r = 0; ...)` inside a binding that reads a shared Theme.
429
+ const applicationCode =
430
+ JSON.stringify([...graph.documents.values()].map((d) => d.ast)) +
431
+ [...graph.scripts.values()].map((s) => s.source).join('\n');
432
+ let generatedPrefix = '$qml$';
433
+ while (applicationCode.includes(generatedPrefix)) generatedPrefix += '$';
434
+ const runtimeRef = generatedPrefix + 'runtime';
435
+ const expression = (property, scope) =>
436
+ rewriteExpression(property, scope, runtimeRef);
437
+ const records = [],
438
+ contexts = [],
439
+ singletons = new Map(),
440
+ scripts = [];
441
+ for (const url of externalSingletons)
442
+ singletons.set(url, {
443
+ external: `${runtimeRef}.shared(${JSON.stringify(url)})`,
444
+ });
445
+ let template = null;
446
+ const constants =
447
+ 'const MediaPlayer={NoError:0,ResourceError:1,NetworkError:2,FormatError:3,AccessDeniedError:4,StoppedState:0,PlayingState:1,PausedState:2,NoMedia:0,LoadingMedia:1,LoadedMedia:2,StalledMedia:3,BufferingMedia:4,BufferedMedia:5,EndOfMedia:6,InvalidMedia:7,Infinite:-1};const VideoOutput={Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2};const XmlListModel={Null:0,Ready:1,Loading:2,Error:3};const LoggingCategory={Debug:0,Info:4,Warning:1,Critical:2,Fatal:3};const Flow={LeftToRight:0,TopToBottom:1};const ListView={Beginning:0,Center:1,End:2,Visible:3,Contain:4,SnapPosition:5,Vertical:2,Horizontal:1,NoHighlightRange:0};const GridView={...ListView,FlowLeftToRight:0,FlowTopToBottom:1};const Binding={RestoreNone:0,RestoreBinding:1,RestoreValue:2,RestoreBindingOrValue:3};const Gradient={Vertical:0,Horizontal:1};const PointerHandler={CanTakeOverFromAnything:31,ApprovesTakeOverByAnything:511,TakeOverForbidden:0};const Drag={None:0,Automatic:1,Internal:2};const Application={styleHints:{wheelScrollLines:3,mouseDoubleClickInterval:400}};const Flickable={AutoFlickDirection:0,HorizontalFlick:1,VerticalFlick:2,HorizontalAndVerticalFlick:3,StopAtBounds:0,DragOverBounds:1,OvershootBounds:2,DragAndOvershootBounds:3};const Image={Null:0,Ready:1,Loading:2,Error:3,Stretch:0,PreserveAspectFit:1,PreserveAspectCrop:2,Tile:3,Pad:6};const TextInput={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,Normal:0,Password:2,NoEcho:1,PasswordEchoOnEdit:3,NativeRendering:1};const TextEdit=TextInput;const Text={AlignLeft:1,AlignRight:2,AlignHCenter:4,AlignTop:32,AlignBottom:64,AlignVCenter:128,PlainText:0,RichText:1,AutoText:2,StyledText:3,MarkdownText:4,NoWrap:0,WordWrap:1,WrapAnywhere:2,Wrap:3,WrapAtWordBoundaryOrAnywhere:3,ElideNone:0,ElideLeft:1,ElideRight:2,ElideMiddle:3,ProportionalHeight:0,FixedHeight:1};const Font={MixedCase:0,AllUppercase:1,AllLowercase:2,SmallCaps:3,Capitalize:4};const Accessible={Button:43,CheckBox:44};const Shape={CurveRenderer:1};const ShapePath={SquareCap:16,FlatCap:0,RoundCap:32,MiterJoin:0,BevelJoin:64,RoundJoin:128,DashLine:2,WindingFill:1,OddEvenFill:0};const Easing={Linear:0,InQuad:1,OutQuad:2,InOutQuad:3,InCubic:5,OutCubic:6,InOutCubic:7,OutQuart:10,OutQuint:14,OutSine:18,OutExpo:22,OutBack:34};const Animation={Infinite:-1};const Component={Null:0,Ready:1,Loading:2,Error:3};const Loader=Component;const TapHandler={ReleaseWithinBounds:1};const Qt=r.Qt,Quickshell=r.Quickshell,Hyprland=r.Hyprland,FileViewError={FileNotFound:"ENOENT"};';
448
+ const ref = (record) =>
449
+ record.external ?? `${generatedPrefix}n${record.index}`;
450
+ function context(
451
+ doc,
452
+ parent = null,
453
+ chain = [],
454
+ singletonUrl = null,
455
+ inline = null,
456
+ outer = null,
457
+ ) {
458
+ const ctx = {
459
+ doc,
460
+ ids: new Map(),
461
+ values: new Map(),
462
+ index: contexts.length,
463
+ root: null,
464
+ singletonUrl,
465
+ template,
466
+ outer,
467
+ };
468
+ contexts.push(ctx);
469
+ build(inline ?? doc.ast.root, parent, ctx, chain);
470
+ if (singletonUrl) singletons.set(singletonUrl, ctx.root);
471
+ for (const [name, value] of doc.values) {
472
+ if (value.kind === 'singleton') {
473
+ if (!singletons.has(value.url)) {
474
+ const savedTemplate = template;
475
+ template = null;
476
+ const child = context(
477
+ graph.documents.get(value.url),
478
+ null,
479
+ [],
480
+ value.url,
481
+ );
482
+ template = savedTemplate;
483
+ const record = child.root;
484
+ record.element.style.display = 'none';
485
+ record.nonvisual = true;
486
+ singletons.set(value.url, record);
487
+ }
488
+ ctx.values.set(name, ref(singletons.get(value.url)));
489
+ } else ctx.values.set(name, script(value.url, ctx));
490
+ }
491
+ for (const alias of doc.builtins.keys())
492
+ ctx.values.set(`${alias}.Text`, 'Text');
493
+ return ctx;
494
+ }
495
+ const libraryScripts = new Map();
496
+ function script(href, ctx, chain = [], libraryParent = false) {
497
+ if (chain.includes(href)) throw Error(`Cyclic JS import: ${href}`);
498
+ const item = graph.scripts.get(href);
499
+ if (!item) throw Error(`Missing JS resource ${href}`);
500
+ const library = item.library || libraryParent;
501
+ const cache = library ? libraryScripts : (ctx.scriptCache ??= new Map());
502
+ if (cache.has(href)) return cache.get(href);
503
+ const deps = item.deps
504
+ .map(
505
+ (d) =>
506
+ `const ${d.alias}=${script(d.url, ctx, [...chain, href], library)};`,
507
+ )
508
+ .join('\n');
509
+ const name = `${generatedPrefix}j${scripts.length}`;
510
+ scripts.push({ name, deps, item, ctx, library, href });
511
+ cache.set(href, name);
512
+ return name;
513
+ }
514
+ function addProperties(record, object, ctx) {
515
+ for (const p of object.properties) {
516
+ if (p.key === 'id') {
517
+ if (!/^[A-Za-z_][\w]*$/.test(p.code) || ctx.ids.has(p.code))
518
+ throw Error(`Invalid or duplicate QML id: ${p.code}`);
519
+ ctx.ids.set(p.code, ref(record));
520
+ record.element.id =
521
+ ctx.index === 0 && !template
522
+ ? p.code
523
+ : `qml-${ctx.index}-${p.code}`;
524
+ continue;
525
+ }
526
+ if (['__proto__', 'constructor', 'prototype'].includes(p.key))
527
+ throw Error(`Invalid QML property: ${p.key}`);
528
+ if (p.custom || p.method || p.signal) {
529
+ if (
530
+ p.custom &&
531
+ ![
532
+ 'int',
533
+ 'real',
534
+ 'bool',
535
+ 'string',
536
+ 'var',
537
+ 'color',
538
+ 'alias',
539
+ 'double',
540
+ 'url',
541
+ 'point',
542
+ 'size',
543
+ 'rect',
544
+ ].includes(p.propertyType) &&
545
+ !/^[A-Z]/.test(p.propertyType)
546
+ )
547
+ throw Error(`Unsupported QML property type: ${p.propertyType}`);
548
+ if (
549
+ record.properties.has(p.key) &&
550
+ record.properties.get(p.key).p.custom
551
+ )
552
+ throw Error(`Duplicate QML property: ${p.key}`);
553
+ record.names.add(p.key);
554
+ } else if (
555
+ !record.names.has(p.key) &&
556
+ !groups.has(p.key) &&
557
+ !/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
558
+ p.key,
559
+ ) &&
560
+ !['Component.onCompleted', 'Component.onDestruction'].includes(p.key)
561
+ )
562
+ throw Error(
563
+ `Unsupported QML property: ${record.type}.${p.key} (${ctx.doc.url})`,
564
+ );
565
+ if (p.propertyType === 'alias') record.aliases.set(p.key, { p, ctx });
566
+ const previous = record.properties.get(p.key);
567
+ // Each component layer connects its own signal handler. Overriding a
568
+ // property binding must not discard the base component's connections.
569
+ if (previous && !p.method && !p.signal &&
570
+ (/^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(p.key) ||
571
+ ['Component.onCompleted', 'Component.onDestruction'].includes(p.key)))
572
+ (record.inheritedHandlers ??= []).push(previous);
573
+ if (previous?.p.readonly && !p.custom)
574
+ throw Error(`Cannot override readonly QML property: ${p.key}`);
575
+ record.properties.set(p.key, {
576
+ p:
577
+ record.aliases.has(p.key) && p.propertyType !== 'alias'
578
+ ? { ...p, aliasOverride: true }
579
+ : previous?.p.custom && !p.custom
580
+ ? {
581
+ ...p,
582
+ custom: true,
583
+ propertyType: previous.p.propertyType,
584
+ defaultProperty: previous.p.defaultProperty,
585
+ }
586
+ : p,
587
+ ctx,
588
+ });
589
+ }
590
+ }
591
+ function aliasSlot(record, key, seen = new Set()) {
592
+ const token = record.index + ':' + key;
593
+ if (seen.has(token)) throw Error('Cyclic property alias');
594
+ seen.add(token);
595
+ const entry = record.aliases.get(key);
596
+ if (!entry) return null;
597
+ const { p, ctx } = entry,
598
+ ast = p.ast;
599
+ if (
600
+ ast.type !== 'MemberExpression' ||
601
+ ast.object.type !== 'Identifier' ||
602
+ ast.computed
603
+ )
604
+ return null;
605
+ const target = records.find(
606
+ (r) => ref(r) === ctx.ids.get(ast.object.name),
607
+ );
608
+ if (!target) return null;
609
+ if (['data', 'children'].includes(ast.property.name)) return target;
610
+ return aliasSlot(target, ast.property.name, seen);
611
+ }
612
+ function defaultSlot(record) {
613
+ const entry = [...record.aliases.values()].find(
614
+ ({ p }) => p.defaultProperty,
615
+ );
616
+ if (!entry) return record;
617
+ const target = aliasSlot(record, entry.p.key);
618
+ if (!target)
619
+ throw Error('Default property must reference a child collection');
620
+ return target;
621
+ }
622
+ function buildPropertyObject(p, record, ctx, chain) {
623
+ const slot = aliasSlot(record, p.key);
624
+ const child = build(
625
+ p.key === 'sourceComponent' && p.object.type !== 'Component'
626
+ ? { type: 'Component', properties: [], children: [p.object] }
627
+ : p.object,
628
+ slot ?? record,
629
+ ctx,
630
+ chain,
631
+ );
632
+ record.properties.set(p.key, {
633
+ p: {
634
+ ...record.properties.get(p.key).p,
635
+ reference: child,
636
+ collectionSlot: !!slot,
637
+ },
638
+ ctx,
639
+ });
640
+ }
641
+ function build(object, parent, ctx, chain) {
642
+ const inline = ctx.doc.inlineScopes?.get(object)?.get(object.type);
643
+ const resolution = inline
644
+ ? { url: ctx.doc.url + '#inline-' + object.type }
645
+ : ctx.doc.types.get(object.type);
646
+ if (resolution?.url) {
647
+ if (chain.includes(resolution.url) || chain.length > 48)
648
+ throw Error(
649
+ `Recursive QML component: ${[...chain, resolution.url].join(' -> ')}`,
650
+ );
651
+ const inner = context(
652
+ inline ? ctx.doc : graph.documents.get(resolution.url),
653
+ parent,
654
+ [...chain, resolution.url],
655
+ null,
656
+ inline,
657
+ inline ? ctx : null,
658
+ );
659
+ const record = inner.root;
660
+ record.tags.add(object.type.split('.').at(-1));
661
+ if (!ctx.root) ctx.root = record;
662
+ addProperties(record, object, ctx);
663
+ for (const p of object.properties)
664
+ if (
665
+ p.object &&
666
+ !(
667
+ ['Repeater', 'ListView', 'GridView'].includes(record.type) &&
668
+ p.key === 'delegate'
669
+ )
670
+ )
671
+ buildPropertyObject(p, record, ctx, chain);
672
+ const slot = defaultSlot(record);
673
+ for (const child of object.children) build(child, slot, ctx, chain);
674
+ return record;
675
+ }
676
+ const type = resolution?.builtin ?? object.type;
677
+ if (!types.has(type)) throw Error(`Unsupported QML type: ${object.type}`);
678
+ if (records.length >= 2000) throw Error('QML expanded object limit');
679
+ const svgTag = { ShapePath: 'g', PathSvg: 'path', PathRectangle: 'rect' }[
680
+ type
681
+ ];
682
+ const element = svgTag
683
+ ? document.createElementNS('http://www.w3.org/2000/svg', svgTag)
684
+ : document.createElement(
685
+ ['TextField', 'TextInput'].includes(type)
686
+ ? 'input'
687
+ : type === 'TextEdit'
688
+ ? 'textarea'
689
+ : type === 'Image'
690
+ ? 'div'
691
+ : type === 'Button'
692
+ ? 'button'
693
+ : 'div',
694
+ );
695
+ const record = {
696
+ index: records.length,
697
+ type,
698
+ parent,
699
+ ctx,
700
+ element,
701
+ template,
702
+ bindingTarget: object.bindingTarget,
703
+ tags: new Set([
704
+ type,
705
+ ...(!nonvisualTypes.has(type) ? ['Item'] : []),
706
+ 'QtObject',
707
+ ]),
708
+ names: new Set([
709
+ 'objectName',
710
+ ...(nonvisualTypes.has(type) ? [] : itemProperties(type)),
711
+ ...(typeProperties[type] ?? []),
712
+ ]),
713
+ properties: new Map(),
714
+ aliases: new Map(),
715
+ nonvisual: nonvisualTypes.has(type),
716
+ };
717
+ records.push(record);
718
+ if (!ctx.root) {
719
+ ctx.root = record;
720
+ if (ctx.singletonUrl) singletons.set(ctx.singletonUrl, record);
721
+ }
722
+ if (record.nonvisual)
723
+ element.style.display = type === 'ShellRoot' ? 'contents' : 'none';
724
+ element.setAttribute('data-qml-node', String(record.index));
725
+ element.setAttribute('data-qml-type', type);
726
+ if (type === 'TextField') element.setAttribute('type', 'text');
727
+ if (type === 'Button') element.setAttribute('type', 'button');
728
+ (parent?.element ?? document.body).appendChild(element);
729
+ addProperties(record, object, ctx);
730
+ if (['Repeater', 'ListView', 'GridView', 'Component'].includes(type)) {
731
+ const delegate =
732
+ type === 'Component'
733
+ ? object.children[0]
734
+ : (record.properties.get('delegate')?.p.object ??
735
+ (type === 'Repeater' ? object.children[0] : null));
736
+ if (!delegate) throw Error(type + ' requires an object delegate');
737
+ if (type === 'Component' && object.children.length !== 1)
738
+ throw Error('Component requires exactly one root');
739
+ record.properties.delete('delegate');
740
+ const outer = template;
741
+ const group = {
742
+ owner: record,
743
+ outer,
744
+ root: null,
745
+ kind:
746
+ type === 'Component'
747
+ ? 'component'
748
+ : type === 'Repeater'
749
+ ? 'repeater'
750
+ : 'view',
751
+ };
752
+ template = group;
753
+ const delegateContext = {
754
+ ...ctx,
755
+ ids: new Map(),
756
+ outer: ctx,
757
+ template: group,
758
+ };
759
+ // Delegate IDs are local to each factory invocation; outer IDs remain visible.
760
+ for (const p of delegate.properties)
761
+ if (p.key === 'id') delegateContext.ids.delete(p.code);
762
+ group.root = build(
763
+ delegate,
764
+ ['ListView', 'GridView'].includes(type) ? record : parent,
765
+ delegateContext,
766
+ chain,
767
+ );
768
+ group.root.element.remove();
769
+ group.root.names.add('index');
770
+ group.root.names.add('modelData');
771
+ record.delegate = group;
772
+ template = outer;
773
+ }
774
+ for (const p of object.properties)
775
+ if (
776
+ p.object &&
777
+ !(
778
+ ['Repeater', 'ListView', 'GridView'].includes(record.type) &&
779
+ p.key === 'delegate'
780
+ )
781
+ )
782
+ buildPropertyObject(p, record, ctx, chain);
783
+ if (
784
+ type !== 'Component' &&
785
+ !(
786
+ type === 'Repeater' &&
787
+ !object.properties.some((p) => p.key === 'delegate')
788
+ )
789
+ )
790
+ for (const child of object.children) build(child, record, ctx, chain);
791
+ return record;
792
+ }
793
+ let entryContext;
794
+ entryContext = context(graph.documents.get(graph.entry), null, [
795
+ graph.entry,
796
+ ]);
797
+ for (const record of records)
798
+ for (const { p } of record.properties.values())
799
+ if (
800
+ p.required &&
801
+ !(component && record === entryContext.root) &&
802
+ !(
803
+ record.template?.kind === 'component' &&
804
+ record.template.root === record
805
+ ) &&
806
+ !(
807
+ record.template?.root === record &&
808
+ ['index', 'modelData'].includes(p.key)
809
+ )
810
+ )
811
+ throw Error(`Missing required QML property: ${p.key}`);
812
+ // Local createComponent factories already belong to a running QML page.
813
+ // Do not parse a second copy of the entire runtime for every lazy pane.
814
+ const runtimeSource = component
815
+ ? 'globalThis.__qmlRuntime'
816
+ : `globalThis.__qmlRuntime??=(${createQmlRuntime.toString()})((${createReactive.toString()})(),(${createQt.toString()})(),(${createQmlModels.toString()}),(${createQmlText.toString()}),(${createQmlShapes.toString()}),(${createQmlImages.toString()}),(${createQmlTextures.toString()}),(${createQmlMedia.toString()}))`;
817
+ const code = [
818
+ `(()=>{const r=${runtimeSource};if(!r)throw Error('QML component requires an initialized page');const ${runtimeRef}=r;`,
819
+ constants,
820
+ ];
821
+
822
+ function scopeFor(record, ctx) {
823
+ const scope = new Map();
824
+ const namespaces = new Map();
825
+ for (const [name, value] of ctx.values) {
826
+ const parts = name.split('.');
827
+ if (parts.length === 1) scope.set(name, value);
828
+ else {
829
+ if (!namespaces.has(parts[0])) namespaces.set(parts[0], []);
830
+ namespaces
831
+ .get(parts[0])
832
+ .push(`[${JSON.stringify(parts[1])}]:${value}`);
833
+ }
834
+ }
835
+ for (const [name, parts] of namespaces)
836
+ scope.set(name, `({${parts.join(',')}})`);
837
+ const idScopes = [];
838
+ for (let c = ctx; c; c = c.outer) idScopes.unshift(c);
839
+ for (const c of idScopes)
840
+ for (const [name, value] of c.ids) scope.set(name, value);
841
+ // A local component instance can acquire properties at its use site;
842
+ // children declared there must resolve those enclosing properties too.
843
+ const ancestors = [];
844
+ for (let p = record; p; p = p.parent)
845
+ if (p.ctx === ctx || p === record ||
846
+ [...p.properties.values()].some(entry => entry.ctx === ctx)) ancestors.unshift(p);
847
+ if (!ancestors.includes(ctx.root)) ancestors.unshift(ctx.root);
848
+ for (const p of ancestors)
849
+ for (const key of p.names) scope.set(key, `${ref(p)}.${key}`);
850
+ // Component IDs win over same-named properties (e.g. a caption string and caption Text).
851
+ for (const c of idScopes)
852
+ for (const [name, value] of c.ids) scope.set(name, value);
853
+ scope.set(
854
+ 'parent',
855
+ record.parent || record.template || component
856
+ ? `${ref(record)}.parent`
857
+ : runtimeRef + '.viewport',
858
+ );
859
+ for (const name of ['ListView', 'GridView'])
860
+ scope.set(name, `({...${name},view:${ref(record)}.${name}.view})`);
861
+ scope.set('console', runtimeRef + '.console');
862
+ scope.set('Qt', `${runtimeRef}.qt(${JSON.stringify(ctx.doc.url)})`);
863
+ return scope;
864
+ }
865
+ const scriptCode = new Map();
866
+ for (const { name, deps, item, ctx, library, href } of scripts) {
867
+ const scope = library ? new Map() : scopeFor(ctx.root, ctx);
868
+ // Library scripts have no component IDs, but still receive Qt services.
869
+ scope.set('Qt', `${runtimeRef}.qt(${JSON.stringify(href)})`);
870
+ scope.set('console', runtimeRef + '.console');
871
+ for (const dep of item.deps) scope.delete(dep.alias);
872
+ const body = expression(
873
+ { code: item.source, ast: item.ast, block: false },
874
+ scope,
875
+ );
876
+ const scriptGroup = library ? null : ctx.template;
877
+ if (!scriptCode.has(scriptGroup)) scriptCode.set(scriptGroup, []);
878
+ scriptCode
879
+ .get(scriptGroup)
880
+ .push(
881
+ `const ${name}=(()=>{${deps}\n${body}\nreturn Object.defineProperties({}, {${item.exports.map((e) => `[${JSON.stringify(e.name)}]:{enumerable:true,get:()=>${e.name}${e.writable ? `,set:v=>{${e.name}=v;}` : ''}}`).join(',')}});})();`,
882
+ );
883
+ }
884
+ function generate(group) {
885
+ const members = records.filter((record) => record.template === group);
886
+ for (const record of members) {
887
+ if (
888
+ group &&
889
+ ![
890
+ 'ShaderEffectSource',
891
+ 'LinearGradient',
892
+ 'XmlListModel',
893
+ 'XmlListModelRole',
894
+ 'WorkerScript',
895
+ 'LoggingCategory',
896
+ 'Flow',
897
+ 'RowLayout',
898
+ 'ColumnLayout',
899
+ 'ListView',
900
+ 'GridView',
901
+ 'ListModel',
902
+ 'DelegateModel',
903
+ 'DelegateModelGroup',
904
+ 'PointHandler',
905
+ 'Binding',
906
+ 'Gradient',
907
+ 'GradientStop',
908
+ 'PathRectangle',
909
+ 'IpcHandler',
910
+ 'DropArea',
911
+ 'DragHandler',
912
+ 'FrameAnimation',
913
+ 'Image',
914
+ 'Flickable',
915
+ 'TextInput',
916
+ 'TextEdit',
917
+ 'Behavior',
918
+ 'NumberAnimation',
919
+ 'PropertyAnimation',
920
+ 'PauseAnimation',
921
+ 'SequentialAnimation',
922
+ 'ParallelAnimation',
923
+ 'ScriptAction',
924
+ 'PropertyAction',
925
+ 'Timer',
926
+ 'Connections',
927
+ 'Process',
928
+ 'StdioCollector',
929
+ 'SplitParser',
930
+ 'FileView',
931
+ 'TextMetrics',
932
+ 'FontMetrics',
933
+ 'QtObject',
934
+ 'Item',
935
+ 'Rectangle',
936
+ 'Text',
937
+ 'Label',
938
+ 'TextField',
939
+ 'Button',
940
+ 'MouseArea',
941
+ 'Column',
942
+ 'Row',
943
+ 'Grid',
944
+ 'Shape',
945
+ 'ShapePath',
946
+ 'PathSvg',
947
+ 'Scale',
948
+ 'Translate',
949
+ 'FocusScope',
950
+ 'Loader',
951
+ 'Component',
952
+ 'Repeater',
953
+ 'TapHandler',
954
+ 'HoverHandler',
955
+ ].includes(record.type)
956
+ )
957
+ throw Error(
958
+ 'Unsupported service inside Repeater delegate: ' + record.type,
959
+ );
960
+ let persistent = false;
961
+ for (let a = record; a; a = a.parent)
962
+ if (a.ctx.singletonUrl) persistent = true;
963
+ const parent =
964
+ group?.kind === 'component' && record === group.root
965
+ ? 'mount'
966
+ : record.parent
967
+ ? ref(record.parent)
968
+ : component && record === entryContext.root
969
+ ? 'mount'
970
+ : 'null';
971
+ if (group || component)
972
+ code.push(
973
+ `const ${ref(record)}=r.create(${JSON.stringify(record.type)},${record.index},${parent},${record === group?.root && group.kind === 'repeater' ? ref(group.owner) : 'null'},${persistent});`,
974
+ );
975
+ else
976
+ code.push(
977
+ `const ${ref(record)}=r.make(document.querySelector('[data-qml-node="${record.index}"]'),${JSON.stringify(record.type)},${parent});`,
978
+ );
979
+ }
980
+ for (const record of members)
981
+ for (const { p } of record.properties.values())
982
+ if (p.custom && p.propertyType !== 'alias') {
983
+ const initial = ['int', 'real', 'double'].includes(p.propertyType)
984
+ ? '0'
985
+ : p.propertyType === 'bool'
986
+ ? 'false'
987
+ : ['string', 'url'].includes(p.propertyType)
988
+ ? '""'
989
+ : p.propertyType === 'color'
990
+ ? '"transparent"'
991
+ : p.propertyType === 'var'
992
+ ? 'undefined'
993
+ : 'null';
994
+ code.push(`${ref(record)}[${JSON.stringify(p.key)}]=${initial};`);
995
+ }
996
+ for (const record of members)
997
+ code.push(
998
+ `r.describe(${ref(record)},${JSON.stringify([...record.tags])},${JSON.stringify([...record.properties.keys()])},${JSON.stringify(record.ctx.doc.url)},${JSON.stringify([...record.properties.values()].filter(({ p }) => p.readonly).map(({ p }) => p.key))});`,
999
+ );
1000
+ for (const record of members)
1001
+ if (record.type === 'Behavior') {
1002
+ const animation = members.find((child) => child.parent === record);
1003
+ if (!record.parent || !record.bindingTarget || !animation)
1004
+ throw Error('Behavior needs a target property and animation');
1005
+ code.push(
1006
+ `r.behavior(${ref(record.parent)},${JSON.stringify(record.bindingTarget)},${ref(record)},${ref(animation)});`,
1007
+ );
1008
+ }
1009
+ if (!group)
1010
+ for (const [url, record] of singletons)
1011
+ if (!record.external)
1012
+ code.push(`r.share(${JSON.stringify(url)},${ref(record)});`);
1013
+ code.push(...(scriptCode.get(group) ?? []));
1014
+ if (group && group.kind !== 'component')
1015
+ code.push(
1016
+ `${ref(group.root)}.index=index;${ref(group.root)}.modelData=modelData;r.attachDelegate(${ref(group.root)},index,${ref(group.owner)});`,
1017
+ );
1018
+ for (const record of members)
1019
+ if (record.delegate) {
1020
+ code.push(
1021
+ record.delegate.kind === 'component'
1022
+ ? `r.component(${ref(record)},(mount,properties={})=>{`
1023
+ : `r.delegate(${ref(record)},(index,modelData)=>{`,
1024
+ );
1025
+ generate(record.delegate);
1026
+ code.push(`return ${ref(record.delegate.root)};});`);
1027
+ }
1028
+ // Methods/aliases exist before any property bindings read them.
1029
+ for (const record of members)
1030
+ for (const { p, ctx } of record.properties.values()) {
1031
+ const scope = scopeFor(record, ctx);
1032
+ if (p.signal) code.push(`${ref(record)}.${p.key}=r.signal();`);
1033
+ if (p.reference && !p.collectionSlot && !p.aliasOverride)
1034
+ code.push(`${ref(record)}.${p.key}=${ref(p.reference)};`);
1035
+ if (p.method) {
1036
+ const expr = expression(p, scope);
1037
+ if (
1038
+ record.type === 'Connections' &&
1039
+ /^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
1040
+ p.key,
1041
+ )
1042
+ )
1043
+ code.push(
1044
+ `r.on(${ref(record)},${JSON.stringify(p.key)},(${expr}));`,
1045
+ );
1046
+ else code.push(`${ref(record)}.${p.key}=(${expr});`);
1047
+ }
1048
+ }
1049
+ for (const record of members)
1050
+ for (const { p, ctx } of record.aliases.values()) {
1051
+ const scope = scopeFor(record, ctx);
1052
+ if (p.propertyType === 'alias') {
1053
+ if (!['Identifier', 'MemberExpression'].includes(p.ast.type))
1054
+ throw Error(`Invalid QML alias ${p.key}`);
1055
+ const aliasScope = new Map(scope);
1056
+ for (let c = ctx; c; c = c.outer)
1057
+ for (const [name, id] of c.ids)
1058
+ if (!aliasScope.has(name) || ctx.ids.has(name))
1059
+ aliasScope.set(name, id);
1060
+ const value = expression(p, aliasScope);
1061
+ code.push(
1062
+ `r.alias(${ref(record)},${JSON.stringify(p.key)},()=>(${value}),v=>{${value}=v;},${!!p.readonly});`,
1063
+ );
1064
+ }
1065
+ }
1066
+ const completed = [];
1067
+ for (const record of members) {
1068
+ const n = ref(record),
1069
+ override =
1070
+ (component && record === entryContext.root) ||
1071
+ (group?.kind === 'component' && record === group.root);
1072
+ for (const dimension of ['width', 'height'])
1073
+ if (!record.nonvisual && !record.properties.has(dimension)) {
1074
+ const implicit =
1075
+ dimension === 'width' ? 'implicitWidth' : 'implicitHeight';
1076
+ code.push(
1077
+ `${override ? `if(!Object.hasOwn(properties,'${dimension}')) ` : ''}r.bind(${n},'${dimension}',()=>r.dimension(${n},${JSON.stringify(dimension)},${!record.parent || record.type === 'FloatingWindow'}));`,
1078
+ );
1079
+ }
1080
+ for (const axis of ['x', 'y'])
1081
+ if (!record.properties.has(axis) && !record.nonvisual)
1082
+ code.push(
1083
+ `${override ? `if(!Object.hasOwn(properties,'${axis}')) ` : ''}r.bind(${n},'${axis}',()=>r.position(${n},'${axis}'));`,
1084
+ );
1085
+ for (const { p, ctx } of [...(record.inheritedHandlers ?? []), ...record.properties.values()]) {
1086
+ if (p.method || p.signal || p.propertyType === 'alias' || p.required)
1087
+ continue;
1088
+ if (p.collectionSlot) continue;
1089
+ if (p.reference) {
1090
+ code.push(
1091
+ `r.${p.aliasOverride ? 'bindAlias' : 'bind'}(${n},${JSON.stringify(p.key)},()=>${ref(p.reference)},${!!p.readonly});`,
1092
+ );
1093
+ continue;
1094
+ }
1095
+ const value = expression(p, scopeFor(record, ctx));
1096
+ if (p.aliasOverride) {
1097
+ code.push(
1098
+ `r.bindAlias(${n},${JSON.stringify(p.key)},()=>(${value}));`,
1099
+ );
1100
+ continue;
1101
+ }
1102
+ if (record.type === 'ScriptAction' && p.key === 'script') {
1103
+ code.push(`r.on(${n},'script',()=>{${value};});`);
1104
+ continue;
1105
+ }
1106
+ if (p.key === 'Component.onDestruction') {
1107
+ code.push(`r.on(${n},'Component.onDestruction',()=>{${value};});`);
1108
+ continue;
1109
+ }
1110
+ if (p.key === 'Component.onCompleted') {
1111
+ completed.push(`${value};`);
1112
+ continue;
1113
+ }
1114
+ if (
1115
+ /^(?:Keys\.|Accessible\.|Drag\.|ListView\.|GridView\.)?on[A-Z_]/.test(
1116
+ p.key,
1117
+ ) &&
1118
+ p.key !== 'onClicked'
1119
+ ) {
1120
+ const handler = [
1121
+ 'FunctionExpression',
1122
+ 'ArrowFunctionExpression',
1123
+ ].includes(p.ast?.type)
1124
+ ? value
1125
+ : `function(event,button){const mouse=event,wheel=event;${value};}`;
1126
+ code.push(`r.on(${n},${JSON.stringify(p.key)},${handler});`);
1127
+ continue;
1128
+ }
1129
+ if (p.key === 'onClicked') {
1130
+ const handler = [
1131
+ 'FunctionExpression',
1132
+ 'ArrowFunctionExpression',
1133
+ ].includes(p.ast?.type)
1134
+ ? value
1135
+ : `function(event){const mouse=event;${value};}`;
1136
+ code.push(`r.on(${n},'onClicked',${handler});`);
1137
+ continue;
1138
+ }
1139
+ const parts = p.key.split('.'),
1140
+ key = parts.pop(),
1141
+ target = n + (parts.length ? '.' + parts.join('.') : '');
1142
+ code.push(
1143
+ `${override ? `if(!Object.hasOwn(properties,${JSON.stringify(p.key)})) ` : ''}r.bind(${target},${JSON.stringify(key)},()=>(${value}),${!!p.readonly},${JSON.stringify(p.propertyType)});`,
1144
+ );
1145
+ }
1146
+ if (record.nonvisual) code.push(`${n}.visible=false;`);
1147
+ }
1148
+ if ((component && !group) || group?.kind === 'component') {
1149
+ const root = group?.root ?? entryContext.root;
1150
+ for (const { p } of root.properties.values())
1151
+ if (p.required)
1152
+ code.push(
1153
+ `if(!Object.hasOwn(properties,${JSON.stringify(p.key)}))throw Error('Missing required QML property: '+${JSON.stringify(p.key)});`,
1154
+ );
1155
+ code.push(`r.apply(${ref(root)},properties);`);
1156
+ }
1157
+ code.push('r.start();', ...completed);
1158
+ }
1159
+ generate(null);
1160
+ code.push(`return ${ref(entryContext.root)};})();`);
1161
+ const scriptElement = document.createElement('script');
1162
+ scriptElement.textContent = code.join('\n');
1163
+ document.body.appendChild(scriptElement);
1164
+ } catch (error) {
1165
+ document.body.replaceChildren();
1166
+ document.body.style.cssText = 'margin:16px';
1167
+ const heading = document.createElement('h1');
1168
+ heading.textContent = 'QML experiment';
1169
+ const message = document.createElement('p');
1170
+ message.textContent = String(error.message);
1171
+ const pre = document.createElement('pre');
1172
+ pre.textContent = source;
1173
+ document.body.append(heading, message, pre);
1174
+ }
1175
+ return { window, document };
1176
+ }