UNPKG

@cloudpss/template

Version:

String and object template engine for Node.js and the browser.

193 lines (179 loc) 6.77 kB
/** 公式模板 */ export type FormulaTemplate = { type: 'formula'; value: string; }; /** 字符串插值模板 */ export type InterpolationTemplate = { type: 'interpolation'; templates: string[]; values: string[]; }; /** 字符串模板 */ export type Template = string | FormulaTemplate | InterpolationTemplate; /** 字符串模板类型 */ export type TemplateType = (Template & object)['type']; const PARSER_STATUS_TEXT = 0; const PARSER_STATUS_EXPRESSION_SIMPLE = 1; const PARSER_STATUS_EXPRESSION_COMPLEX = 2; /** 解析状态机 */ type ParserStatus = | typeof PARSER_STATUS_TEXT | typeof PARSER_STATUS_EXPRESSION_SIMPLE | typeof PARSER_STATUS_EXPRESSION_COMPLEX; /** 检查字符满足 [a-zA-Z0-9_] */ function isIdentifierChar(char: string): boolean { const charCode = char.charCodeAt(0); return ( (charCode >= 97 && charCode <= 122) || (charCode >= 65 && charCode <= 90) || (charCode >= 48 && charCode <= 57) || charCode === 95 ); } /** 检查字符满足 [a-zA-Z_] */ function isIdentifierFirstChar(char: string): boolean { const charCode = char.charCodeAt(0); return (charCode >= 97 && charCode <= 122) || (charCode >= 65 && charCode <= 90) || charCode === 95; } /** 字符串插值模板标识符 */ const INTERPOLATION_CHAR = '$'; /** 字符串插值模板表达式开始标识符 */ const INTERPOLATION_EXPRESSION_START = ['{', '[', '<', '(']; /** 字符串插值模板表达式结束标识符 */ const INTERPOLATION_EXPRESSION_END = ['}', ']', '>', ')']; /** * 解析字符串插值模板 * @example parseInterpolationImpl("hello $name! I am $age years old. I'll be ${age+1} next year. Give me $$100.") * // { * // type: 'interpolation', * // templates: ['hello ', '! I am ', ' years old. I\'ll be ', ' next year. Give me $$100.'], * // values: ['name', 'age', 'age+1'] * // } */ function parseInterpolationImpl(template: string, start: number, length: number): InterpolationTemplate { const templates: string[] = []; const values: string[] = []; let currentTemplate = ''; let currentValue = ''; let expressionComplexDepth = 0; let status: ParserStatus = PARSER_STATUS_TEXT; let complexExpressionStartType = -1; const end = start + length - 1; for (let i = start; i <= end; i++) { if (status === PARSER_STATUS_TEXT) { const nextInterpolationChar = template.indexOf(INTERPOLATION_CHAR, i); if (nextInterpolationChar < 0 || nextInterpolationChar >= end) { // No more interpolation currentTemplate += template.slice(i, end + 1); break; } currentTemplate += template.slice(i, nextInterpolationChar); i = nextInterpolationChar; const nextChar = template.charAt(nextInterpolationChar + 1); complexExpressionStartType = INTERPOLATION_EXPRESSION_START.indexOf(nextChar); if (complexExpressionStartType >= 0) { // Start of complex expression templates.push(currentTemplate); currentTemplate = ''; status = PARSER_STATUS_EXPRESSION_COMPLEX; expressionComplexDepth = 1; i++; continue; } if (isIdentifierFirstChar(nextChar)) { // Start of simple expression templates.push(currentTemplate); currentTemplate = ''; currentValue = nextChar; status = PARSER_STATUS_EXPRESSION_SIMPLE; i++; continue; } // Not an expression currentTemplate += INTERPOLATION_CHAR; continue; } const char = template.charAt(i); if (status === PARSER_STATUS_EXPRESSION_SIMPLE) { if (isIdentifierChar(char)) { currentValue += char; continue; } // End of expression values.push(currentValue); currentValue = ''; status = PARSER_STATUS_TEXT; i--; continue; } if (status === PARSER_STATUS_EXPRESSION_COMPLEX) { if (char === INTERPOLATION_EXPRESSION_START[complexExpressionStartType]) { expressionComplexDepth++; } else if (char === INTERPOLATION_EXPRESSION_END[complexExpressionStartType]) { expressionComplexDepth--; if (expressionComplexDepth === 0) { // End of expression values.push(currentValue.trim()); currentValue = ''; status = PARSER_STATUS_TEXT; continue; } } currentValue += char; continue; } } if (status === PARSER_STATUS_TEXT) { templates.push(currentTemplate); } else if (status === PARSER_STATUS_EXPRESSION_SIMPLE) { values.push(currentValue); templates.push(''); } else { throw new Error('Unexpected end of input'); } return { type: 'interpolation', templates, values, }; } /** * 解析字符串插值模板 * @example parseInterpolation("hello $name! I am $age years old. I'll be ${age+1} next year. And $(2*age) after $<age> years.") * // { * // type: 'interpolation', * // templates: ['hello ', '! I am ', ' years old. I\'ll be ', ' next year. And ', ' after ', ' years.'], * // values: ['name', 'age', 'age+1', '2*age', 'age'] * // } */ export function parseInterpolation(template: string, start?: number, length?: number): InterpolationTemplate { start ??= 0; length ??= template.length - start; if (start < 0 || start > template.length) throw new Error('start out of range'); if (length < 0 || start + length > template.length) throw new Error('length out of range'); return parseInterpolationImpl(template, start, length); } /** * 解析字符串模板 * - 长度大于 1 * - 如果模板以 `=` 开头,则表示是一个公式 * - 如果模板以 `$` 开头,则表示是一个插值模板 * - 否则表示是一个普通字符串 */ export function parseTemplate(template: string): Template { if (!template) return ''; if (template.length <= 1) return template; if (template.startsWith('=')) { return { type: 'formula', value: template.slice(1), }; } if (template.startsWith('$')) { const result = parseInterpolationImpl(template, 1, template.length - 1); if (result.values.length === 0) return result.templates[0]!; return result; } return template; }