@react-x11/components 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.
Files changed (314) hide show
  1. package/README.md +131 -4
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +11 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +26 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/qml/containers.d.ts +3 -0
  140. package/dist/qml/containers.d.ts.map +1 -0
  141. package/dist/qml/containers.js +331 -0
  142. package/dist/qml/containers.js.map +1 -0
  143. package/dist/qml/controls.d.ts +12 -0
  144. package/dist/qml/controls.d.ts.map +1 -0
  145. package/dist/qml/controls.js +21 -0
  146. package/dist/qml/controls.js.map +1 -0
  147. package/dist/qml/globals.d.ts +6 -0
  148. package/dist/qml/globals.d.ts.map +1 -0
  149. package/dist/qml/globals.js +25 -0
  150. package/dist/qml/globals.js.map +1 -0
  151. package/dist/qml/index.d.ts +15 -0
  152. package/dist/qml/index.d.ts.map +1 -0
  153. package/dist/qml/index.js +25 -0
  154. package/dist/qml/index.js.map +1 -0
  155. package/dist/qml/interaction.d.ts +12 -0
  156. package/dist/qml/interaction.d.ts.map +1 -0
  157. package/dist/qml/interaction.js +246 -0
  158. package/dist/qml/interaction.js.map +1 -0
  159. package/dist/qml/ir.d.ts +106 -0
  160. package/dist/qml/ir.d.ts.map +1 -0
  161. package/dist/qml/ir.js +7 -0
  162. package/dist/qml/ir.js.map +1 -0
  163. package/dist/qml/layouts.d.ts +3 -0
  164. package/dist/qml/layouts.d.ts.map +1 -0
  165. package/dist/qml/layouts.js +122 -0
  166. package/dist/qml/layouts.js.map +1 -0
  167. package/dist/qml/models.d.ts +14 -0
  168. package/dist/qml/models.d.ts.map +1 -0
  169. package/dist/qml/models.js +80 -0
  170. package/dist/qml/models.js.map +1 -0
  171. package/dist/qml/objects.d.ts +228 -0
  172. package/dist/qml/objects.d.ts.map +1 -0
  173. package/dist/qml/objects.js +1103 -0
  174. package/dist/qml/objects.js.map +1 -0
  175. package/dist/qml/parse.d.ts +10 -0
  176. package/dist/qml/parse.d.ts.map +1 -0
  177. package/dist/qml/parse.js +607 -0
  178. package/dist/qml/parse.js.map +1 -0
  179. package/dist/qml/qtquick.d.ts +7 -0
  180. package/dist/qml/qtquick.d.ts.map +1 -0
  181. package/dist/qml/qtquick.js +347 -0
  182. package/dist/qml/qtquick.js.map +1 -0
  183. package/dist/qml/react.d.ts +83 -0
  184. package/dist/qml/react.d.ts.map +1 -0
  185. package/dist/qml/react.js +323 -0
  186. package/dist/qml/react.js.map +1 -0
  187. package/dist/qml/resolver.d.ts +38 -0
  188. package/dist/qml/resolver.d.ts.map +1 -0
  189. package/dist/qml/resolver.js +64 -0
  190. package/dist/qml/resolver.js.map +1 -0
  191. package/dist/qml/slots.d.ts +65 -0
  192. package/dist/qml/slots.d.ts.map +1 -0
  193. package/dist/qml/slots.js +230 -0
  194. package/dist/qml/slots.js.map +1 -0
  195. package/dist/qml/states.d.ts +5 -0
  196. package/dist/qml/states.d.ts.map +1 -0
  197. package/dist/qml/states.js +276 -0
  198. package/dist/qml/states.js.map +1 -0
  199. package/dist/qml/view-utils.d.ts +42 -0
  200. package/dist/qml/view-utils.d.ts.map +1 -0
  201. package/dist/qml/view-utils.js +121 -0
  202. package/dist/qml/view-utils.js.map +1 -0
  203. package/dist/reorder/hx.d.ts +18 -0
  204. package/dist/reorder/hx.d.ts.map +1 -0
  205. package/dist/reorder/hx.js +10 -0
  206. package/dist/reorder/hx.js.map +1 -0
  207. package/dist/reorder/index.d.ts +314 -0
  208. package/dist/reorder/index.d.ts.map +1 -0
  209. package/dist/reorder/index.js +1539 -0
  210. package/dist/reorder/index.js.map +1 -0
  211. package/dist/reorder/model.d.ts +107 -0
  212. package/dist/reorder/model.d.ts.map +1 -0
  213. package/dist/reorder/model.js +162 -0
  214. package/dist/reorder/model.js.map +1 -0
  215. package/dist/richtext/node.d.ts +15 -5
  216. package/dist/richtext/node.d.ts.map +1 -1
  217. package/dist/richtext/node.js +40 -11
  218. package/dist/richtext/node.js.map +1 -1
  219. package/dist/richtext/runs.d.ts +35 -13
  220. package/dist/richtext/runs.d.ts.map +1 -1
  221. package/dist/richtext/runs.js +83 -27
  222. package/dist/richtext/runs.js.map +1 -1
  223. package/dist/tabs/hx.d.ts +18 -0
  224. package/dist/tabs/hx.d.ts.map +1 -0
  225. package/dist/tabs/hx.js +10 -0
  226. package/dist/tabs/hx.js.map +1 -0
  227. package/dist/tabs/index.d.ts +160 -0
  228. package/dist/tabs/index.d.ts.map +1 -0
  229. package/dist/tabs/index.js +1265 -0
  230. package/dist/tabs/index.js.map +1 -0
  231. package/dist/terminal/vt/fonts.d.ts +45 -15
  232. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  233. package/dist/terminal/vt/fonts.js +89 -19
  234. package/dist/terminal/vt/fonts.js.map +1 -1
  235. package/dist/terminal/vt/index.d.ts +1 -1
  236. package/dist/terminal/vt/index.d.ts.map +1 -1
  237. package/dist/terminal/vt/index.js +3 -3
  238. package/dist/terminal/vt/index.js.map +1 -1
  239. package/dist/terminal/vt/node.d.ts +42 -0
  240. package/dist/terminal/vt/node.d.ts.map +1 -1
  241. package/dist/terminal/vt/node.js +95 -11
  242. package/dist/terminal/vt/node.js.map +1 -1
  243. package/dist/terminal/vt/pty.d.ts +16 -0
  244. package/dist/terminal/vt/pty.d.ts.map +1 -1
  245. package/dist/terminal/vt/pty.js +186 -0
  246. package/dist/terminal/vt/pty.js.map +1 -1
  247. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  248. package/dist/terminal/vt/renderer.js +13 -3
  249. package/dist/terminal/vt/renderer.js.map +1 -1
  250. package/package.json +23 -3
  251. package/src/charts/components.ts +14 -7
  252. package/src/charts/node.ts +81 -26
  253. package/src/charts/render.ts +16 -6
  254. package/src/code-editor/node.ts +147 -53
  255. package/src/flow/draw.ts +104 -35
  256. package/src/flow/index.ts +33 -13
  257. package/src/flow/model.ts +4 -2
  258. package/src/flow/node.ts +158 -59
  259. package/src/flow/types.ts +46 -21
  260. package/src/formula/index.ts +4 -2
  261. package/src/formula/node.ts +24 -5
  262. package/src/html/controls.ts +4 -1
  263. package/src/html/css/cascade.ts +18 -7
  264. package/src/html/css/parse.ts +46 -21
  265. package/src/html/css/style.ts +6 -1
  266. package/src/html/css/values.ts +25 -9
  267. package/src/html/index.ts +1 -0
  268. package/src/html/layout/boxes.ts +19 -9
  269. package/src/html/layout/inline.ts +23 -1
  270. package/src/html/node.ts +110 -17
  271. package/src/html/paint.ts +17 -2
  272. package/src/index.ts +142 -0
  273. package/src/maps/clip.ts +184 -0
  274. package/src/maps/gzip.ts +66 -0
  275. package/src/maps/index.ts +316 -0
  276. package/src/maps/labels.ts +442 -0
  277. package/src/maps/mvt.ts +857 -0
  278. package/src/maps/node.ts +1819 -0
  279. package/src/maps/overlay.ts +639 -0
  280. package/src/maps/paint.ts +978 -0
  281. package/src/maps/proj.ts +641 -0
  282. package/src/maps/sources.ts +425 -0
  283. package/src/maps/style.ts +309 -0
  284. package/src/maps/styles.ts +1040 -0
  285. package/src/maps/tiles.ts +676 -0
  286. package/src/maps/types.ts +260 -0
  287. package/src/qml/containers.tsx +380 -0
  288. package/src/qml/controls.tsx +47 -0
  289. package/src/qml/globals.ts +33 -0
  290. package/src/qml/index.ts +67 -0
  291. package/src/qml/interaction.tsx +317 -0
  292. package/src/qml/ir.ts +80 -0
  293. package/src/qml/layouts.tsx +141 -0
  294. package/src/qml/models.ts +101 -0
  295. package/src/qml/objects.ts +1431 -0
  296. package/src/qml/parse.ts +624 -0
  297. package/src/qml/qtquick.tsx +413 -0
  298. package/src/qml/react.tsx +454 -0
  299. package/src/qml/resolver.ts +99 -0
  300. package/src/qml/slots.ts +249 -0
  301. package/src/qml/states.ts +318 -0
  302. package/src/qml/view-utils.ts +140 -0
  303. package/src/reorder/hx.ts +31 -0
  304. package/src/reorder/index.ts +2223 -0
  305. package/src/reorder/model.ts +232 -0
  306. package/src/richtext/node.ts +49 -14
  307. package/src/richtext/runs.ts +115 -30
  308. package/src/tabs/hx.ts +31 -0
  309. package/src/tabs/index.ts +1686 -0
  310. package/src/terminal/vt/fonts.ts +128 -36
  311. package/src/terminal/vt/index.ts +4 -2
  312. package/src/terminal/vt/node.ts +105 -12
  313. package/src/terminal/vt/pty.ts +243 -0
  314. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,624 @@
1
+ // QML document → IR. Zero dependencies.
2
+ //
3
+ // This does not parse the JavaScript inside bindings — it finds where each
4
+ // expression ends (bracket depth + string/comment awareness + a
5
+ // QML-flavoured newline rule) and hands the source text to the runtime,
6
+ // which compiles it with `new Function`. That split is what keeps this
7
+ // small: QML's object grammar is tiny; JavaScript is the part we refuse to
8
+ // re-implement.
9
+ //
10
+ // Known, accepted holes (docs/components/qml.md, "the parser"): regex
11
+ // literals containing brackets or quotes, and a ternary branch that lands a
12
+ // bare `identifier :` at the start of a line. Real QML corpus hits neither.
13
+
14
+ import type {
15
+ AliasTargetIR,
16
+ BindingMemberIR,
17
+ MemberIR,
18
+ ObjectIR,
19
+ QmlDocument,
20
+ QmlImport,
21
+ ValueIR,
22
+ } from './ir.js';
23
+
24
+ const KEYWORDS = new Set([
25
+ 'import',
26
+ 'pragma',
27
+ 'property',
28
+ 'signal',
29
+ 'function',
30
+ 'readonly',
31
+ 'default',
32
+ 'required',
33
+ 'component',
34
+ 'enum',
35
+ 'on',
36
+ 'as',
37
+ ]);
38
+
39
+ export class ParseError extends Error {
40
+ line: number;
41
+ col: number;
42
+ constructor(message: string, source: string, pos: number) {
43
+ const { line, col } = lineCol(source, pos);
44
+ super(`QML parse error at ${line}:${col}: ${message}`);
45
+ this.line = line;
46
+ this.col = col;
47
+ }
48
+ }
49
+
50
+ function lineCol(source: string, pos: number): { line: number; col: number } {
51
+ let line = 1;
52
+ let last = 0;
53
+ for (let i = 0; i < pos && i < source.length; i++) {
54
+ if (source[i] === '\n') {
55
+ line++;
56
+ last = i + 1;
57
+ }
58
+ }
59
+ return { line, col: pos - last + 1 };
60
+ }
61
+
62
+ const isIdStart = (c: string) => /[A-Za-z_$]/.test(c);
63
+ const isIdChar = (c: string) => /[A-Za-z0-9_$]/.test(c);
64
+ const isUpper = (c: string) => c >= 'A' && c <= 'Z';
65
+
66
+ class Scanner {
67
+ source: string;
68
+ pos = 0;
69
+
70
+ constructor(source: string) {
71
+ this.source = source;
72
+ }
73
+
74
+ error(message: string, pos = this.pos): ParseError {
75
+ return new ParseError(message, this.source, pos);
76
+ }
77
+
78
+ atEnd(): boolean {
79
+ return this.pos >= this.source.length;
80
+ }
81
+
82
+ /** Skip whitespace and comments. Returns true if a newline was crossed. */
83
+ skipTrivia(): boolean {
84
+ const src = this.source;
85
+ let newline = false;
86
+ while (this.pos < src.length) {
87
+ const c = src[this.pos];
88
+ if (c === '\n') {
89
+ newline = true;
90
+ this.pos++;
91
+ } else if (c === ' ' || c === '\t' || c === '\r') {
92
+ this.pos++;
93
+ } else if (c === '/' && src[this.pos + 1] === '/') {
94
+ while (this.pos < src.length && src[this.pos] !== '\n') this.pos++;
95
+ } else if (c === '/' && src[this.pos + 1] === '*') {
96
+ const end = src.indexOf('*/', this.pos + 2);
97
+ if (end === -1) throw this.error('unterminated comment');
98
+ if (src.slice(this.pos, end).includes('\n')) newline = true;
99
+ this.pos = end + 2;
100
+ } else {
101
+ break;
102
+ }
103
+ }
104
+ return newline;
105
+ }
106
+
107
+ peekChar(): string | undefined {
108
+ const save = this.pos;
109
+ this.skipTrivia();
110
+ const c = this.source[this.pos];
111
+ this.pos = save;
112
+ return c;
113
+ }
114
+
115
+ /** The identifier at the next token position, or null. Does not consume. */
116
+ peekIdent(): string | null {
117
+ const save = this.pos;
118
+ this.skipTrivia();
119
+ const id = this.tryIdent();
120
+ this.pos = save;
121
+ return id;
122
+ }
123
+
124
+ tryIdent(): string | null {
125
+ if (this.atEnd() || !isIdStart(this.source[this.pos])) return null;
126
+ const start = this.pos;
127
+ while (this.pos < this.source.length && isIdChar(this.source[this.pos]))
128
+ this.pos++;
129
+ return this.source.slice(start, this.pos);
130
+ }
131
+
132
+ expectIdent(what = 'identifier'): string {
133
+ this.skipTrivia();
134
+ const id = this.tryIdent();
135
+ if (!id) throw this.error(`expected ${what}`);
136
+ return id;
137
+ }
138
+
139
+ tryPunct(p: string): boolean {
140
+ const save = this.pos;
141
+ this.skipTrivia();
142
+ if (this.source.startsWith(p, this.pos)) {
143
+ this.pos += p.length;
144
+ return true;
145
+ }
146
+ this.pos = save;
147
+ return false;
148
+ }
149
+
150
+ expectPunct(p: string): void {
151
+ if (!this.tryPunct(p)) throw this.error(`expected '${p}'`);
152
+ }
153
+
154
+ /** A dotted name: `anchors.fill`, `Component.onCompleted`. */
155
+ dottedName(): string[] {
156
+ const parts = [this.expectIdent()];
157
+ while (this.tryPunct('.')) parts.push(this.expectIdent());
158
+ return parts;
159
+ }
160
+
161
+ // --- raw-source slurping ---------------------------------------------
162
+
163
+ /** Consume a string/template literal (opening quote at `pos`). */
164
+ skipString(): void {
165
+ const src = this.source;
166
+ const quote = src[this.pos];
167
+ this.pos++;
168
+ while (this.pos < src.length) {
169
+ const c = src[this.pos];
170
+ if (c === '\\') {
171
+ this.pos += 2;
172
+ continue;
173
+ }
174
+ if (c === quote) {
175
+ this.pos++;
176
+ return;
177
+ }
178
+ if (quote === '`' && c === '$' && src[this.pos + 1] === '{') {
179
+ this.pos += 2;
180
+ this.skipBalancedUntil('}');
181
+ continue;
182
+ }
183
+ this.pos++;
184
+ }
185
+ throw this.error('unterminated string');
186
+ }
187
+
188
+ /** After an opener was consumed, skip to its closer, tracking nesting. */
189
+ skipBalancedUntil(closer: string): void {
190
+ const openers: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
191
+ const depthFor: Record<string, number> = { '(': 0, '[': 0, '{': 0 };
192
+ const src = this.source;
193
+ while (this.pos < src.length) {
194
+ const c = src[this.pos];
195
+ if (c === '"' || c === "'" || c === '`') {
196
+ this.skipString();
197
+ continue;
198
+ }
199
+ if (
200
+ c === '/' &&
201
+ (src[this.pos + 1] === '/' || src[this.pos + 1] === '*')
202
+ ) {
203
+ this.skipTrivia();
204
+ continue;
205
+ }
206
+ if (c === '(' || c === '[' || c === '{') {
207
+ depthFor[c]++;
208
+ this.pos++;
209
+ continue;
210
+ }
211
+ if (c === ')' || c === ']' || c === '}') {
212
+ const open = openers[c];
213
+ if (depthFor[open] === 0) {
214
+ if (c === closer) {
215
+ this.pos++;
216
+ return;
217
+ }
218
+ throw this.error(`unbalanced '${c}'`);
219
+ }
220
+ depthFor[open]--;
221
+ this.pos++;
222
+ continue;
223
+ }
224
+ this.pos++;
225
+ }
226
+ throw this.error(`expected '${closer}'`);
227
+ }
228
+
229
+ /** Slurp a balanced `{ … }` block; returns the inner source. */
230
+ slurpBlock(): string {
231
+ this.skipTrivia();
232
+ this.expectPunct('{');
233
+ const start = this.pos;
234
+ this.skipBalancedUntil('}');
235
+ return this.source.slice(start, this.pos - 1);
236
+ }
237
+
238
+ /**
239
+ * Slurp a JS expression as raw text. Ends at `;` (consumed), before an
240
+ * unbalanced `}`/`]`, at a depth-0 `,` (when `stopComma`), or at a
241
+ * newline followed by what can only be the next member (QML's ASI).
242
+ */
243
+ slurpExpression({ stopComma = false } = {}): string {
244
+ const src = this.source;
245
+ this.skipTrivia();
246
+ const start = this.pos;
247
+ let depth = 0;
248
+ let lastSig = ''; // last significant char, for the continuation test
249
+ while (this.pos < src.length) {
250
+ const c = src[this.pos];
251
+ if (c === '"' || c === "'" || c === '`') {
252
+ this.skipString();
253
+ lastSig = '"';
254
+ continue;
255
+ }
256
+ if (
257
+ c === '/' &&
258
+ (src[this.pos + 1] === '/' || src[this.pos + 1] === '*')
259
+ ) {
260
+ const crossed = this.skipTrivia();
261
+ if (crossed && depth === 0 && this.expressionEndsHere(lastSig)) break;
262
+ continue;
263
+ }
264
+ if (c === '(' || c === '[' || c === '{') {
265
+ depth++;
266
+ lastSig = c;
267
+ this.pos++;
268
+ continue;
269
+ }
270
+ if (c === ')' || c === ']' || c === '}') {
271
+ if (depth === 0) break; // the parent context's closer
272
+ depth--;
273
+ lastSig = c;
274
+ this.pos++;
275
+ continue;
276
+ }
277
+ if (c === ';' && depth === 0) {
278
+ const end = this.pos;
279
+ this.pos++;
280
+ return src.slice(start, end);
281
+ }
282
+ if (c === ',' && depth === 0 && stopComma) break;
283
+ if (c === '\n') {
284
+ if (depth === 0 && this.expressionEndsHere(lastSig)) break;
285
+ this.pos++;
286
+ continue;
287
+ }
288
+ if (!' \t\r'.includes(c)) lastSig = c;
289
+ this.pos++;
290
+ }
291
+ const text = src.slice(start, this.pos).trim();
292
+ if (!text) throw this.error('expected expression', start);
293
+ return text;
294
+ }
295
+
296
+ /**
297
+ * At a depth-0 newline: does the expression end, or continue? It
298
+ * continues when the last char reads as an unfinished operator, or the
299
+ * next line starts with one; it ends when the next tokens look like a
300
+ * member start (`ident… :`, `Type {`, a keyword, or `}`).
301
+ */
302
+ expressionEndsHere(lastSig: string): boolean {
303
+ if ('+-*/%<>=&|^!?:.,([{'.includes(lastSig) || lastSig === '') return false;
304
+ const save = this.pos;
305
+ this.skipTrivia();
306
+ const c = this.source[this.pos];
307
+ if (c === undefined || c === '}' || c === ']') {
308
+ this.pos = save;
309
+ return true;
310
+ }
311
+ if ('+-*/%<>=&|^?:.'.includes(c) || c === ')' || c === ',') {
312
+ this.pos = save;
313
+ return false; // `.method()` chains, operators, ternary arms
314
+ }
315
+ const id = this.tryIdent();
316
+ if (id) {
317
+ if (KEYWORDS.has(id) && id !== 'on' && id !== 'as') {
318
+ this.pos = save;
319
+ return true;
320
+ }
321
+ // `Type {`, `Type on prop {`
322
+ if (isUpper(id[0])) {
323
+ const next = this.peekIdent();
324
+ if (this.peekChar() === '{' || next === 'on') {
325
+ this.pos = save;
326
+ return true;
327
+ }
328
+ }
329
+ // `name:` / `group.sub:` / `group {`
330
+ while (this.tryPunct('.')) {
331
+ if (!this.tryIdent()) break;
332
+ }
333
+ const after = this.peekChar();
334
+ this.pos = save;
335
+ return after === ':' || after === '{';
336
+ }
337
+ this.pos = save;
338
+ return false;
339
+ }
340
+ }
341
+
342
+ // --- grammar ---------------------------------------------------------------
343
+
344
+ export function parseQml(
345
+ source: string,
346
+ { fileName = '<qml>' }: { fileName?: string } = {},
347
+ ): QmlDocument {
348
+ const s = new Scanner(source);
349
+ const imports: QmlImport[] = [];
350
+ const pragmas: string[] = [];
351
+ for (;;) {
352
+ const id = s.peekIdent();
353
+ if (id === 'import') {
354
+ s.expectIdent();
355
+ imports.push(parseImport(s));
356
+ } else if (id === 'pragma') {
357
+ s.expectIdent();
358
+ pragmas.push(s.expectIdent('pragma name'));
359
+ } else {
360
+ break;
361
+ }
362
+ }
363
+ const root = parseObject(s);
364
+ s.skipTrivia();
365
+ if (!s.atEnd()) throw s.error('expected end of document after root object');
366
+ return { fileName, imports, pragmas, root };
367
+ }
368
+
369
+ function parseImport(s: Scanner): QmlImport {
370
+ s.skipTrivia();
371
+ const imp: QmlImport = {};
372
+ if (s.source[s.pos] === '"' || s.source[s.pos] === "'") {
373
+ const start = s.pos;
374
+ s.skipString();
375
+ imp.path = s.source.slice(start + 1, s.pos - 1);
376
+ } else {
377
+ imp.module = s.dottedName().join('.');
378
+ s.skipTrivia();
379
+ if (/[0-9]/.test(s.source[s.pos])) {
380
+ const m = /^[0-9]+(\.[0-9]+)?/.exec(s.source.slice(s.pos));
381
+ if (m) {
382
+ imp.version = m[0];
383
+ s.pos += m[0].length;
384
+ }
385
+ }
386
+ }
387
+ if (s.peekIdent() === 'as') {
388
+ s.expectIdent();
389
+ imp.alias = s.expectIdent('import alias');
390
+ }
391
+ s.tryPunct(';');
392
+ return imp;
393
+ }
394
+
395
+ function parseObject(s: Scanner): ObjectIR {
396
+ s.skipTrivia();
397
+ const loc = s.pos;
398
+ const type = s.dottedName().join('.');
399
+ if (!isUpper(type[0]))
400
+ throw s.error(`type name must start uppercase: '${type}'`, loc);
401
+ return parseObjectBody(s, type, loc);
402
+ }
403
+
404
+ function parseObjectBody(s: Scanner, type: string, loc: number): ObjectIR {
405
+ s.expectPunct('{');
406
+ const members: MemberIR[] = [];
407
+ const obj: ObjectIR = { type, id: null, members, loc };
408
+ for (;;) {
409
+ s.skipTrivia();
410
+ if (s.tryPunct('}')) return obj;
411
+ if (s.atEnd()) throw s.error('unterminated object body', loc);
412
+ members.push(parseMember(s, obj));
413
+ s.tryPunct(';');
414
+ }
415
+ }
416
+
417
+ // A declaration keyword used as a plain property name — Qt's own Binding
418
+ // element has `property: "width"` — is told apart by the colon right after.
419
+ function identIsBindingName(s: Scanner): boolean {
420
+ const save = s.pos;
421
+ s.skipTrivia();
422
+ s.tryIdent();
423
+ const colon = s.peekChar() === ':';
424
+ s.pos = save;
425
+ return colon;
426
+ }
427
+
428
+ function parseMember(s: Scanner, obj: ObjectIR): MemberIR {
429
+ const loc = s.pos;
430
+ const kw = s.peekIdent();
431
+ const asBinding = kw !== null && KEYWORDS.has(kw) && identIsBindingName(s);
432
+
433
+ if (
434
+ !asBinding &&
435
+ (kw === 'property' ||
436
+ kw === 'readonly' ||
437
+ kw === 'default' ||
438
+ kw === 'required')
439
+ ) {
440
+ return parsePropertyDecl(s, loc);
441
+ }
442
+ if (!asBinding && kw === 'signal') {
443
+ s.expectIdent();
444
+ const name = s.expectIdent('signal name');
445
+ const params: Array<{ type: string; name: string }> = [];
446
+ if (s.tryPunct('(')) {
447
+ while (!s.tryPunct(')')) {
448
+ const a = s.expectIdent('parameter type');
449
+ const b =
450
+ s.peekIdent() && s.peekChar() !== ',' && s.peekChar() !== ')'
451
+ ? s.expectIdent()
452
+ : null;
453
+ params.push(b ? { type: a, name: b } : { type: 'var', name: a });
454
+ if (s.tryPunct(':')) s.expectIdent('parameter type'); // `x: int` style
455
+ s.tryPunct(',');
456
+ }
457
+ }
458
+ return { kind: 'signal', name, params, loc };
459
+ }
460
+ if (!asBinding && kw === 'function') {
461
+ s.expectIdent();
462
+ const name = s.expectIdent('function name');
463
+ s.expectPunct('(');
464
+ const start = s.pos;
465
+ s.skipBalancedUntil(')');
466
+ const params = s.source
467
+ .slice(start, s.pos - 1)
468
+ .split(',')
469
+ .map((p) => p.trim())
470
+ .filter(Boolean)
471
+ .map((p) => p.split(':')[0].trim()); // tolerate type annotations
472
+ const body = s.slurpBlock();
473
+ return { kind: 'function', name, params, body, loc };
474
+ }
475
+ if (!asBinding && kw === 'component') {
476
+ // inline component: `component Foo: Item { … }`
477
+ s.expectIdent();
478
+ const name = s.expectIdent('component name');
479
+ s.expectPunct(':');
480
+ const object = parseObject(s);
481
+ return { kind: 'inline-component', name, object, loc };
482
+ }
483
+
484
+ // From here: `Type {`, `Type on prop {`, `name: value`, `group { … }`.
485
+ const path = s.dottedName();
486
+ const head = path[0];
487
+
488
+ if (isUpper(head[0]) && path.length === 1 && s.peekIdent() === 'on') {
489
+ s.expectIdent(); // 'on'
490
+ const target = s.dottedName();
491
+ const object = parseObjectBody(s, head, loc);
492
+ return { kind: 'value-source', object, on: target, loc };
493
+ }
494
+ if (isUpper(head[0]) && s.peekChar() === '{') {
495
+ const object = parseObjectBody(s, path.join('.'), loc);
496
+ return { kind: 'object', object, loc };
497
+ }
498
+ if (s.tryPunct(':')) {
499
+ if (path.length === 1 && head === 'id') {
500
+ const id = s.expectIdent('id');
501
+ obj.id = id;
502
+ return { kind: 'id', id, loc };
503
+ }
504
+ const value = parseBindingValue(s);
505
+ return { kind: 'binding', path, value, loc };
506
+ }
507
+ if (s.peekChar() === '{' && !isUpper(head[0])) {
508
+ // grouped-property block: `anchors { fill: parent; margins: 4 }`
509
+ s.expectPunct('{');
510
+ const group: BindingMemberIR[] = [];
511
+ for (;;) {
512
+ s.skipTrivia();
513
+ if (s.tryPunct('}')) break;
514
+ const sub = s.dottedName();
515
+ s.expectPunct(':');
516
+ group.push({
517
+ kind: 'binding',
518
+ path: [...path, ...sub],
519
+ value: parseBindingValue(s),
520
+ loc,
521
+ });
522
+ s.tryPunct(';');
523
+ }
524
+ return { kind: 'group', bindings: group, loc };
525
+ }
526
+ throw s.error(`unexpected member '${path.join('.')}'`, loc);
527
+ }
528
+
529
+ function parsePropertyDecl(s: Scanner, loc: number): MemberIR {
530
+ let isReadonly = false;
531
+ let isDefault = false;
532
+ let isRequired = false;
533
+ for (;;) {
534
+ const kw = s.peekIdent();
535
+ if (kw === 'readonly') {
536
+ s.expectIdent();
537
+ isReadonly = true;
538
+ } else if (kw === 'default') {
539
+ s.expectIdent();
540
+ isDefault = true;
541
+ } else if (kw === 'required') {
542
+ s.expectIdent();
543
+ isRequired = true;
544
+ } else break;
545
+ }
546
+ const kw = s.expectIdent();
547
+ if (kw !== 'property') throw s.error(`expected 'property', got '${kw}'`, loc);
548
+ let propType = s.expectIdent('property type');
549
+ if (propType === 'list') {
550
+ s.expectPunct('<');
551
+ s.dottedName();
552
+ s.expectPunct('>');
553
+ propType = 'list';
554
+ }
555
+ const name = s.expectIdent('property name');
556
+ let value: ValueIR | AliasTargetIR | null = null;
557
+ if (s.tryPunct(':')) {
558
+ value =
559
+ propType === 'alias'
560
+ ? { kind: 'alias-target', path: s.dottedName() }
561
+ : parseBindingValue(s);
562
+ }
563
+ return {
564
+ kind: 'property',
565
+ name,
566
+ propType,
567
+ readonly: isReadonly,
568
+ default: isDefault,
569
+ required: isRequired,
570
+ value,
571
+ loc,
572
+ };
573
+ }
574
+
575
+ function parseBindingValue(s: Scanner, { stopComma = false } = {}): ValueIR {
576
+ s.skipTrivia();
577
+ const c = s.source[s.pos];
578
+ if (c === '[') {
579
+ // Array of objects (`states: [State { … }, …]`) or a JS array
580
+ // expression; objects win when the first element looks like `Type {`.
581
+ const save = s.pos;
582
+ s.expectPunct('[');
583
+ const first = s.peekIdent();
584
+ if (first && isUpper(first[0])) {
585
+ const inner = s.pos;
586
+ s.skipTrivia();
587
+ s.tryIdent();
588
+ while (s.tryPunct('.')) s.tryIdent();
589
+ const isObj = s.peekChar() === '{';
590
+ s.pos = inner;
591
+ if (isObj) {
592
+ const items: ValueIR[] = [];
593
+ for (;;) {
594
+ s.skipTrivia();
595
+ if (s.tryPunct(']')) break;
596
+ items.push({ kind: 'object', object: parseObject(s) });
597
+ s.tryPunct(',');
598
+ }
599
+ return { kind: 'array', items };
600
+ }
601
+ }
602
+ s.pos = save;
603
+ const loc = s.pos;
604
+ return { kind: 'expr', src: s.slurpExpression({ stopComma }), loc };
605
+ }
606
+ if (c === '{') {
607
+ // QML rule: a brace after ':' is a statement block, not an object
608
+ // literal (and the block is still a reactive binding).
609
+ const loc = s.pos;
610
+ return { kind: 'block', src: s.slurpBlock(), loc };
611
+ }
612
+ const id = s.peekIdent();
613
+ if (id && isUpper(id[0])) {
614
+ const save = s.pos;
615
+ s.skipTrivia();
616
+ s.tryIdent();
617
+ while (s.tryPunct('.')) s.tryIdent();
618
+ const isObj = s.peekChar() === '{';
619
+ s.pos = save;
620
+ if (isObj) return { kind: 'object', object: parseObject(s) };
621
+ }
622
+ const loc = s.pos;
623
+ return { kind: 'expr', src: s.slurpExpression({ stopComma }), loc };
624
+ }