@glint/ember-tsc 1.0.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/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/ember-tsc.js +4 -0
- package/bin/glint-language-server.js +2 -0
- package/lib/cli/run-volar-tsc.d.ts +2 -0
- package/lib/cli/run-volar-tsc.d.ts.map +1 -0
- package/lib/cli/run-volar-tsc.js +30 -0
- package/lib/cli/run-volar-tsc.js.map +1 -0
- package/lib/config/config.d.ts +15 -0
- package/lib/config/config.d.ts.map +1 -0
- package/lib/config/config.js +21 -0
- package/lib/config/config.js.map +1 -0
- package/lib/config/environment.d.ts +26 -0
- package/lib/config/environment.d.ts.map +1 -0
- package/lib/config/environment.js +96 -0
- package/lib/config/environment.js.map +1 -0
- package/lib/config/index.d.ts +17 -0
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/index.js +26 -0
- package/lib/config/index.js.map +1 -0
- package/lib/config/loader.d.ts +25 -0
- package/lib/config/loader.d.ts.map +1 -0
- package/lib/config/loader.js +110 -0
- package/lib/config/loader.js.map +1 -0
- package/lib/config/types.cjs +3 -0
- package/lib/config/types.cjs.map +1 -0
- package/lib/config/types.d.cts +60 -0
- package/lib/config/types.d.cts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/common.d.ts +13 -0
- package/lib/environment-ember-template-imports/-private/environment/common.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/common.js +2 -0
- package/lib/environment-ember-template-imports/-private/environment/common.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/index.d.ts +3 -0
- package/lib/environment-ember-template-imports/-private/environment/index.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/index.js +76 -0
- package/lib/environment-ember-template-imports/-private/environment/index.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts +4 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js +73 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.d.ts +4 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.js +134 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -0
- package/lib/plugins/g-compiler-errors.d.ts +12 -0
- package/lib/plugins/g-compiler-errors.d.ts.map +1 -0
- package/lib/plugins/g-compiler-errors.js +58 -0
- package/lib/plugins/g-compiler-errors.js.map +1 -0
- package/lib/plugins/g-template-tag-symbols.d.ts +11 -0
- package/lib/plugins/g-template-tag-symbols.d.ts.map +1 -0
- package/lib/plugins/g-template-tag-symbols.js +48 -0
- package/lib/plugins/g-template-tag-symbols.js.map +1 -0
- package/lib/plugins/utils.d.ts +25 -0
- package/lib/plugins/utils.d.ts.map +1 -0
- package/lib/plugins/utils.js +63 -0
- package/lib/plugins/utils.js.map +1 -0
- package/lib/transform/diagnostics/augmentation.d.ts +4 -0
- package/lib/transform/diagnostics/augmentation.d.ts.map +1 -0
- package/lib/transform/diagnostics/augmentation.js +223 -0
- package/lib/transform/diagnostics/augmentation.js.map +1 -0
- package/lib/transform/diagnostics/index.d.ts +5 -0
- package/lib/transform/diagnostics/index.d.ts.map +1 -0
- package/lib/transform/diagnostics/index.js +2 -0
- package/lib/transform/diagnostics/index.js.map +1 -0
- package/lib/transform/index.d.ts +4 -0
- package/lib/transform/index.d.ts.map +1 -0
- package/lib/transform/index.js +2 -0
- package/lib/transform/index.js.map +1 -0
- package/lib/transform/template/code-features.d.ts +30 -0
- package/lib/transform/template/code-features.d.ts.map +1 -0
- package/lib/transform/template/code-features.js +26 -0
- package/lib/transform/template/code-features.js.map +1 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts +80 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts.map +1 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.js +132 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.js.map +1 -0
- package/lib/transform/template/inlining/index.d.ts +16 -0
- package/lib/transform/template/inlining/index.d.ts.map +1 -0
- package/lib/transform/template/inlining/index.js +21 -0
- package/lib/transform/template/inlining/index.js.map +1 -0
- package/lib/transform/template/inlining/tagged-strings.d.ts +8 -0
- package/lib/transform/template/inlining/tagged-strings.d.ts.map +1 -0
- package/lib/transform/template/inlining/tagged-strings.js +140 -0
- package/lib/transform/template/inlining/tagged-strings.js.map +1 -0
- package/lib/transform/template/map-template-contents.d.ts +121 -0
- package/lib/transform/template/map-template-contents.d.ts.map +1 -0
- package/lib/transform/template/map-template-contents.js +287 -0
- package/lib/transform/template/map-template-contents.js.map +1 -0
- package/lib/transform/template/rewrite-module.d.ts +22 -0
- package/lib/transform/template/rewrite-module.d.ts.map +1 -0
- package/lib/transform/template/rewrite-module.js +265 -0
- package/lib/transform/template/rewrite-module.js.map +1 -0
- package/lib/transform/template/scope-stack.d.ts +13 -0
- package/lib/transform/template/scope-stack.d.ts.map +1 -0
- package/lib/transform/template/scope-stack.js +28 -0
- package/lib/transform/template/scope-stack.js.map +1 -0
- package/lib/transform/template/template-to-typescript.d.ts +19 -0
- package/lib/transform/template/template-to-typescript.d.ts.map +1 -0
- package/lib/transform/template/template-to-typescript.js +1095 -0
- package/lib/transform/template/template-to-typescript.js.map +1 -0
- package/lib/transform/template/transformed-module.d.ts +111 -0
- package/lib/transform/template/transformed-module.d.ts.map +1 -0
- package/lib/transform/template/transformed-module.js +287 -0
- package/lib/transform/template/transformed-module.js.map +1 -0
- package/lib/transform/util.d.ts +7 -0
- package/lib/transform/util.d.ts.map +1 -0
- package/lib/transform/util.js +15 -0
- package/lib/transform/util.js.map +1 -0
- package/lib/volar/ember-language-plugin.d.ts +14 -0
- package/lib/volar/ember-language-plugin.d.ts.map +1 -0
- package/lib/volar/ember-language-plugin.js +91 -0
- package/lib/volar/ember-language-plugin.js.map +1 -0
- package/lib/volar/gts-virtual-code.d.ts +83 -0
- package/lib/volar/gts-virtual-code.d.ts.map +1 -0
- package/lib/volar/gts-virtual-code.js +210 -0
- package/lib/volar/gts-virtual-code.js.map +1 -0
- package/lib/volar/language-server.d.ts +2 -0
- package/lib/volar/language-server.d.ts.map +1 -0
- package/lib/volar/language-server.js +214 -0
- package/lib/volar/language-server.js.map +1 -0
- package/lib/volar/script-snapshot.d.ts +17 -0
- package/lib/volar/script-snapshot.d.ts.map +1 -0
- package/lib/volar/script-snapshot.js +24 -0
- package/lib/volar/script-snapshot.js.map +1 -0
- package/package.json +104 -0
- package/src/cli/run-volar-tsc.ts +36 -0
- package/src/config/config.ts +33 -0
- package/src/config/environment.ts +128 -0
- package/src/config/index.ts +30 -0
- package/src/config/loader.ts +143 -0
- package/src/config/types.cts +85 -0
- package/src/environment-ember-template-imports/-private/environment/common.ts +14 -0
- package/src/environment-ember-template-imports/-private/environment/index.ts +83 -0
- package/src/environment-ember-template-imports/-private/environment/preprocess.ts +90 -0
- package/src/environment-ember-template-imports/-private/environment/transform.ts +202 -0
- package/src/index.ts +9 -0
- package/src/plugins/g-compiler-errors.ts +67 -0
- package/src/plugins/g-template-tag-symbols.ts +54 -0
- package/src/plugins/utils.ts +86 -0
- package/src/transform/diagnostics/augmentation.ts +333 -0
- package/src/transform/diagnostics/index.ts +5 -0
- package/src/transform/index.ts +4 -0
- package/src/transform/template/code-features.ts +30 -0
- package/src/transform/template/glimmer-ast-mapping-tree.ts +173 -0
- package/src/transform/template/inlining/index.ts +33 -0
- package/src/transform/template/inlining/tagged-strings.ts +187 -0
- package/src/transform/template/map-template-contents.ts +501 -0
- package/src/transform/template/rewrite-module.ts +372 -0
- package/src/transform/template/scope-stack.ts +34 -0
- package/src/transform/template/template-to-typescript.ts +1476 -0
- package/src/transform/template/transformed-module.ts +431 -0
- package/src/transform/util.ts +24 -0
- package/src/volar/ember-language-plugin.ts +108 -0
- package/src/volar/gts-virtual-code.ts +249 -0
- package/src/volar/language-server.ts +250 -0
- package/src/volar/script-snapshot.ts +27 -0
- package/types/-private/dsl/globals.d.ts +204 -0
- package/types/-private/dsl/index.d.ts +50 -0
- package/types/-private/dsl/integration-declarations.d.ts +143 -0
- package/types/-private/intrinsics/action.d.ts +45 -0
- package/types/-private/intrinsics/concat.d.ts +6 -0
- package/types/-private/intrinsics/each-in.d.ts +24 -0
- package/types/-private/intrinsics/each.d.ts +17 -0
- package/types/-private/intrinsics/fn.d.ts +44 -0
- package/types/-private/intrinsics/get.d.ts +31 -0
- package/types/-private/intrinsics/input.d.ts +24 -0
- package/types/-private/intrinsics/link-to.d.ts +31 -0
- package/types/-private/intrinsics/log.d.ts +6 -0
- package/types/-private/intrinsics/mount.d.ts +9 -0
- package/types/-private/intrinsics/mut.d.ts +14 -0
- package/types/-private/intrinsics/on.d.ts +21 -0
- package/types/-private/intrinsics/outlet.d.ts +8 -0
- package/types/-private/intrinsics/textarea.d.ts +16 -0
- package/types/-private/intrinsics/unbound.d.ts +10 -0
- package/types/-private/intrinsics/unique-id.d.ts +5 -0
- package/types/globals/index.d.ts +3 -0
- package/types/silent-error.d.ts +4 -0
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
import { AST } from '@glimmer/syntax';
|
|
2
|
+
import { GlintEmitMetadata, GlintSpecialForm } from '@glint/ember-tsc/config-types';
|
|
3
|
+
import { assert, unreachable } from '../util.js';
|
|
4
|
+
import { TextContent } from './glimmer-ast-mapping-tree.js';
|
|
5
|
+
import { EmbeddingSyntax, mapTemplateContents, RewriteResult } from './map-template-contents.js';
|
|
6
|
+
import ScopeStack from './scope-stack.js';
|
|
7
|
+
|
|
8
|
+
const SPLATTRIBUTES = '...attributes';
|
|
9
|
+
|
|
10
|
+
export type TemplateToTypescriptOptions = {
|
|
11
|
+
typesModule: string;
|
|
12
|
+
meta?: GlintEmitMetadata | undefined;
|
|
13
|
+
globals?: Array<string> | undefined;
|
|
14
|
+
backingValue?: string;
|
|
15
|
+
preamble?: Array<string>;
|
|
16
|
+
embeddingSyntax?: EmbeddingSyntax;
|
|
17
|
+
useJsDoc?: boolean;
|
|
18
|
+
specialForms?: Record<string, GlintSpecialForm>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* NOTE: this is tech debt. Tho solving it will require more work than polyfilling old behavior of path.parts
|
|
23
|
+
* @param node
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
function getPathParts(node: AST.PathExpression): string[] {
|
|
27
|
+
// The original code which used old @glimmer/syntax used node.parts,
|
|
28
|
+
// which never included the @ of the path.
|
|
29
|
+
let atLess = node.head.original.replace(/^@/, '');
|
|
30
|
+
|
|
31
|
+
// The original path.parts array did not include "this" in the parts.
|
|
32
|
+
if (atLess === 'this') return node.tail;
|
|
33
|
+
|
|
34
|
+
return [atLess, ...node.tail];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Given the text contents of a template, returns a TypeScript representation
|
|
39
|
+
* of that template's contents, as well as a mapping of offsets and ranges between
|
|
40
|
+
* the original and transformed contents.
|
|
41
|
+
*/
|
|
42
|
+
export function templateToTypescript(
|
|
43
|
+
originalTemplate: string,
|
|
44
|
+
{
|
|
45
|
+
typesModule,
|
|
46
|
+
globals,
|
|
47
|
+
meta,
|
|
48
|
+
backingValue,
|
|
49
|
+
preamble = [],
|
|
50
|
+
embeddingSyntax = { prefix: '', suffix: '' },
|
|
51
|
+
specialForms = {},
|
|
52
|
+
useJsDoc = false,
|
|
53
|
+
}: TemplateToTypescriptOptions,
|
|
54
|
+
): RewriteResult {
|
|
55
|
+
let { prefix, suffix } = embeddingSyntax;
|
|
56
|
+
let template = `${''.padEnd(prefix.length)}${originalTemplate}${''.padEnd(suffix.length)}`;
|
|
57
|
+
|
|
58
|
+
return mapTemplateContents(originalTemplate, { embeddingSyntax }, (ast, mapper) => {
|
|
59
|
+
let { rangeForNode } = mapper;
|
|
60
|
+
let scope = new ScopeStack([]);
|
|
61
|
+
let inHtmlContext: 'svg' | 'math' | 'default' = 'default';
|
|
62
|
+
|
|
63
|
+
emitTemplateBoilerplate(() => {
|
|
64
|
+
for (let statement of ast?.body ?? []) {
|
|
65
|
+
emitTopLevelStatement(statement);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
|
|
71
|
+
function emitTopLevelStatement(node: AST.TopLevelStatement): void {
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case 'Block':
|
|
74
|
+
throw new Error(`Internal error: unexpected top-level ${node.type}`);
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
76
|
+
// @ts-expect-error
|
|
77
|
+
case 'PartialStatement':
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
79
|
+
// @ts-expect-error
|
|
80
|
+
throw new Error(`Internal error: unexpected top-level ${node.type}`);
|
|
81
|
+
|
|
82
|
+
case 'TextNode':
|
|
83
|
+
return emitTopLevelTextNode(node);
|
|
84
|
+
|
|
85
|
+
case 'CommentStatement':
|
|
86
|
+
case 'MustacheCommentStatement':
|
|
87
|
+
return emitComment(node);
|
|
88
|
+
|
|
89
|
+
case 'MustacheStatement':
|
|
90
|
+
return emitTopLevelMustacheStatement(node);
|
|
91
|
+
|
|
92
|
+
case 'BlockStatement':
|
|
93
|
+
return emitBlockStatement(node);
|
|
94
|
+
|
|
95
|
+
case 'ElementNode':
|
|
96
|
+
return emitElementNode(node);
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
unreachable(node);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function emitTemplateBoilerplate(emitBody: () => void): void {
|
|
104
|
+
if (meta?.prepend) {
|
|
105
|
+
mapper.text(meta.prepend);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (useJsDoc) {
|
|
109
|
+
mapper.text(`(/** @type {typeof import(`);
|
|
110
|
+
if (ast) {
|
|
111
|
+
mapper.forNode(ast, () => {
|
|
112
|
+
mapper.text(`"${typesModule}"`);
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
mapper.text(`"${typesModule}"`);
|
|
116
|
+
}
|
|
117
|
+
mapper.text(`)} */ ({}))`);
|
|
118
|
+
} else {
|
|
119
|
+
mapper.text(`({} as typeof import(`);
|
|
120
|
+
if (ast) {
|
|
121
|
+
mapper.forNode(ast, () => {
|
|
122
|
+
mapper.text(`"${typesModule}"`);
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
mapper.text(`"${typesModule}"`);
|
|
126
|
+
}
|
|
127
|
+
mapper.text(`))`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (backingValue) {
|
|
131
|
+
mapper.text(`.templateForBackingValue(${backingValue}, function(__glintRef__`);
|
|
132
|
+
} else {
|
|
133
|
+
mapper.text(`.templateExpression(function(__glintRef__`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (useJsDoc) {
|
|
137
|
+
mapper.text(`, /** @type {typeof import("${typesModule}")} */ __glintDSL__) {`);
|
|
138
|
+
} else {
|
|
139
|
+
mapper.text(`, __glintDSL__: typeof import("${typesModule}")) {`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mapper.newline();
|
|
143
|
+
mapper.indent();
|
|
144
|
+
|
|
145
|
+
for (let line of preamble) {
|
|
146
|
+
mapper.text(line);
|
|
147
|
+
mapper.newline();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (ast) {
|
|
151
|
+
mapper.forNode(ast, emitBody);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ensure the context and lib variables are always consumed to prevent
|
|
155
|
+
// an unused variable warning
|
|
156
|
+
mapper.text('__glintRef__; __glintDSL__;');
|
|
157
|
+
mapper.newline();
|
|
158
|
+
|
|
159
|
+
mapper.emitDirectivePlaceholders();
|
|
160
|
+
|
|
161
|
+
mapper.dedent();
|
|
162
|
+
mapper.text('})');
|
|
163
|
+
|
|
164
|
+
if (meta?.append) {
|
|
165
|
+
mapper.text(meta.append);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function emitTopLevelTextNode(node: AST.TextNode): void {
|
|
170
|
+
// We don't need to emit any code for text nodes, but we want to track
|
|
171
|
+
// where they are so we know NOT to try and suggest global completions
|
|
172
|
+
// in "text space" where it wouldn't make sense.
|
|
173
|
+
mapper.nothing(node, new TextContent());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitComment(node: AST.MustacheCommentStatement | AST.CommentStatement): void {
|
|
177
|
+
let text = node.value.trim();
|
|
178
|
+
const directiveRegex = /^@glint-([a-z-]+)/i;
|
|
179
|
+
let match = directiveRegex.exec(text);
|
|
180
|
+
if (!match) {
|
|
181
|
+
return mapper.nothing(node);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
emitDirective(match, node);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitDirective(
|
|
188
|
+
match: RegExpExecArray,
|
|
189
|
+
node: AST.CommentStatement | AST.MustacheCommentStatement,
|
|
190
|
+
): void {
|
|
191
|
+
let kind = match[1];
|
|
192
|
+
let location = rangeForNode(node);
|
|
193
|
+
if (kind === 'ignore' || kind === 'expect-error') {
|
|
194
|
+
mapper.directive(kind, node, location, mapper.rangeForLine(node.loc.end.line + 1));
|
|
195
|
+
} else if (kind === 'nocheck') {
|
|
196
|
+
mapper.directive('ignore', node, location, { start: 0, end: template.length - 1 });
|
|
197
|
+
} else if (kind === 'in-svg') {
|
|
198
|
+
inHtmlContext = 'svg';
|
|
199
|
+
} else if (kind === 'in-mathml') {
|
|
200
|
+
inHtmlContext = 'math';
|
|
201
|
+
} else if (kind === 'out-svg' || kind === 'out-mathml') {
|
|
202
|
+
inHtmlContext = 'default';
|
|
203
|
+
} else {
|
|
204
|
+
// Push an error on the record
|
|
205
|
+
mapper.error(`Unknown directive @glint-${kind}`, location);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function emitTopLevelMustacheStatement(node: AST.MustacheStatement): void {
|
|
210
|
+
emitMustacheStatement(node, 'top-level');
|
|
211
|
+
mapper.text(';');
|
|
212
|
+
mapper.newline();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Captures the context in which a given invocation (i.e. a mustache or
|
|
216
|
+
// sexpr) is being performed. Certain keywords like `yield` are only
|
|
217
|
+
// valid in certain positions, and whether a param-less mustache implicitly
|
|
218
|
+
// evaluates a helper or returns it also depends on the location it's in.
|
|
219
|
+
type InvokePosition = 'top-level' | 'attr' | 'arg' | 'concat' | 'sexpr';
|
|
220
|
+
|
|
221
|
+
function emitSpecialFormExpression(
|
|
222
|
+
formInfo: SpecialFormInfo,
|
|
223
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
224
|
+
position: InvokePosition,
|
|
225
|
+
): void {
|
|
226
|
+
if (formInfo.requiresConsumption) {
|
|
227
|
+
mapper.text('(__glintDSL__.noop(');
|
|
228
|
+
emitExpression(node.path);
|
|
229
|
+
mapper.text('), ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
switch (formInfo.form) {
|
|
233
|
+
case 'yield':
|
|
234
|
+
emitYieldExpression(formInfo, node, position);
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case 'if':
|
|
238
|
+
emitIfExpression(formInfo, node);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case 'if-not':
|
|
242
|
+
emitIfNotExpression(formInfo, node);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'object-literal':
|
|
246
|
+
emitObjectExpression(formInfo, node);
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case 'array-literal':
|
|
250
|
+
emitArrayExpression(formInfo, node);
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'bind-invokable':
|
|
254
|
+
emitBindInvokableExpression(formInfo, node, position);
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case '===':
|
|
258
|
+
case '!==':
|
|
259
|
+
emitBinaryOperatorExpression(formInfo, node);
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case '&&':
|
|
263
|
+
case '||':
|
|
264
|
+
emitLogicalExpression(formInfo, node);
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case '!':
|
|
268
|
+
emitUnaryOperatorExpression(formInfo, node);
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
default:
|
|
272
|
+
mapper.error(`${formInfo.name} is not valid in inline form`, rangeForNode(node));
|
|
273
|
+
mapper.text('undefined');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (formInfo.requiresConsumption) {
|
|
277
|
+
mapper.text(')');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function emitBindInvokableExpression(
|
|
282
|
+
formInfo: SpecialFormInfo,
|
|
283
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
284
|
+
position: InvokePosition,
|
|
285
|
+
): void {
|
|
286
|
+
mapper.forNode(node, () => {
|
|
287
|
+
assert(
|
|
288
|
+
node.params.length >= 1,
|
|
289
|
+
() => `{{${formInfo.name}}} requires at least one positional argument`,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
assert(
|
|
293
|
+
node.params.length === 1 || node.hash.pairs.length === 0,
|
|
294
|
+
() =>
|
|
295
|
+
`Due to TypeScript inference limitations, {{${formInfo.name}}} can only pre-bind ` +
|
|
296
|
+
`either named or positional arguments in a single pass. You can instead break the ` +
|
|
297
|
+
`binding into two parts, e.g. ` +
|
|
298
|
+
`{{${formInfo.name} (${formInfo.name} ... posA posB) namedA=true namedB=true}}`,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (position === 'top-level') {
|
|
302
|
+
mapper.text('__glintDSL__.emitContent(');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Treat the first argument to a bind-invokable expression (`{{component}}`,
|
|
306
|
+
// `{{helper}}`, etc) as special: we wrap it in a `resolve` call so that the
|
|
307
|
+
// type machinery for those helpers can always operate against the resolved value.
|
|
308
|
+
// We wrap the `resolveForBind` call in an IIFE to prevent "backpressure" in
|
|
309
|
+
// type inference from the subsequent arguments that are being passed: the bound
|
|
310
|
+
// invokable is the source of record for its own type and we don't want inference
|
|
311
|
+
// from the `resolveForBind` call to be affected by other (potentially incorrect)
|
|
312
|
+
// parameter types.
|
|
313
|
+
mapper.text('__glintDSL__.resolve(');
|
|
314
|
+
emitExpression(node.path);
|
|
315
|
+
mapper.text(')((() => __glintDSL__.resolveForBind(');
|
|
316
|
+
emitExpression(node.params[0]);
|
|
317
|
+
mapper.text('))(), ');
|
|
318
|
+
emitArgs(node.params.slice(1), node.hash);
|
|
319
|
+
mapper.text(')');
|
|
320
|
+
|
|
321
|
+
if (position === 'top-level') {
|
|
322
|
+
mapper.text(')');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function emitObjectExpression(
|
|
328
|
+
formInfo: SpecialFormInfo,
|
|
329
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
330
|
+
): void {
|
|
331
|
+
mapper.forNode(node, () => {
|
|
332
|
+
assert(
|
|
333
|
+
node.params.length === 0,
|
|
334
|
+
() => `{{${formInfo.name}}} only accepts named parameters`,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (!node.hash.pairs.length) {
|
|
338
|
+
mapper.text('{}');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
mapper.text('({');
|
|
343
|
+
mapper.indent();
|
|
344
|
+
mapper.newline();
|
|
345
|
+
|
|
346
|
+
let start = template.indexOf('hash', rangeForNode(node).start) + 4;
|
|
347
|
+
for (let pair of node.hash.pairs) {
|
|
348
|
+
start = template.indexOf(pair.key, start);
|
|
349
|
+
emitHashKey(pair.key, start);
|
|
350
|
+
mapper.text(': ');
|
|
351
|
+
emitExpression(pair.value);
|
|
352
|
+
mapper.text(',');
|
|
353
|
+
mapper.newline();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
mapper.dedent();
|
|
357
|
+
mapper.text('})');
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function emitArrayExpression(
|
|
362
|
+
formInfo: SpecialFormInfo,
|
|
363
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
364
|
+
): void {
|
|
365
|
+
mapper.forNode(node, () => {
|
|
366
|
+
assert(
|
|
367
|
+
node.hash.pairs.length === 0,
|
|
368
|
+
() => `{{${formInfo.name}}} only accepts positional parameters`,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
mapper.text('[');
|
|
372
|
+
|
|
373
|
+
for (let [index, param] of node.params.entries()) {
|
|
374
|
+
emitExpression(param);
|
|
375
|
+
|
|
376
|
+
if (index < node.params.length - 1) {
|
|
377
|
+
mapper.text(', ');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
mapper.text(']');
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function emitIfExpression(
|
|
386
|
+
formInfo: SpecialFormInfo,
|
|
387
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
388
|
+
): void {
|
|
389
|
+
mapper.forNode(node, () => {
|
|
390
|
+
assert(
|
|
391
|
+
node.params.length >= 2,
|
|
392
|
+
() => `{{${formInfo.name}}} requires at least two parameters`,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
mapper.text('(');
|
|
396
|
+
emitExpression(node.params[0]);
|
|
397
|
+
mapper.text(') ? (');
|
|
398
|
+
emitExpression(node.params[1]);
|
|
399
|
+
mapper.text(') : (');
|
|
400
|
+
|
|
401
|
+
if (node.params[2]) {
|
|
402
|
+
emitExpression(node.params[2]);
|
|
403
|
+
} else {
|
|
404
|
+
mapper.text('undefined');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
mapper.text(')');
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function emitIfNotExpression(
|
|
412
|
+
formInfo: SpecialFormInfo,
|
|
413
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
414
|
+
): void {
|
|
415
|
+
mapper.forNode(node, () => {
|
|
416
|
+
assert(
|
|
417
|
+
node.params.length >= 2,
|
|
418
|
+
() => `{{${formInfo.name}}} requires at least two parameters`,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
mapper.text('!(');
|
|
422
|
+
emitExpression(node.params[0]);
|
|
423
|
+
mapper.text(') ? (');
|
|
424
|
+
emitExpression(node.params[1]);
|
|
425
|
+
mapper.text(') : (');
|
|
426
|
+
|
|
427
|
+
if (node.params[2]) {
|
|
428
|
+
emitExpression(node.params[2]);
|
|
429
|
+
} else {
|
|
430
|
+
mapper.text('undefined');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
mapper.text(')');
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function emitBinaryOperatorExpression(
|
|
438
|
+
formInfo: SpecialFormInfo,
|
|
439
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
440
|
+
): void {
|
|
441
|
+
mapper.forNode(node, () => {
|
|
442
|
+
assert(
|
|
443
|
+
node.hash.pairs.length === 0,
|
|
444
|
+
() => `{{${formInfo.name}}} only accepts positional parameters`,
|
|
445
|
+
);
|
|
446
|
+
assert(
|
|
447
|
+
node.params.length === 2,
|
|
448
|
+
() => `{{${formInfo.name}}} requires exactly two parameters`,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const [left, right] = node.params;
|
|
452
|
+
|
|
453
|
+
mapper.text('(');
|
|
454
|
+
emitExpression(left);
|
|
455
|
+
mapper.text(` ${formInfo.form} `);
|
|
456
|
+
emitExpression(right);
|
|
457
|
+
mapper.text(')');
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function emitLogicalExpression(
|
|
462
|
+
formInfo: SpecialFormInfo,
|
|
463
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
464
|
+
): void {
|
|
465
|
+
mapper.forNode(node, () => {
|
|
466
|
+
assert(
|
|
467
|
+
node.hash.pairs.length === 0,
|
|
468
|
+
() => `{{${formInfo.name}}} only accepts positional parameters`,
|
|
469
|
+
);
|
|
470
|
+
assert(
|
|
471
|
+
node.params.length >= 2,
|
|
472
|
+
() => `{{${formInfo.name}}} requires at least two parameters`,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
mapper.text('(');
|
|
476
|
+
for (const [index, param] of node.params.entries()) {
|
|
477
|
+
emitExpression(param);
|
|
478
|
+
|
|
479
|
+
if (index < node.params.length - 1) {
|
|
480
|
+
mapper.text(` ${formInfo.form} `);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
mapper.text(')');
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function emitUnaryOperatorExpression(
|
|
488
|
+
formInfo: SpecialFormInfo,
|
|
489
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
490
|
+
): void {
|
|
491
|
+
mapper.forNode(node, () => {
|
|
492
|
+
assert(
|
|
493
|
+
node.hash.pairs.length === 0,
|
|
494
|
+
() => `{{${formInfo.name}}} only accepts positional parameters`,
|
|
495
|
+
);
|
|
496
|
+
assert(
|
|
497
|
+
node.params.length === 1,
|
|
498
|
+
() => `{{${formInfo.name}}} requires exactly one parameter`,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const [param] = node.params;
|
|
502
|
+
|
|
503
|
+
mapper.text(formInfo.form);
|
|
504
|
+
emitExpression(param);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
type SpecialFormInfo = {
|
|
509
|
+
form: GlintSpecialForm;
|
|
510
|
+
name: string;
|
|
511
|
+
requiresConsumption: boolean;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function checkSpecialForm(node: AST.CallNode): SpecialFormInfo | null {
|
|
515
|
+
if (
|
|
516
|
+
node.path.type === 'PathExpression' &&
|
|
517
|
+
node.path.head.type === 'VarHead' &&
|
|
518
|
+
!node.path.tail.length
|
|
519
|
+
) {
|
|
520
|
+
let name = node.path.head.name;
|
|
521
|
+
if (typeof specialForms[name] === 'string' && !scope.hasBinding(name)) {
|
|
522
|
+
let isGlobal = globals ? globals.includes(name) : true;
|
|
523
|
+
let form = specialForms[name];
|
|
524
|
+
|
|
525
|
+
return { name, form, requiresConsumption: !isGlobal };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function emitExpression(node: AST.Expression): void {
|
|
533
|
+
switch (node.type) {
|
|
534
|
+
case 'PathExpression':
|
|
535
|
+
return emitPath(node);
|
|
536
|
+
|
|
537
|
+
case 'SubExpression':
|
|
538
|
+
return emitSubExpression(node);
|
|
539
|
+
|
|
540
|
+
case 'BooleanLiteral':
|
|
541
|
+
case 'NullLiteral':
|
|
542
|
+
case 'NumberLiteral':
|
|
543
|
+
case 'StringLiteral':
|
|
544
|
+
case 'UndefinedLiteral':
|
|
545
|
+
return emitLiteral(node);
|
|
546
|
+
|
|
547
|
+
default:
|
|
548
|
+
unreachable(node);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function emitElementNode(node: AST.ElementNode): void {
|
|
553
|
+
let firstCharacter = node.tag.charAt(0);
|
|
554
|
+
if (
|
|
555
|
+
firstCharacter.toUpperCase() === firstCharacter ||
|
|
556
|
+
node.tag.includes('.') ||
|
|
557
|
+
scope.hasBinding(node.tag)
|
|
558
|
+
) {
|
|
559
|
+
emitComponent(node);
|
|
560
|
+
} else {
|
|
561
|
+
emitPlainElement(node);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function emitConcatStatement(node: AST.ConcatStatement): void {
|
|
566
|
+
mapper.forNode(node, () => {
|
|
567
|
+
mapper.text('`');
|
|
568
|
+
for (let part of node.parts) {
|
|
569
|
+
if (part.type === 'MustacheStatement') {
|
|
570
|
+
mapper.text('$' + '{');
|
|
571
|
+
emitMustacheStatement(part, 'concat');
|
|
572
|
+
mapper.text('}');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
mapper.text('`');
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function emitIdentifierReference(name: string, hbsOffset: number): void {
|
|
580
|
+
if (treatAsGlobal(name)) {
|
|
581
|
+
mapper.text('__glintDSL__.Globals["');
|
|
582
|
+
mapper.identifier(JSON.stringify(name).slice(1, -1), hbsOffset, name.length);
|
|
583
|
+
mapper.text('"]');
|
|
584
|
+
} else {
|
|
585
|
+
mapper.identifier(makeJSSafe(name), hbsOffset, name.length);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function treatAsGlobal(name: string): boolean {
|
|
590
|
+
if (globals) {
|
|
591
|
+
// If we have a known set of global identifiers, we should only treat
|
|
592
|
+
// members of that set as global, unless the identifier is in scope,
|
|
593
|
+
// and assume everything else is local. This is typically true in
|
|
594
|
+
// environments that capture scope, like strict-mode Ember.
|
|
595
|
+
return globals.includes(name) && !scope.hasBinding(name);
|
|
596
|
+
} else {
|
|
597
|
+
// Otherwise, we assume everything is global unless we can see it
|
|
598
|
+
// in scope as a block variable. This is the case in resolver-based
|
|
599
|
+
// environments like loose-mode Ember.
|
|
600
|
+
return !scope.hasBinding(name);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function tagNameToPathContents(node: AST.ElementNode): {
|
|
605
|
+
start: number;
|
|
606
|
+
kind: PathKind;
|
|
607
|
+
path: Array<string>;
|
|
608
|
+
} {
|
|
609
|
+
let tagName = node.tag;
|
|
610
|
+
let start = template.indexOf(tagName, rangeForNode(node).start);
|
|
611
|
+
|
|
612
|
+
if (tagName.startsWith('@')) {
|
|
613
|
+
return {
|
|
614
|
+
start,
|
|
615
|
+
kind: 'arg',
|
|
616
|
+
path: tagName.slice(1).split('.'),
|
|
617
|
+
};
|
|
618
|
+
} else if (tagName.startsWith('this.')) {
|
|
619
|
+
return {
|
|
620
|
+
start,
|
|
621
|
+
kind: 'this',
|
|
622
|
+
path: tagName.slice('this.'.length).split('.'),
|
|
623
|
+
};
|
|
624
|
+
} else {
|
|
625
|
+
return {
|
|
626
|
+
start,
|
|
627
|
+
kind: 'free',
|
|
628
|
+
path: tagName.split('.'),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function emitComponent(node: AST.ElementNode): void {
|
|
634
|
+
mapper.forNode(node, () => {
|
|
635
|
+
let { start, path, kind } = tagNameToPathContents(node);
|
|
636
|
+
|
|
637
|
+
for (let comment of node.comments) {
|
|
638
|
+
emitComment(comment);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
mapper.text('{');
|
|
642
|
+
mapper.newline();
|
|
643
|
+
mapper.indent();
|
|
644
|
+
|
|
645
|
+
// Resolve the component and stash into the `__glintY__` variable for later invocation.
|
|
646
|
+
mapper.text('const __glintY__ = __glintDSL__.emitComponent(');
|
|
647
|
+
|
|
648
|
+
// Error boundary: "Expected 1 arguments, but got 0." e.g. when invoking `<ComponentThatHasArgs />`
|
|
649
|
+
mapper.forNode(node.path, () => {
|
|
650
|
+
mapper.text('__glintDSL__.resolve(');
|
|
651
|
+
emitPathContents(path, start, kind);
|
|
652
|
+
mapper.text(')');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// "Call" the component, optionally passing args if they are provided in the template.
|
|
656
|
+
mapper.text('(');
|
|
657
|
+
|
|
658
|
+
let dataAttrs = node.attributes.filter(({ name }) => name.startsWith('@'));
|
|
659
|
+
if (dataAttrs.length) {
|
|
660
|
+
// Error boundary: "Expected 0 arguments, but got 1." e.g. when invoking `<ComponentThatHasNoArgs @foo={{bar}} />`
|
|
661
|
+
mapper.forNodeWithSpan(node, node.openTag, () => {
|
|
662
|
+
mapper.text('{ ');
|
|
663
|
+
|
|
664
|
+
for (let attr of dataAttrs) {
|
|
665
|
+
mapper.forNode(attr, () => {
|
|
666
|
+
mapper.newline();
|
|
667
|
+
|
|
668
|
+
const attrStartOffset = attr.loc.getStart().offset!;
|
|
669
|
+
emitHashKey(attr.name.slice(1), attrStartOffset + prefix.length + 1);
|
|
670
|
+
mapper.text(': ');
|
|
671
|
+
|
|
672
|
+
switch (attr.value.type) {
|
|
673
|
+
case 'TextNode':
|
|
674
|
+
mapper.text(JSON.stringify(attr.value.chars));
|
|
675
|
+
break;
|
|
676
|
+
case 'ConcatStatement':
|
|
677
|
+
emitConcatStatement(attr.value);
|
|
678
|
+
break;
|
|
679
|
+
case 'MustacheStatement':
|
|
680
|
+
emitMustacheStatement(attr.value, 'arg');
|
|
681
|
+
break;
|
|
682
|
+
default:
|
|
683
|
+
unreachable(attr.value);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
start = rangeForNode(attr.value).end;
|
|
688
|
+
mapper.text(', ');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
mapper.text('...__glintDSL__.NamedArgsMarker }');
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
mapper.text('));');
|
|
696
|
+
mapper.newline();
|
|
697
|
+
|
|
698
|
+
emitAttributesAndModifiers(node);
|
|
699
|
+
|
|
700
|
+
if (!node.selfClosing) {
|
|
701
|
+
let blocks = determineBlockChildren(node);
|
|
702
|
+
if (blocks.type === 'named') {
|
|
703
|
+
for (const child of blocks.children) {
|
|
704
|
+
if (child.type === 'CommentStatement' || child.type === 'MustacheCommentStatement') {
|
|
705
|
+
emitComment(child);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let childStart = rangeForNode(child).start;
|
|
710
|
+
let nameStart = template.indexOf(child.tag, childStart) + ':'.length;
|
|
711
|
+
let blockParamsStart = template.indexOf('|', childStart);
|
|
712
|
+
let name = child.tag.slice(1);
|
|
713
|
+
|
|
714
|
+
mapper.forNode(child, () =>
|
|
715
|
+
emitBlockContents(
|
|
716
|
+
name,
|
|
717
|
+
nameStart,
|
|
718
|
+
child.blockParams,
|
|
719
|
+
blockParamsStart,
|
|
720
|
+
child.children,
|
|
721
|
+
),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
let blockParamsStart = template.indexOf('|', rangeForNode(node).start);
|
|
726
|
+
emitBlockContents(
|
|
727
|
+
'default',
|
|
728
|
+
undefined,
|
|
729
|
+
node.blockParams,
|
|
730
|
+
blockParamsStart,
|
|
731
|
+
blocks.children,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
mapper.dedent();
|
|
737
|
+
mapper.text('}');
|
|
738
|
+
mapper.newline();
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function isAllowedAmongNamedBlocks(node: AST.Node): boolean {
|
|
743
|
+
return (
|
|
744
|
+
(node.type === 'TextNode' && node.chars.trim() === '') ||
|
|
745
|
+
node.type === 'CommentStatement' ||
|
|
746
|
+
node.type === 'MustacheCommentStatement'
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function isNamedBlock(node: AST.Node): boolean {
|
|
751
|
+
return node.type === 'ElementNode' && node.tag.startsWith(':');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
type NamedBlockChild = AST.ElementNode | AST.CommentStatement | AST.MustacheCommentStatement;
|
|
755
|
+
type BlockChildren =
|
|
756
|
+
| { type: 'named'; children: NamedBlockChild[] }
|
|
757
|
+
| { type: 'default'; children: AST.TopLevelStatement[] };
|
|
758
|
+
|
|
759
|
+
function determineBlockChildren(node: AST.ElementNode): BlockChildren {
|
|
760
|
+
let named = 0;
|
|
761
|
+
let other = 0;
|
|
762
|
+
|
|
763
|
+
for (let child of node.children) {
|
|
764
|
+
if (isAllowedAmongNamedBlocks(child)) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (isNamedBlock(child)) {
|
|
769
|
+
named += 1;
|
|
770
|
+
} else {
|
|
771
|
+
other += 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (named === 0) {
|
|
776
|
+
return { type: 'default', children: node.children };
|
|
777
|
+
} else if (other === 0) {
|
|
778
|
+
return {
|
|
779
|
+
type: 'named',
|
|
780
|
+
children: node.children.filter(
|
|
781
|
+
// Filter out ignorable content between named blocks
|
|
782
|
+
(child): child is NamedBlockChild => child.type === 'ElementNode',
|
|
783
|
+
// ||
|
|
784
|
+
// child.type === 'CommentStatement' ||
|
|
785
|
+
// child.type === 'MustacheCommentStatement',
|
|
786
|
+
),
|
|
787
|
+
};
|
|
788
|
+
} else {
|
|
789
|
+
// If we get here, meaningful content was mixed with named blocks,
|
|
790
|
+
// so it's worth doing the additional work to produce errors for
|
|
791
|
+
// those nodes
|
|
792
|
+
for (let child of node.children) {
|
|
793
|
+
if (!isNamedBlock(child)) {
|
|
794
|
+
mapper.forNode(child, () =>
|
|
795
|
+
assert(
|
|
796
|
+
isAllowedAmongNamedBlocks(child),
|
|
797
|
+
'Named blocks may not be mixed with other content',
|
|
798
|
+
),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return { type: 'named', children: [] };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function emitPlainElement(node: AST.ElementNode): void {
|
|
808
|
+
mapper.forNode(node, () => {
|
|
809
|
+
if (node.tag === 'svg') {
|
|
810
|
+
inHtmlContext = 'svg';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (node.tag === 'math') {
|
|
814
|
+
inHtmlContext = 'math';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
for (let comment of node.comments) {
|
|
818
|
+
emitComment(comment);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
mapper.text('{');
|
|
822
|
+
mapper.newline();
|
|
823
|
+
mapper.indent();
|
|
824
|
+
|
|
825
|
+
if (inHtmlContext === 'default') {
|
|
826
|
+
mapper.text('const __glintY__ = __glintDSL__.emitElement("');
|
|
827
|
+
} else if (inHtmlContext === 'svg') {
|
|
828
|
+
mapper.text('const __glintY__ = __glintDSL__.emitSVGElement("');
|
|
829
|
+
} else if (inHtmlContext === 'math') {
|
|
830
|
+
mapper.text('const __glintY__ = __glintDSL__.emitMathMlElement("');
|
|
831
|
+
}
|
|
832
|
+
mapper.forNode(node.path, () => {
|
|
833
|
+
mapper.text(node.tag);
|
|
834
|
+
});
|
|
835
|
+
mapper.text('");');
|
|
836
|
+
mapper.newline();
|
|
837
|
+
|
|
838
|
+
emitAttributesAndModifiers(node);
|
|
839
|
+
|
|
840
|
+
for (let child of node.children) {
|
|
841
|
+
emitTopLevelStatement(child);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (node.tag === 'svg' || node.tag === 'math') {
|
|
845
|
+
inHtmlContext = 'default';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
mapper.dedent();
|
|
849
|
+
mapper.text('}');
|
|
850
|
+
mapper.newline();
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function emitAttributesAndModifiers(node: AST.ElementNode): void {
|
|
855
|
+
emitSplattributes(node);
|
|
856
|
+
emitPlainAttributes(node);
|
|
857
|
+
emitModifiers(node);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function emitPlainAttributes(node: AST.ElementNode): void {
|
|
861
|
+
let attributes = node.attributes.filter(
|
|
862
|
+
(attr) => !attr.name.startsWith('@') && attr.name !== SPLATTRIBUTES,
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// Only emit `applyAttributes` if there are attributes to apply.
|
|
866
|
+
if (attributes.length === 0) return;
|
|
867
|
+
|
|
868
|
+
const attrsSpan = node.openTag
|
|
869
|
+
.withStart(node.path.loc.getEnd())
|
|
870
|
+
.withEnd(node.openTag.getEnd().move(-1));
|
|
871
|
+
mapper.forNodeWithSpan(node, attrsSpan, () => {
|
|
872
|
+
let isFirstAttribute = true;
|
|
873
|
+
|
|
874
|
+
for (let attr of attributes) {
|
|
875
|
+
if (isFirstAttribute) {
|
|
876
|
+
isFirstAttribute = false;
|
|
877
|
+
|
|
878
|
+
mapper.text('__glintDSL__.applyAttributes(');
|
|
879
|
+
|
|
880
|
+
// We map the `__glintY__.element` arg to the first attribute node, which has the effect
|
|
881
|
+
// such that diagnostics due to passing attributes to invalid elements will show up
|
|
882
|
+
// on the attribute, rather than on the whole element.
|
|
883
|
+
mapper.forNode(attr, () => {
|
|
884
|
+
mapper.text('__glintY__.element');
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
mapper.text(', {');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
mapper.newline();
|
|
891
|
+
mapper.indent();
|
|
892
|
+
|
|
893
|
+
mapper.forNode(attr, () => {
|
|
894
|
+
const attrStartOffset = attr.loc.getStart().offset!;
|
|
895
|
+
emitHashKey(attr.name, attrStartOffset + prefix.length);
|
|
896
|
+
mapper.text(': ');
|
|
897
|
+
|
|
898
|
+
if (attr.value.type === 'MustacheStatement') {
|
|
899
|
+
emitMustacheStatement(attr.value, 'attr');
|
|
900
|
+
} else if (attr.value.type === 'ConcatStatement') {
|
|
901
|
+
emitConcatStatement(attr.value);
|
|
902
|
+
} else {
|
|
903
|
+
mapper.text(JSON.stringify(attr.value.chars));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
mapper.text(',');
|
|
907
|
+
mapper.newline();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
mapper.newline();
|
|
911
|
+
});
|
|
912
|
+
mapper.newline();
|
|
913
|
+
mapper.dedent();
|
|
914
|
+
mapper.text('});');
|
|
915
|
+
mapper.newline();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function emitSplattributes(node: AST.ElementNode): void {
|
|
919
|
+
let splattributes = node.attributes.find((attr) => attr.name === SPLATTRIBUTES);
|
|
920
|
+
if (!splattributes) return;
|
|
921
|
+
|
|
922
|
+
assert(
|
|
923
|
+
splattributes.value.type === 'TextNode' && splattributes.value.chars === '',
|
|
924
|
+
'`...attributes` cannot accept a value',
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
mapper.forNode(splattributes, () => {
|
|
928
|
+
mapper.text('__glintDSL__.applySplattributes(__glintRef__.element, __glintY__.element);');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
mapper.newline();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function emitModifiers(node: AST.ElementNode): void {
|
|
935
|
+
for (let modifier of node.modifiers) {
|
|
936
|
+
mapper.forNode(modifier, () => {
|
|
937
|
+
mapper.text('__glintDSL__.applyModifier(');
|
|
938
|
+
|
|
939
|
+
mapper.forNode(modifier, () => {
|
|
940
|
+
mapper.text('__glintDSL__.resolve(');
|
|
941
|
+
emitExpression(modifier.path);
|
|
942
|
+
mapper.text(')');
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
mapper.text('(__glintY__.element, ');
|
|
946
|
+
emitArgs(modifier.params, modifier.hash);
|
|
947
|
+
mapper.text('));');
|
|
948
|
+
mapper.newline();
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function emitMustacheStatement(node: AST.MustacheStatement, position: InvokePosition): void {
|
|
954
|
+
let specialFormInfo = checkSpecialForm(node);
|
|
955
|
+
if (specialFormInfo) {
|
|
956
|
+
emitSpecialFormExpression(specialFormInfo, node, position);
|
|
957
|
+
return;
|
|
958
|
+
} else if (node.path.type !== 'PathExpression' && node.path.type !== 'SubExpression') {
|
|
959
|
+
// This assertion is currently meaningless, as @glimmer/syntax silently drops
|
|
960
|
+
// any named or positional parameters passed in a literal mustache
|
|
961
|
+
assert(
|
|
962
|
+
node.params.length === 0 && node.hash.pairs.length === 0,
|
|
963
|
+
'Literals do not accept params',
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
emitLiteral(node.path);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
mapper.forNode(node, () => {
|
|
971
|
+
// If a mustache has parameters, we know it must be an invocation; if
|
|
972
|
+
// not, it depends on where it appears. In arg position, it's always
|
|
973
|
+
// passed directly as a value; otherwise it's invoked if it's a
|
|
974
|
+
// component/helper, and returned as a value otherwise.
|
|
975
|
+
let hasParams = Boolean(node.hash.pairs.length || node.params.length);
|
|
976
|
+
if (!hasParams && position === 'arg' && !isGlobal(node.path)) {
|
|
977
|
+
emitExpression(node.path);
|
|
978
|
+
} else if (position === 'top-level') {
|
|
979
|
+
// e.g. top-level mustache `{{someValue}}`
|
|
980
|
+
mapper.text('__glintDSL__.emitContent(');
|
|
981
|
+
emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
|
|
982
|
+
mapper.text(')');
|
|
983
|
+
} else {
|
|
984
|
+
emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function isGlobal(path: AST.Expression): boolean {
|
|
990
|
+
return Boolean(
|
|
991
|
+
path.type === 'PathExpression' &&
|
|
992
|
+
path.head.type === 'VarHead' &&
|
|
993
|
+
globals?.includes(path.head.name) &&
|
|
994
|
+
!scope.hasBinding(path.head.name),
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function emitYieldExpression(
|
|
999
|
+
formInfo: SpecialFormInfo,
|
|
1000
|
+
node: AST.MustacheStatement | AST.SubExpression,
|
|
1001
|
+
position: InvokePosition,
|
|
1002
|
+
): void {
|
|
1003
|
+
mapper.forNode(node, () => {
|
|
1004
|
+
assert(
|
|
1005
|
+
position === 'top-level',
|
|
1006
|
+
() => `{{${formInfo.name}}} may only appear as a top-level statement`,
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
let to = 'default';
|
|
1010
|
+
let toPair = node.hash.pairs.find((pair) => pair.key === 'to');
|
|
1011
|
+
if (toPair) {
|
|
1012
|
+
assert(
|
|
1013
|
+
toPair.value.type === 'StringLiteral',
|
|
1014
|
+
() => `Named block {{${formInfo.name}}}s must have a literal block name`,
|
|
1015
|
+
);
|
|
1016
|
+
to = toPair.value.value;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (to === 'inverse') {
|
|
1020
|
+
to = 'else';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
mapper.text('__glintDSL__.yieldToBlock(__glintRef__, ');
|
|
1024
|
+
mapper.text(JSON.stringify(to));
|
|
1025
|
+
mapper.text(')(');
|
|
1026
|
+
|
|
1027
|
+
for (let [index, param] of node.params.entries()) {
|
|
1028
|
+
if (index) {
|
|
1029
|
+
mapper.text(', ');
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
emitExpression(param);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
mapper.text(')');
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function emitSpecialFormStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
|
|
1040
|
+
if (formInfo.requiresConsumption) {
|
|
1041
|
+
emitExpression(node.path);
|
|
1042
|
+
mapper.text(';');
|
|
1043
|
+
mapper.newline();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
switch (formInfo.form) {
|
|
1047
|
+
case 'if':
|
|
1048
|
+
emitIfStatement(formInfo, node);
|
|
1049
|
+
break;
|
|
1050
|
+
|
|
1051
|
+
case 'if-not':
|
|
1052
|
+
emitUnlessStatement(formInfo, node);
|
|
1053
|
+
break;
|
|
1054
|
+
|
|
1055
|
+
case 'bind-invokable':
|
|
1056
|
+
mapper.error(
|
|
1057
|
+
`The {{${formInfo.name}}} helper can't be used directly in block form under Glint. ` +
|
|
1058
|
+
`Consider first binding the result to a variable, e.g. '{{#let (${formInfo.name} ...) as |...|}}' ` +
|
|
1059
|
+
`and then using the bound value.`,
|
|
1060
|
+
rangeForNode(node.path),
|
|
1061
|
+
);
|
|
1062
|
+
break;
|
|
1063
|
+
|
|
1064
|
+
default:
|
|
1065
|
+
mapper.error(`${formInfo.name} is not valid in block form`, rangeForNode(node.path));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function emitIfStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
|
|
1070
|
+
mapper.forNode(node, () => {
|
|
1071
|
+
assert(
|
|
1072
|
+
node.params.length === 1,
|
|
1073
|
+
() => `{{#${formInfo.name}}} requires exactly one condition`,
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
mapper.text('if (');
|
|
1077
|
+
emitExpression(node.params[0]);
|
|
1078
|
+
mapper.text(') {');
|
|
1079
|
+
mapper.newline();
|
|
1080
|
+
mapper.indent();
|
|
1081
|
+
|
|
1082
|
+
for (let statement of node.program.body) {
|
|
1083
|
+
emitTopLevelStatement(statement);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (node.inverse) {
|
|
1087
|
+
mapper.dedent();
|
|
1088
|
+
mapper.text('} else {');
|
|
1089
|
+
mapper.indent();
|
|
1090
|
+
mapper.newline();
|
|
1091
|
+
|
|
1092
|
+
for (let statement of node.inverse.body) {
|
|
1093
|
+
emitTopLevelStatement(statement);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
mapper.dedent();
|
|
1098
|
+
mapper.text('}');
|
|
1099
|
+
mapper.newline();
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function emitUnlessStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
|
|
1104
|
+
mapper.forNode(node, () => {
|
|
1105
|
+
assert(
|
|
1106
|
+
node.params.length === 1,
|
|
1107
|
+
() => `{{#${formInfo.name}}} requires exactly one condition`,
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
mapper.text('if (!(');
|
|
1111
|
+
emitExpression(node.params[0]);
|
|
1112
|
+
mapper.text(')) {');
|
|
1113
|
+
mapper.newline();
|
|
1114
|
+
mapper.indent();
|
|
1115
|
+
|
|
1116
|
+
for (let statement of node.program.body) {
|
|
1117
|
+
emitTopLevelStatement(statement);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (node.inverse) {
|
|
1121
|
+
mapper.dedent();
|
|
1122
|
+
mapper.text('} else {');
|
|
1123
|
+
mapper.indent();
|
|
1124
|
+
mapper.newline();
|
|
1125
|
+
|
|
1126
|
+
for (let statement of node.inverse.body) {
|
|
1127
|
+
emitTopLevelStatement(statement);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
mapper.dedent();
|
|
1132
|
+
mapper.text('}');
|
|
1133
|
+
mapper.newline();
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function emitBlockStatement(node: AST.BlockStatement): void {
|
|
1138
|
+
let specialFormInfo = checkSpecialForm(node);
|
|
1139
|
+
if (specialFormInfo) {
|
|
1140
|
+
emitSpecialFormStatement(specialFormInfo, node);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
mapper.forNode(node, () => {
|
|
1145
|
+
mapper.text('{');
|
|
1146
|
+
mapper.newline();
|
|
1147
|
+
mapper.indent();
|
|
1148
|
+
|
|
1149
|
+
mapper.text('const __glintY__ = __glintDSL__.emitComponent(');
|
|
1150
|
+
emitResolve(node, 'resolve');
|
|
1151
|
+
mapper.text(');');
|
|
1152
|
+
mapper.newline();
|
|
1153
|
+
|
|
1154
|
+
emitBlock('default', node.program);
|
|
1155
|
+
|
|
1156
|
+
if (node.inverse) {
|
|
1157
|
+
emitBlock('else', node.inverse);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// TODO: emit something corresponding to `{{/foo}}` like we do
|
|
1161
|
+
// for angle bracket components, so that symbol renames propagate?
|
|
1162
|
+
// A little hairier (ha) for mustaches, since they
|
|
1163
|
+
if (node.path.type === 'PathExpression') {
|
|
1164
|
+
let start = template.lastIndexOf(node.path.original, rangeForNode(node).end);
|
|
1165
|
+
emitPathContents(getPathParts(node.path), start, determinePathKind(node.path));
|
|
1166
|
+
mapper.text(';');
|
|
1167
|
+
mapper.newline();
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
mapper.dedent();
|
|
1171
|
+
mapper.text('}');
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
mapper.newline();
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function emitBlock(name: string, node: AST.Block): void {
|
|
1178
|
+
let paramsStart = template.lastIndexOf(
|
|
1179
|
+
'|',
|
|
1180
|
+
template.lastIndexOf('|', rangeForNode(node).end) - 1,
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
emitBlockContents(name, undefined, node.blockParams, paramsStart, node.body);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function emitBlockContents(
|
|
1187
|
+
name: string,
|
|
1188
|
+
nameOffset: number | undefined,
|
|
1189
|
+
blockParams: string[],
|
|
1190
|
+
blockParamsOffset: number,
|
|
1191
|
+
children: AST.TopLevelStatement[],
|
|
1192
|
+
): void {
|
|
1193
|
+
assert(
|
|
1194
|
+
blockParams.every((name) => !name.includes('-')),
|
|
1195
|
+
'Block params must be valid TypeScript identifiers',
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
scope.push(blockParams);
|
|
1199
|
+
|
|
1200
|
+
mapper.text('{');
|
|
1201
|
+
mapper.newline();
|
|
1202
|
+
mapper.indent();
|
|
1203
|
+
|
|
1204
|
+
mapper.text('const [');
|
|
1205
|
+
|
|
1206
|
+
let start = blockParamsOffset;
|
|
1207
|
+
for (let [index, param] of blockParams.entries()) {
|
|
1208
|
+
if (index) mapper.text(', ');
|
|
1209
|
+
|
|
1210
|
+
start = template.indexOf(param, start);
|
|
1211
|
+
mapper.identifier(makeJSSafe(param), start, param.length);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
mapper.text('] = __glintY__.blockParams');
|
|
1215
|
+
emitPropertyAccesss(name, { offset: nameOffset, synthetic: true });
|
|
1216
|
+
mapper.text(';');
|
|
1217
|
+
mapper.newline();
|
|
1218
|
+
|
|
1219
|
+
for (let statement of children) {
|
|
1220
|
+
emitTopLevelStatement(statement);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
mapper.dedent();
|
|
1224
|
+
mapper.text('}');
|
|
1225
|
+
mapper.newline();
|
|
1226
|
+
scope.pop();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function emitSubExpression(node: AST.SubExpression): void {
|
|
1230
|
+
let specialFormInfo = checkSpecialForm(node);
|
|
1231
|
+
if (specialFormInfo) {
|
|
1232
|
+
emitSpecialFormExpression(specialFormInfo, node, 'sexpr');
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
mapper.forNode(node, () => {
|
|
1237
|
+
emitResolve(node, 'resolve');
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** An AST node that represents an invocation of some template entity in curlies */
|
|
1242
|
+
type CurlyInvocationNode =
|
|
1243
|
+
| AST.MustacheStatement
|
|
1244
|
+
| AST.SubExpression
|
|
1245
|
+
| AST.BlockStatement
|
|
1246
|
+
| AST.ElementModifierStatement;
|
|
1247
|
+
|
|
1248
|
+
function emitResolve(node: CurlyInvocationNode, resolveType: string): void {
|
|
1249
|
+
// We use forNode here to wrap the emitted resolve expression here so that when
|
|
1250
|
+
// we convert to Volar mappings, we can create a boundary around
|
|
1251
|
+
// e.g. "__glintDSL__.resolveOrReturn(expectsAtLeastOneArg)()", which is required because
|
|
1252
|
+
// this is where TS might generate a diagnostic error.
|
|
1253
|
+
mapper.forNode(node, () => {
|
|
1254
|
+
mapper.text('__glintDSL__.');
|
|
1255
|
+
mapper.text(resolveType);
|
|
1256
|
+
mapper.text('(');
|
|
1257
|
+
emitExpression(node.path);
|
|
1258
|
+
mapper.text(')(');
|
|
1259
|
+
emitArgs(node.params, node.hash);
|
|
1260
|
+
mapper.text(')');
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function emitArgs(positional: Array<AST.Expression>, named: AST.Hash): void {
|
|
1265
|
+
// Emit positional args
|
|
1266
|
+
for (let [index, param] of positional.entries()) {
|
|
1267
|
+
if (index) {
|
|
1268
|
+
mapper.text(', ');
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
emitExpression(param);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Emit named args
|
|
1275
|
+
if (named.pairs.length) {
|
|
1276
|
+
if (positional.length) {
|
|
1277
|
+
mapper.text(', ');
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// TS diagnostic error boundary
|
|
1281
|
+
mapper.forNode(named, () => {
|
|
1282
|
+
mapper.text('{ ');
|
|
1283
|
+
|
|
1284
|
+
let { start } = rangeForNode(named);
|
|
1285
|
+
for (let [index, pair] of named.pairs.entries()) {
|
|
1286
|
+
start = template.indexOf(pair.key, start);
|
|
1287
|
+
emitHashKey(pair.key, start);
|
|
1288
|
+
mapper.text(': ');
|
|
1289
|
+
emitExpression(pair.value);
|
|
1290
|
+
|
|
1291
|
+
if (index === named.pairs.length - 1) {
|
|
1292
|
+
mapper.text(' ');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
start = rangeForNode(pair.value).end;
|
|
1296
|
+
mapper.text(', ');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
mapper.text('...__glintDSL__.NamedArgsMarker }');
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
type PathKind = 'this' | 'arg' | 'free';
|
|
1305
|
+
|
|
1306
|
+
function emitPath(node: AST.PathExpression): void {
|
|
1307
|
+
mapper.forNode(node, () => {
|
|
1308
|
+
let { start } = rangeForNode(node);
|
|
1309
|
+
emitPathContents(getPathParts(node), start, determinePathKind(node));
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function determinePathKind(node: AST.PathExpression): PathKind {
|
|
1314
|
+
switch (node.head.type) {
|
|
1315
|
+
case 'AtHead':
|
|
1316
|
+
return 'arg';
|
|
1317
|
+
case 'ThisHead':
|
|
1318
|
+
return 'this';
|
|
1319
|
+
case 'VarHead':
|
|
1320
|
+
return 'free';
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function emitPathContents(parts: string[], start: number, kind: PathKind): void {
|
|
1325
|
+
if (kind === 'this') {
|
|
1326
|
+
let thisStart = template.indexOf('this', start);
|
|
1327
|
+
mapper.text('__glintRef__.');
|
|
1328
|
+
mapper.identifier('this', thisStart);
|
|
1329
|
+
start = template.indexOf('.', thisStart) + 1;
|
|
1330
|
+
} else if (kind === 'arg') {
|
|
1331
|
+
mapper.text('__glintRef__.args');
|
|
1332
|
+
start = template.indexOf('@', start) + 1;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
let head = parts[0];
|
|
1336
|
+
if (!head) return;
|
|
1337
|
+
|
|
1338
|
+
start = template.indexOf(head, start);
|
|
1339
|
+
|
|
1340
|
+
// The first segment of a non-this, non-arg path must resolve
|
|
1341
|
+
// to some in-scope identifier.
|
|
1342
|
+
if (kind === 'free') {
|
|
1343
|
+
emitIdentifierReference(head, start);
|
|
1344
|
+
} else {
|
|
1345
|
+
emitPropertyAccesss(head, { offset: start, optional: false });
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
start += head.length;
|
|
1349
|
+
|
|
1350
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1351
|
+
let part = parts[i];
|
|
1352
|
+
start = template.indexOf(part, start);
|
|
1353
|
+
emitPropertyAccesss(part, { offset: start, optional: true });
|
|
1354
|
+
start += part.length;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
type PropertyAccessOptions = {
|
|
1359
|
+
offset?: number;
|
|
1360
|
+
optional?: boolean;
|
|
1361
|
+
synthetic?: boolean;
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
function emitPropertyAccesss(
|
|
1365
|
+
name: string,
|
|
1366
|
+
{ offset, optional, synthetic }: PropertyAccessOptions = {},
|
|
1367
|
+
): void {
|
|
1368
|
+
// Synthetic accesses should always use `[]` notation to avoid incidentally triggering
|
|
1369
|
+
// `noPropertyAccessFromIndexSignature`. Emitting `{{foo.bar}}` property accesses, however,
|
|
1370
|
+
// should use `.` notation for exactly the same reason.
|
|
1371
|
+
if (!synthetic && isSafeKey(name)) {
|
|
1372
|
+
mapper.text(optional ? '?.' : '.');
|
|
1373
|
+
if (offset) {
|
|
1374
|
+
mapper.identifier(name, offset);
|
|
1375
|
+
} else {
|
|
1376
|
+
mapper.text(name);
|
|
1377
|
+
}
|
|
1378
|
+
} else {
|
|
1379
|
+
mapper.text(optional ? '?.[' : '[');
|
|
1380
|
+
if (offset) {
|
|
1381
|
+
emitIdentifierString(name, offset);
|
|
1382
|
+
} else {
|
|
1383
|
+
mapper.text(JSON.stringify(name));
|
|
1384
|
+
}
|
|
1385
|
+
mapper.text(']');
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function emitHashKey(name: string, start: number): void {
|
|
1390
|
+
if (isSafeKey(name)) {
|
|
1391
|
+
mapper.identifier(name, start);
|
|
1392
|
+
} else {
|
|
1393
|
+
emitIdentifierString(name, start);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function emitIdentifierString(name: string, start: number): void {
|
|
1398
|
+
mapper.text('"');
|
|
1399
|
+
mapper.identifier(JSON.stringify(name).slice(1, -1), start, name.length);
|
|
1400
|
+
mapper.text('"');
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function emitLiteral(node: AST.Literal): void {
|
|
1404
|
+
mapper.forNode(node, () =>
|
|
1405
|
+
mapper.text(node.value === undefined ? 'undefined' : JSON.stringify(node.value)),
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function isSafeKey(key: string): boolean {
|
|
1410
|
+
return /^[a-z_$][a-z0-9_$]*$/i.test(key);
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const JSKeywords = new Set([
|
|
1416
|
+
'await',
|
|
1417
|
+
'break',
|
|
1418
|
+
'case',
|
|
1419
|
+
'catch',
|
|
1420
|
+
'class',
|
|
1421
|
+
'const',
|
|
1422
|
+
'continue',
|
|
1423
|
+
'debugger',
|
|
1424
|
+
'default',
|
|
1425
|
+
'delete',
|
|
1426
|
+
'do',
|
|
1427
|
+
'else',
|
|
1428
|
+
'enum',
|
|
1429
|
+
'eval',
|
|
1430
|
+
'export',
|
|
1431
|
+
'extends',
|
|
1432
|
+
'false',
|
|
1433
|
+
'finally',
|
|
1434
|
+
'for',
|
|
1435
|
+
'function',
|
|
1436
|
+
'if',
|
|
1437
|
+
'implements',
|
|
1438
|
+
'import',
|
|
1439
|
+
'in',
|
|
1440
|
+
'instanceof',
|
|
1441
|
+
'interface',
|
|
1442
|
+
'let',
|
|
1443
|
+
'new',
|
|
1444
|
+
'null',
|
|
1445
|
+
'package',
|
|
1446
|
+
'private',
|
|
1447
|
+
'protected',
|
|
1448
|
+
'public',
|
|
1449
|
+
'return',
|
|
1450
|
+
'static',
|
|
1451
|
+
'super',
|
|
1452
|
+
'switch',
|
|
1453
|
+
'this',
|
|
1454
|
+
'throw',
|
|
1455
|
+
'true',
|
|
1456
|
+
'try',
|
|
1457
|
+
'typeof',
|
|
1458
|
+
'undefined',
|
|
1459
|
+
'var',
|
|
1460
|
+
'void',
|
|
1461
|
+
'while',
|
|
1462
|
+
'with',
|
|
1463
|
+
'yield',
|
|
1464
|
+
]);
|
|
1465
|
+
|
|
1466
|
+
function isJSKeyword(token: string): boolean {
|
|
1467
|
+
return JSKeywords.has(token);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function makeJSSafe(identifier: string): string {
|
|
1471
|
+
if (isJSKeyword(identifier) || identifier.startsWith('__')) {
|
|
1472
|
+
return `__${identifier}`;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return identifier;
|
|
1476
|
+
}
|