UNPKG

@airtasker/form-schema-compiler

Version:
299 lines (266 loc) 7.33 kB
import find from "lodash/find"; import { OPERATORS, PUNCTUATIONS } from "./../const"; import * as utils from "./utils"; import { BOOLEANS, TYPES } from "../const"; const defaultShouldEnd = inputStream => inputStream.eof(); /** * transpiling expression string input to an expression token stream * e.g "a" to {type: identifier, name: 'a'} * more example see tests * @param {{next, peek, croak}}: inputStream * @param {Function}: shouldEnd * @param {{eof}}: shouldEnd * @returns {{next: (function(): Object), peek: (function(): Object), eof: (function(): boolean), croak: (function(*)), position: (function(): {pos: number, line: number, col: number})}} */ const createExpressionTokenStream = ( inputStream, shouldEnd = defaultShouldEnd, errors = { eof: "Not expect input ended at time" } ) => { let isStringTemplateMode = false; const readNumber = () => { let hasDot = false; const number = utils.readWhile(inputStream, ch => { if (ch === ".") { if (hasDot) { inputStream.croak(`Unexpected token:. , double dot in a number`); } hasDot = true; return true; } return utils.isDigit(ch); }); return { type: TYPES.Numeric, value: Number(number) }; }; /** * readIdentifier * @returns {*} */ function readIdentifier() { // read until char is not matching identifier rule const value = utils.readWhile(inputStream, utils.isId); if (utils.isBoolean(value)) { // if identifier is 'true' or 'false' then it's a boolean return { type: TYPES.Boolean, value: value === BOOLEANS[1] }; } if (utils.isOperatorString(value)) { // if identifier is 'is' 'isnt' 'not' 'match' then it's an operator return { type: TYPES.Operator, value }; } if (utils.isKeyword(value)) { return { type: TYPES.Keyword, value }; } if (utils.isNull(value)) { // if identifier is 'null' then it's a null return { type: TYPES.Null, value: null }; } return { type: TYPES.Identifier, name: value }; } const validFlagChars = "gimuy"; const readRegexp = () => { inputStream.next(); // read regexp until see tne regexp ending char (/) const pattern = utils.readWhileWithEscaped(inputStream, ch => ch === "/"); let flags = ""; while (!inputStream.eof()) { const ch = inputStream.peek(); if (validFlagChars.includes(ch) && !flags.includes(ch)) { // only add valid flags and not allow duplication flags += ch; inputStream.next(); } else if (utils.isWhitespace(ch) || utils.isPunctuation(ch)) { // end of regexp break; } else { inputStream.croak( `Uncaught SyntaxError: Invalid regular expression flags: "${flags}${ch}"` ); } } return { type: TYPES.RegExp, pattern, flags }; }; const readString = ch => { inputStream.next(); const ast = { type: TYPES.String, value: utils.readEscaped(inputStream, c => c === ch) }; inputStream.next(); return ast; }; let stringTemplateExpressionSteam = null; const initStringTemplateMode = () => { stringTemplateExpressionSteam = null; isStringTemplateMode = true; }; const readTemplateLiteral = () => { let ch = inputStream.peek(); if (inputStream.eof()) { inputStream.croak(`String template does not end`); } if (stringTemplateExpressionSteam) { if (stringTemplateExpressionSteam.eof()) { stringTemplateExpressionSteam = null; return { type: TYPES.Punctuation, value: inputStream.next() }; } return stringTemplateExpressionSteam.next(); } if (utils.isBackQuote(ch)) { isStringTemplateMode = false; return { type: TYPES.Punctuation, value: inputStream.next() }; } if (utils.isBraceStart(ch)) { const ast = { type: TYPES.Punctuation, value: inputStream.next() }; stringTemplateExpressionSteam = createExpressionTokenStream( inputStream, (stream, c) => utils.isBraceEnd(c), { eof: 'String Template expression missing a "}"' } ); if (stringTemplateExpressionSteam.eof()) { inputStream.croak( "Unexpected empty expression in string template e.g {}" ); } return ast; } // must be string return { type: TYPES.String, value: utils.readEscaped( inputStream, c => utils.isBraceStart(c) || utils.isBackQuote(c) ) }; }; const readOperator = () => { let value = inputStream.next(); if (inputStream.peek() === "=") { value += inputStream.next(); } return { type: TYPES.Operator, value }; }; const skipComment = () => { inputStream.next(); utils.readWhile(inputStream, ch => ch !== "\n" && ch !== "#"); inputStream.next(); }; // eslint-disable-next-line no-use-before-define const { peek, next, queue } = utils.createPeekAndNext(readNext, 1); const regexpLeadingTokens = [ { type: TYPES.Punctuation, value: PUNCTUATIONS.Parentheses[0] }, { type: TYPES.Punctuation, value: PUNCTUATIONS.Separator }, { type: TYPES.Operator, value: OPERATORS.Match } ]; // check previous token if read / // if previous token is number or string or identifier it's must be division instead of a start of regex const canBeRegexp = () => queue.length === 0 || find(regexpLeadingTokens, queue[0]) !== undefined; // eslint-disable-next-line consistent-return function readNext() { if (isStringTemplateMode) { return readTemplateLiteral(); } // skip the white space if it is not in template literal utils.readWhile(inputStream, utils.isWhitespace); const ch = inputStream.peek(); if (shouldEnd(inputStream, ch)) { return null; } if (ch === "#") { skipComment(); return readNext(); } if (utils.isStringStart(ch)) { return readString(ch); } if (utils.isDigit(ch)) { return readNumber(); } if (utils.isIdStart(ch)) { return readIdentifier(); } if (utils.isRegexpStart(ch) && canBeRegexp()) { return readRegexp(); } if (utils.isOperatorStart(ch)) { // when read a char is > or < means this possibly a >= or <= return readOperator(); } if (utils.isBackQuote(ch)) { initStringTemplateMode(); return { type: TYPES.Punctuation, value: inputStream.next() }; } if (utils.isPunctuation(ch)) { return { type: TYPES.Punctuation, value: inputStream.next() }; } if (utils.isOperatorChar(ch)) { return { type: TYPES.Operator, value: inputStream.next() }; } if (inputStream.eof()) { inputStream.croak(errors.eof); } else { inputStream.croak(`Can't handle character: "${ch}"`); } } const eof = () => peek() === null; return { ...inputStream, next, peek, eof }; }; export default createExpressionTokenStream;