UNPKG

metal-soy-critic

Version:
202 lines (201 loc) 8.14 kB
"use strict"; 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; ;