@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.
- package/README.md +131 -4
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/qml/containers.d.ts +3 -0
- package/dist/qml/containers.d.ts.map +1 -0
- package/dist/qml/containers.js +331 -0
- package/dist/qml/containers.js.map +1 -0
- package/dist/qml/controls.d.ts +12 -0
- package/dist/qml/controls.d.ts.map +1 -0
- package/dist/qml/controls.js +21 -0
- package/dist/qml/controls.js.map +1 -0
- package/dist/qml/globals.d.ts +6 -0
- package/dist/qml/globals.d.ts.map +1 -0
- package/dist/qml/globals.js +25 -0
- package/dist/qml/globals.js.map +1 -0
- package/dist/qml/index.d.ts +15 -0
- package/dist/qml/index.d.ts.map +1 -0
- package/dist/qml/index.js +25 -0
- package/dist/qml/index.js.map +1 -0
- package/dist/qml/interaction.d.ts +12 -0
- package/dist/qml/interaction.d.ts.map +1 -0
- package/dist/qml/interaction.js +246 -0
- package/dist/qml/interaction.js.map +1 -0
- package/dist/qml/ir.d.ts +106 -0
- package/dist/qml/ir.d.ts.map +1 -0
- package/dist/qml/ir.js +7 -0
- package/dist/qml/ir.js.map +1 -0
- package/dist/qml/layouts.d.ts +3 -0
- package/dist/qml/layouts.d.ts.map +1 -0
- package/dist/qml/layouts.js +122 -0
- package/dist/qml/layouts.js.map +1 -0
- package/dist/qml/models.d.ts +14 -0
- package/dist/qml/models.d.ts.map +1 -0
- package/dist/qml/models.js +80 -0
- package/dist/qml/models.js.map +1 -0
- package/dist/qml/objects.d.ts +228 -0
- package/dist/qml/objects.d.ts.map +1 -0
- package/dist/qml/objects.js +1103 -0
- package/dist/qml/objects.js.map +1 -0
- package/dist/qml/parse.d.ts +10 -0
- package/dist/qml/parse.d.ts.map +1 -0
- package/dist/qml/parse.js +607 -0
- package/dist/qml/parse.js.map +1 -0
- package/dist/qml/qtquick.d.ts +7 -0
- package/dist/qml/qtquick.d.ts.map +1 -0
- package/dist/qml/qtquick.js +347 -0
- package/dist/qml/qtquick.js.map +1 -0
- package/dist/qml/react.d.ts +83 -0
- package/dist/qml/react.d.ts.map +1 -0
- package/dist/qml/react.js +323 -0
- package/dist/qml/react.js.map +1 -0
- package/dist/qml/resolver.d.ts +38 -0
- package/dist/qml/resolver.d.ts.map +1 -0
- package/dist/qml/resolver.js +64 -0
- package/dist/qml/resolver.js.map +1 -0
- package/dist/qml/slots.d.ts +65 -0
- package/dist/qml/slots.d.ts.map +1 -0
- package/dist/qml/slots.js +230 -0
- package/dist/qml/slots.js.map +1 -0
- package/dist/qml/states.d.ts +5 -0
- package/dist/qml/states.d.ts.map +1 -0
- package/dist/qml/states.js +276 -0
- package/dist/qml/states.js.map +1 -0
- package/dist/qml/view-utils.d.ts +42 -0
- package/dist/qml/view-utils.d.ts.map +1 -0
- package/dist/qml/view-utils.js +121 -0
- package/dist/qml/view-utils.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/hx.d.ts +18 -0
- package/dist/tabs/hx.d.ts.map +1 -0
- package/dist/tabs/hx.js +10 -0
- package/dist/tabs/hx.js.map +1 -0
- package/dist/tabs/index.d.ts +160 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tabs/index.js +1265 -0
- package/dist/tabs/index.js.map +1 -0
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/index.d.ts +1 -1
- package/dist/terminal/vt/index.d.ts.map +1 -1
- package/dist/terminal/vt/index.js +3 -3
- package/dist/terminal/vt/index.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +16 -0
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +186 -0
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +23 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +142 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/qml/containers.tsx +380 -0
- package/src/qml/controls.tsx +47 -0
- package/src/qml/globals.ts +33 -0
- package/src/qml/index.ts +67 -0
- package/src/qml/interaction.tsx +317 -0
- package/src/qml/ir.ts +80 -0
- package/src/qml/layouts.tsx +141 -0
- package/src/qml/models.ts +101 -0
- package/src/qml/objects.ts +1431 -0
- package/src/qml/parse.ts +624 -0
- package/src/qml/qtquick.tsx +413 -0
- package/src/qml/react.tsx +454 -0
- package/src/qml/resolver.ts +99 -0
- package/src/qml/slots.ts +249 -0
- package/src/qml/states.ts +318 -0
- package/src/qml/view-utils.ts +140 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/hx.ts +31 -0
- package/src/tabs/index.ts +1686 -0
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/index.ts +4 -2
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/pty.ts +243 -0
- package/src/terminal/vt/renderer.ts +13 -3
package/src/qml/parse.ts
ADDED
|
@@ -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
|
+
}
|