@cloudpss/template
Version:
String and object template engine for Node.js and the browser.
193 lines (179 loc) • 6.77 kB
text/typescript
/** 公式模板 */
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;
}