Source: dynamic-data-mapping-form-builder/src/main/resources/META-INF/resources/js/expressions/Tokenizer.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import Token from './Token.es';

const OPERATORS = ['*', '/', '+', '-'];

/**
 * Tokenizer.
 * Transforms an expression into tokens and token into an expression
 */
class Tokenizer {
	static stringifyTokens(tokens) {
		return tokens.reduce((expression, token) => {
			let {value} = token;

			if (token.type === Token.VARIABLE) {
				value = `[${value}]`;
			}

			return expression + value;
		}, '');
	}

	static tokenize(str) {
		const result = [];

		str = str.replace(/\s/g, '');

		let functionBuffer = [];
		let numberBuffer = [];
		let variableBuffer = [];

		const emptyNumberBuffer = () => {
			if (numberBuffer.length) {
				result.push(new Token(Token.LITERAL, numberBuffer.join('')));
				numberBuffer = [];
			}
		};

		const emptyFunctionBuffer = () => {
			result.push(new Token(Token.FUNCTION, functionBuffer.join('')));
			functionBuffer = [];
		};

		const emptyVariableBuffer = () => {
			result.push(new Token(Token.VARIABLE, variableBuffer.join('')));
			variableBuffer = [];
		};

		const inputBuffer = str.split('');

		while (inputBuffer.length) {
			let char = inputBuffer.shift();

			if (this.isDigit(char)) {
				numberBuffer.push(char);
			}
			else if (char === '.') {
				numberBuffer.push(char);
			}
			else if (this.isLeftBracket(char)) {
				if (numberBuffer.length) {
					emptyNumberBuffer();

					result.push(new Token(Token.OPERATOR, '*'));
				}

				do {
					char = inputBuffer.shift();

					if (this.isRightBracket(char)) {
						emptyVariableBuffer();

						break;
					}
					else {
						variableBuffer.push(char);
					}
				} while (inputBuffer.length);
			}
			else if (this.isLetter(char)) {
				if (numberBuffer.length) {
					emptyNumberBuffer();

					result.push(new Token(Token.OPERATOR, '*'));
				}

				do {
					functionBuffer.push(char);

					char = inputBuffer.shift();
				} while (this.isLetter(char));

				if (char !== undefined) {
					inputBuffer.unshift(char);
				}

				emptyFunctionBuffer();
			}
			else if (this.isOperator(char)) {
				emptyNumberBuffer();

				result.push(new Token(Token.OPERATOR, char));
			}
			else if (this.isLeftParenthesis(char)) {
				if (numberBuffer.length) {
					emptyNumberBuffer();

					result.push(new Token(Token.OPERATOR, '*'));
				}

				result.push(new Token(Token.LEFT_PARENTHESIS, char));
			}
			else if (this.isRightParenthesis(char)) {
				emptyNumberBuffer();

				result.push(new Token(Token.RIGHT_PARENTHESIS, char));
			}
			else {
				throw new Error(`Unsupported character ${char}`);
			}
		}

		if (numberBuffer.length) {
			emptyNumberBuffer();
		}
		if (variableBuffer.length) {
			emptyVariableBuffer();
		}

		return result;
	}

	static isDigit(char) {
		return char !== undefined && /[0-9]/.test(char);
	}

	static isLetter(char) {
		return char !== undefined && /[a-zA-Z]/.test(char);
	}

	static isLeftBracket(char) {
		return char === '[';
	}

	static isRightBracket(char) {
		return char === ']';
	}

	static isLeftParenthesis(char) {
		return char === '(';
	}

	static isRightParenthesis(char) {
		return char === ')';
	}

	static isOperator(char) {
		return OPERATORS.includes(char);
	}
}

export default Tokenizer;