metal-soy-critic
Version:
A metal-soy code validation utility.
202 lines (201 loc) • 8.14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
const P = require("parsimmon");
const S = require("./soy-types");
/* Parsers */
const asterisk = P.string('*');
const closingBrace = P.string('/}');
const colon = P.string(':');
const comma = P.string(',');
const docEnd = P.string('*/');
const docStart = P.string('/**');
const dollar = P.string('$');
const dquote = P.string('"');
const lbracket = P.string('[');
const lparen = P.string('(');
const newLine = P.string('\n');
const qmark = P.string('?');
const rbrace = P.string('}');
const rbracket = P.string(']');
const rparen = P.string(')');
const space = P.string(' ');
const squote = P.string('\'');
const underscore = P.string('_');
const attributeName = joined(P.letter, P.string('-'));
const functionName = joined(P.letter, underscore);
const html = P.noneOf('{}').many().desc("Html Char");
const identifierName = joined(P.letter, P.digit, underscore);
const namespace = joined(P.letter, P.digit, P.string('.'));
const templateName = namespace.map(util_1.parseTemplateName);
const namespaceCmd = P.string('{namespace')
.skip(P.whitespace)
.then(namespace)
.skip(rbrace);
const stringLiteral = nodeMap(S.StringLiteral, squote.then(withAny(squote)));
const booleanLiteral = nodeMap(S.BooleanLiteral, P.alt(P.string('true').result(true), P.string('false').result(false)));
const numberLiteral = nodeMap(S.NumberLiteral, P.seq(P.oneOf('+-').fallback(''), joined(P.digit, P.string('.'))).map(([sign, number]) => parseFloat(sign + number)));
const param = P.lazy(() => nodeMap(S.Param, P.string('{param')
.then(spaced(identifierName)), P.alt(spaced(attribute.many()).skip(rbrace).then(bodyFor('param')), spaced(colon).then(expression(closingBrace)))));
const functionCall = P.lazy(() => nodeMap(S.FunctionCall, functionName, lparen.then(functionArgs)));
const functionArgs = P.lazy(() => P.alt(rparen.result([]), expression(rparen).map(result => [result]), P.seqMap(expression(comma), functionArgs, util_1.reverseJoin)));
const reference = nodeMap(S.Reference, dollar.then(identifierName));
const letStatement = P.lazy(() => nodeMap(S.LetStatement, P.string('{let')
.skip(P.whitespace)
.skip(dollar)
.then(identifierName), P.alt(spaced(attribute.many()).skip(rbrace).then(bodyFor('let')), spaced(colon).then(expression(closingBrace)))));
const mapItem = nodeMap(S.MapItem, stringLiteral, spaced(colon).then(expression(P.alt(comma, rbracket))));
const mapLiteral = nodeMap(S.MapLiteral, lbracket.then(P.alt(spaced(mapItem).many(), rbracket.result([]))));
const call = nodeMap(S.Call, P.string('{call')
.skip(P.whitespace)
.then(templateName), P.alt(spaced(closingBrace).result([]), rbrace.then(spaced(param).many())
.skip(spaced(closeCmd('call')))));
const attribute = nodeMap(S.Attribute, attributeName.skip(P.string('="')), withAny(dquote));
const paramDeclaration = nodeMap(S.ParamDeclaration, P.string('{@param')
.then(optional(qmark))
.map(value => !value), spaced(identifierName), spaced(colon)
.then(withAny(rbrace)));
const soyDocComment = P.optWhitespace
.then(asterisk)
.skip(space.many())
.lookahead(P.noneOf('@/'))
.then(withAny(newLine.or(asterisk), false));
const soyDocParam = nodeMap((mark, optional, name) => S.ParamDeclaration(mark, optional, name, 'any'), spaced(asterisk)
.skip(P.string('@param'))
.then(optional(qmark))
.map(value => !value), spaced(identifierName));
const soyDoc = nodeMap((mark, nodes) => {
const lines = [];
const params = [];
nodes.forEach(node => {
if (typeof node === 'string') {
lines.push(node);
}
else {
params.push(node);
}
});
return S.SoyDoc(mark, lines.join('\n'), params);
}, spaced(docStart)
.then(soyDocParam.or(soyDocComment).many())
.skip(spaced(docEnd)));
const template = nodeMap(S.Template, optional(soyDoc), P.string('{template')
.skip(P.whitespace)
.then(templateName), spaced(attribute).many(), spaced(rbrace).then(spaced(paramDeclaration).many()), bodyFor('template'));
const delTemplate = nodeMap(S.DelTemplate, optional(soyDoc), P.string('{deltemplate')
.skip(P.whitespace)
.then(templateName), optional(P.seq(P.whitespace, P.string('variant='))
.then(interpolation('"'))), rbrace.then(spaced(paramDeclaration).many()), bodyFor('deltemplate'));
const program = nodeMap(S.Program, namespaceCmd, spaced(P.alt(template, delTemplate))
.atLeast(1)
.skip(P.eof));
const parser = program;
function nodeMap(mapper, ...parsers) {
return P.seq(...parsers)
.mark()
.map(({ start, value, end }) => {
return mapper({
start,
end
}, ...value);
});
}
function optional(parser) {
return parser
.atMost(1)
.map(values => values[0] || null);
}
function expression(end, stack = []) {
const spacedEnd = P.optWhitespace.then(end);
return realExpression(spacedEnd, stack)
.or(otherExpression(spacedEnd, stack));
}
function realExpression(end, stack) {
return P.lazy(() => P.alt(reference, stringLiteral, booleanLiteral, mapLiteral, numberLiteral, functionCall).chain(tryOperator(end, stack)));
}
function otherExpression(end, stack) {
return nodeMap(S.OtherExpression, withAny(end, false)).chain(tryOperator(end, stack));
}
function tryOperator(end, stack) {
return result => withOperator([...stack, result], end);
}
function withOperator(stack, end) {
switch (stack.length) {
case 1:
return P.alt(ternaryLeft(end, stack), P.succeed(stack[0])).skip(end);
case 2:
return ternaryRight(end, stack);
case 3:
const [cond, left, right] = stack;
return P.succeed(S.Ternary(combineMark(cond.mark, right.mark), cond, left, right));
default:
throw new SoyParseError(`Error parsing an operator of length ${stack.length}.`);
}
}
function ternaryLeft(end, stack) {
return P.whitespace
.skip(qmark)
.skip(P.whitespace)
.then(expression(end, stack));
}
function ternaryRight(end, stack) {
return P.whitespace
.skip(colon)
.skip(P.whitespace)
.then(expression(end, stack));
}
function interpolation(start, end = start) {
return nodeMap(S.Interpolation, P.string(start).then(withAny(P.string(end))));
}
function otherCmd(name, ...inter) {
return nodeMap((mark, body) => S.OtherCmd(mark, name, body), openCmd(name).then(bodyFor(name, ...inter)));
}
function bodyFor(name, ...inter) {
const bodyParser = P.lazy(() => html.then(P.alt(closeCmd(name).result([]), P.alt(...inter.map(openCmd))
.result([])
.then(bodyParser), P.seqMap(P.alt(call, letStatement, otherCmd('if', 'elseif', 'else'), otherCmd('foreach', 'ifempty'), otherCmd('msg', 'fallbackmsg'), otherCmd('switch'), otherCmd('literal'), interpolation('{', '}')), bodyParser, util_1.reverseJoin))));
return bodyParser;
}
function orAny(parser) {
const newParser = P.lazy(() => parser.or(P.any.then(newParser)));
return newParser;
}
function withAny(parser, consumeEnd = true) {
const newParser = P.lazy(() => P.alt(consumeEnd ? parser.result('') : P.lookahead(parser), P.seqMap(P.any, newParser, (s, next) => s + next)));
return newParser;
}
function spaced(parser) {
return P.optWhitespace
.then(parser)
.skip(P.optWhitespace);
}
function joined(...parsers) {
return P.alt(...parsers)
.atLeast(1)
.map(values => values.join(''));
}
function closeCmd(name) {
return P.string(`{/${name}}`);
}
function openCmd(name) {
return P.string(`{${name}`).skip(orAny(rbrace));
}
function combineMark(start, end) {
return {
start: start.start,
end: end.end
};
}
/* API */
class SoyParseError extends Error {
}
exports.SoyParseError = SoyParseError;
function parse(input) {
const result = parser.parse(input);
if (!result.status) {
throw new SoyParseError(`Expected: ${result.expected.join('\n')}`);
}
return result.value;
}
exports.default = parse;
;