els-addon-typed-templates
Version:
Ember Language Server Typed Templates
326 lines (316 loc) • 9.99 kB
text/typescript
import { PLACEHOLDER } from "./utils";
import { ASTv1 } from '@glimmer/syntax';
function serializeKey(key) {
return key.split(" - ")[0];
}
export function keyForItem(item) {
return `${positionForItem(item)} - ${item.type}`;
}
export function positionForItem(item) {
const { start, end } = item.loc;
return `${start.line},${start.column}:${end.line},${end.column}`;
}
function escapeDoubleQuotes(str) {
// from https://gist.github.com/getify/3667624;
return str.replace(/\\([\s\S])|(")/g,"\\$1$2");
}
export function normalizePathOriginal(node: ASTv1.PathExpression) {
let prepared = '';
if (node.head.type === 'AtHead') {
prepared = `this.args.${node.original
.replace(PLACEHOLDER, "")
.replace("@", "")}`;
} else if (node.head.type === 'ThisHead') {
prepared = `${node.original.replace(PLACEHOLDER, "")}`;
} else {
prepared = node.original;
}
let result = prepared.split('.').map(el=>{
if (el === 'firstObject' || el === 'lastObject') {
return `[0].`;
}
return el.includes('-') ? `["${el}"].`: `${el}.`;
}).join('');
result = result.replace(/\.\[/gi, '[');
if (result.endsWith('.')) {
result = result.slice(0, -1);
}
return result;
}
export function transformPathExpression(
node: ASTv1.PathExpression,
key,
{
getItemScopes,
tailForGlobalScope,
pathsForGlobalScope,
importNameForItem,
componentImport,
declaredInScope,
addImport,
addComponentImport,
getPathScopes,
yields,
componentsForImport,
globalScope,
blockPaths,
globalRegistry
}
) {
let result: string = "";
let simpleResult: string = "";
const builtinScopeImports = new Set();
if (node.head.type === 'AtHead') {
result = transform.wrapToFunction(normalizePathOriginal(node), key);
} else if (node.head.type === 'ThisHead') {
result = transform.fn(
componentImport ? "" : "this: null",
normalizePathOriginal(node),
key
);
} else {
const { foundKey, scopeKey, scopeChain } = getPathScopes(node, key);
if (foundKey === "globalScope") {
if (!(scopeKey in globalScope)) {
if (blockPaths.includes(key)) {
if (declaredInScope(scopeKey)) {
globalScope[scopeKey] = "AbstractBlockHelper";
builtinScopeImports.add('AbstractBlockHelper');
} else {
globalScope[scopeKey] = 'undefined';
}
if (
scopeKey in globalRegistry &&
componentsForImport.includes(scopeKey)
) {
addComponentImport(scopeKey, globalRegistry[scopeKey]);
}
} else {
if (scopeKey in globalRegistry) {
addImport(scopeKey, globalRegistry[scopeKey]);
globalScope[scopeKey] = importNameForItem(scopeKey);
} else {
if (declaredInScope(scopeKey)) {
globalScope[scopeKey] = "AbstractHelper";
builtinScopeImports.add('AbstractHelper');
} else {
globalScope[scopeKey] = 'undefined';
}
}
}
}
if (pathsForGlobalScope[scopeKey]) {
simpleResult = `this.globalScope["${scopeKey}"]`;
result = transform.fn(
pathsForGlobalScope[scopeKey],
`this.globalScope["${scopeKey}"]${
tailForGlobalScope[scopeKey]
? tailForGlobalScope[scopeKey]
: "(params, hash)"
}`,
key
);
if (scopeKey === "yield") {
const scopes = getItemScopes(key);
const slosestScope = scopes[0];
if (!slosestScope) {
console.log("unable to find scope for " + key);
} else {
yields.push(slosestScope[0]);
}
}
} else {
if (
scopeKey in globalRegistry &&
componentsForImport.includes(scopeKey)
) {
addComponentImport(scopeKey, globalRegistry[scopeKey]);
result = transform.fn(
`_, hash: typeof ${importNameForItem(
scopeKey
)}.prototype.args`,
`let klass = new ${importNameForItem(
scopeKey
)}(this as unknown, hash); klass.args = hash; return klass.defaultYield()`,
key
);
} else {
simpleResult = `this.globalScope["${scopeKey}"]`;
result = transform.fn(
"params?, hash?",
`this.globalScope["${scopeKey}"](params, hash)`,
key
);
}
}
} else {
if (scopeChain.length) {
result = transform.fn(
"",
`this["${foundKey[0]}"]()[${foundKey[1]}].${scopeChain.join(".")}`,
key
);
} else {
result = transform.fn(
"",
`this["${foundKey[0]}"]()[${foundKey[1]}]`,
key
);
}
}
}
return {result, simpleResult, builtinScopeImports: Array.from(builtinScopeImports)};
}
export const transform = {
klass: {},
support(node) {
return node.type in this;
},
transform(node: ASTv1.BaseNode, key: string, klass?: any) {
if (klass) {
this.klass = klass;
} else {
this.klass = {};
}
const typeKey = `TypeFor${node.type}`;
const typings = (typeKey in this) ? ': ' + this[typeKey](node) : '';
return this._wrap(this[node.type](node), key, typings);
},
wrapToFunction(str: string, key: string) {
return this._wrap(str, key);
},
addMark(key: string) {
return `/*@path-mark ${serializeKey(key)}*/`;
},
_wrap(str: string, key: string, returnType: string = '') {
return `()${returnType} { return ${str}; ${this.addMark(key)}}`;
},
fn(args: string, body: string, key: string) {
return this._makeFn(args, body, key);
},
_makeFn(rawArgs: string, rawBody: string, key: string) {
let body = `return ${rawBody}`;
if (rawBody.includes("return ")) {
body = rawBody;
}
let args = `(${rawArgs})`;
if (rawArgs.includes("(") && rawArgs.includes(")")) {
args = rawArgs;
}
return `${args} { ${body}; ${this.addMark(key)}}`;
},
TextNode(node: ASTv1.TextNode) {
return `"${node.chars}"`;
},
TypeForTextNode(node: ASTv1.TextNode) {
return `"${node.chars}"`;
},
pathCall(node) {
let key = keyForItem(node);
let simpleKey = key + '_simple';
if (this.klass && this.klass[simpleKey]) {
let value = this.klass[simpleKey];
delete this.klass[simpleKey];
delete this.klass[key];
return value;
}
return `this["${keyForItem(node)}"]`;
},
hashedExp(node: ASTv1.BlockStatement | ASTv1.MustacheStatement | ASTv1.ElementModifierStatement | ASTv1.SubExpression, nodeType = 'DEFAULT') {
let result = "";
const params = node.params
.map(p => {
return `this["${keyForItem(p)}"]()`;
})
.join(",");
const hash = node.hash.pairs
.map(p => {
return `'${p.key}':this["${keyForItem(p.value)}"]()`;
})
.join(",");
if (hash.length && params.length) {
result = `${this.pathCall(node.path)}([${params}],{${hash}})`;
} else if (!hash.length && params.length) {
let p = node.params
.map(p => {
return `this["${keyForItem(p)}"]()`;
});
let maybeIf = node as ASTv1.MustacheStatement;
if (maybeIf.path.type === 'PathExpression') {
let maybeIfPath = maybeIf.path as ASTv1.PathExpression;
if (maybeIfPath.original === 'if' && maybeIfPath.head.type === 'VarHead') {
if (p.length === 3) {
return `${p[0]} ? ${p[1]} : ${p[2]}`;
} else if (p.length === 2) {
return `${p[0]} ? ${p[1]} : undefined`
}
}
if (maybeIfPath.original === 'unless' && maybeIfPath.head.type === 'VarHead') {
if (p.length === 3) {
return `!${p[0]} ? ${p[1]} : ${p[2]}`;
} else if (p.length === 2) {
return `!${p[0]} ? ${p[1]} : undefined`
}
}
}
result = `${this.pathCall(node.path)}([${params}])`;
} else if (hash.length && !params.length) {
result = `${this.pathCall(node.path)}([],{${hash}})`;
} else {
if (nodeType === 'BlockStatement') {
result = `${this.pathCall(node.path)}([], {})`;
} else {
result = `${this.pathCall(node.path)}()`;
}
}
return result;
},
SubExpression(node: ASTv1.SubExpression) {
return this.hashedExp(node);
},
ConcatStatement(node: ASTv1.ConcatStatement) {
return "`${"+node.parts.map((part) => this[part.type](part as any)).join('}${') + "}`";
},
TypeForConcatStatement() {
return `string`;
},
MustacheStatement(node: ASTv1.MustacheStatement) {
return this.hashedExp(node);
},
ElementModifierStatement(node: ASTv1.ElementModifierStatement) {
return this.hashedExp(node);
},
BlockStatement(node: ASTv1.BlockStatement) {
return this.hashedExp(node, 'BlockStatement');
},
NumberLiteral(node: ASTv1.NumberLiteral) {
return `${node.value}`;
},
TypeForNumberLiteral(node) {
return `${node.value}`;
},
StringLiteral(node: ASTv1.StringLiteral) {
return `"${escapeDoubleQuotes(node.value)}"`;
},
TypeForStringLiteral(node) {
return this.StringLiteral(node);
},
TypeForNullLiteral() {
return `null`;
},
TypeForUndefinedLiteral() {
return `undefined`;
},
TypeForBooleanLiteral() {
return `boolean`;
},
NullLiteral() {
return "null";
},
BooleanLiteral(node: ASTv1.BooleanLiteral) {
return `${node.value === true ? "true" : "false"}`;
},
UndefinedLiteral() {
return "undefined";
}
};