muz-doraemon
Version:
自主开发的UniApp组件——Жидзин(Zidjin)系列组件库。
428 lines (387 loc) • 19.2 kB
JavaScript
/** x-format-string V1.0 自动格式化字符串,支持双花括号 20230213 by Sieyoo, 赵向明 */
import XTakeAt from './x-take-at.js';
const XTemplateString = function () {};
/**
* function XTemplateString::format 对模板字符串进行编译,格式化为结果字符串
* @param {object} item 给定需要格式化的Key-Value对象。
* @param {string} text 要格式化的字符串文本
* @desc
* @return {string} 格式化完成后字符串
* @example
* 模板语法糖:
* 1、基础版——直接引用值。语法:{{key}}
* 例子:
* param={category_id: 35, category_value: "杭州西湖区"};
* str="类别ID:{{category_id}},类别值:{{category_value}}。";
* XTemplateString.format(param, str);
* 结果 ==> "类别ID:35,类别值:杭州西湖区。"
* 2、高级版——管道选项值。即指定列表对象、标签键、值键,找到引用值。语法:{{key:key|options[labelKey]}}
* 例子:
* param={category_id: 35, category_list: [{value: 27, label: "杭州上城区"}, {value: 35, label: "杭州西湖区"}]};
* str="类别:{{ value: category_id | category_list[label] }}"
* XTemplateString.format(param, str);
* 结果 ==> "类别:杭州西湖区。"
* 3、高级版——强制转换值。即指定来源键、目标键,生成数组对象。语法:{{(Array<targetKey:sourceKey>)key<splitFlag>}}
* 例子:
* param={photo_list: [{link: '/images/a.jpg', label: "杭州上城区"}, {link: '/images/b.jpg', label: "杭州西湖区"}]};
* str="{{ (Array< url: link, text: label >)photo_list }}"
* str2="{{ (Array< :link >)photo_list }}"
* XTemplateString.format(param, str);
* 结果 ==> "[{url: '/images/a.jpg', text: "杭州上城区"}, {url: '/images/b.jpg', text: "杭州西湖区"}]"
* XTemplateString.format(param, str2);
* 结果 ==> "['/images/a.jpg', '/images/b.jpg']"
*/
XTemplateString.format = function (item, text) {
// console.log("XTemplateString.format:", item, text)
if (!text) {
return '';
}
// typeof(formData[it.field]) === 'object' ? JSON.stringify(formData[it.field]) : formData[it.field]
// 暂时弃用,小程序不支持eval语法
// let expression = text.replace(/(\{\{(.*?)\}\})/g, "$${item.$2}");
// let result = eval('`'+expression+'`').replaceAll('"null"', '""');
let result = text;
if (typeof text === 'object') {
result = JSON.stringify(text);
}
// const re = /(\{\{([\w\s\[\]\(\)\\<>.,;#@$%^&*~|!?:='"`-]*?)\}\})/i; // (\{\{(.*?)\}\})";
const re = /(\{\{((?:(?!\{\{).)*?)\}\})/i; // 查找括号对
const reNotString = /("\{\{(?:(?!\{\{).)*?\}\}")|(\{\{(?:(?!\{\{).)*?\}\})/i; // 需要替换的非字符串类型
// const reNotString = /("\{\{(?:(?!\{\{).)*?\}\}")/i; // 需要替换的非字符串类型
while (re.test(result)) {
// console.log("XTemplateString re.exec:", re.exec(result))
let key = re.exec(result)[2];
key = key.replaceAll('\\"', '"'); // 双引号兼容
let value = XTakeAt.getValue(item, key?.trim(), ''); // 注意:如果key是高级模版语法,是匹配不出value值的。
const system = XTemplateString.matchSystem(item, key); // 系统级内置方法处理
const logic = XTemplateString.matchLogic(item, key); // 逻辑运算处理
const label = XTemplateString.matchOptions(item, key); // 管道选项处理
const force = XTemplateString.matchForceType(item, key); // 强制转换处理
if (typeof value === 'string') {
value = value.replace(/"/g, '\\"');
}
// console.log("XTemplateString.format k-v:", key, '=', {value: value}, '==>', label, 'logic:', logic, 'system:', system);
// 强制转换时要改变类型,所以字符串需要替换掉两头的""双引号
if (force) {
result = result.replace(reNotString, force);
} else if (logic || typeof logic === 'boolean' || logic === 0) {
result = result.replace(typeof logic === 'string' ? re : reNotString, typeof logic === 'boolean' ? String(logic) : logic);
} else if (system || typeof system === 'boolean' || system === 0) {
result = result.replace(typeof system === 'string' ? re : reNotString, typeof system === 'boolean' ? String(system) : system);
// result = result.replace(re, system);
} else {
// const val = label || value;
// result = result.replace(typeof val === 'string' ? re : reNotString, typeof val === 'boolean' ? String(val) : val);
result = result.replace(re, label || value);
}
// console.log("XTemplateString end:", result);
}
// 因为JSON反序列化不支持\r\n,所以只能强制替换掉
if (typeof result === 'string') {
result = result
// .replace(/\\/g, '\\\\') //这个千万不能写,会导致\"都失效的
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
if (typeof text === 'object') {
// 注:如果json中混入html语言或\n换行符等异物,会导致反解析失败
try {
result = JSON.parse(result);
} catch (err) {
console.warn('【模块字符串】转换JSON时失败!', err, 'result =>', result, { result });
result = text;
}
}
// console.log("XTemplateString.format end:", result)
return result;
};
/**
* 系统级方法使用
* @example {
例如:$storage.userInfo.user_id;
$storage => 调用缓存
userInfo => 内存key 若不许传惨则直接返回 userInfo 值
user_id => 缓存内具体值 若继续传入则返回 user_id 值
例如:$system.os.osName;
$system => 使用uni.getSystemInfoSync
os => 返回值名称 若不许传惨则直接返回 os 值
osName => 缓存内具体值 若继续传入则返回 osName 值
}
*/
XTemplateString.matchSystem = function (source, expression_string) {
if (!expression_string) {
return null;
}
// console.log(source, expression_string)
// 检查是否为系统级方法使用
const re = /^(\$storage|\$system)\.([_A-Za-z])([\w])?.*/;
// 检查是否
if (!re.test(expression_string)) return null;
let result = expression_string;
// console.log(result)
const sre = /^\$storage.*/;
if (sre.test(result)) {
const split_string = expression_string.split('.');
if (split_string.length < 2) {
console.warn('【模块字符串】转换JSON时失败!未提供内存缓存后续参数', expression_string);
return null;
}
const tag = split_string.shift(); // $storage.
const key = split_string.shift(); // $userInfo
const target = split_string.join('.');
let value = uni.getStorageSync(key);
// console.log("XTemplateString.matchSystem:", tag, key, target, value)
if (!value) {
return null;
}
if (target) {
return XTakeAt.getValue(value, target);
} else {
return value;
}
}
const sysre = /^\$system.*/;
if (sysre.test(result)) {
const split_string = expression_string.split('.');
const tag = split_string.shift(); // $system.
const target = split_string.join('.'); // os.osName
let SystemInfo = uni.getSystemInfoSync();
return XTakeAt.getValue(value, target);
}
return result;
};
// 逻辑运算值。
XTemplateString.matchLogic = function (source, expression_string) {
// console.log("XTemplateString.matchLogic:", source, expression_string);
if (!expression_string) {
return null;
}
// 检查是否为三元表达式
// const re = /(^([\s*=\|\&\w."']+)\s*\?\s*([\s\w?:.=|&"']+)\s*:\s*([\w."']+))([\s=?:&|()]?$)/;
const re = /(^([\s*=\|\&\w.\-_"'\u4e00-\u9fa5]+)\s*\?\s*([\s\w?:.\-_=|&"'\u4e00-\u9fa5]+)\s*:\s*([\s\w:.\-_=|&"']+))([\s=?:&|()]?$)/;
// console.log("XTemplateString re:", !re.test(expression_string))
// 检查是否
if (!re.test(expression_string)) return null;
let result = expression_string;
// console.log(result,'matchLogic')
// 生成node节点
function createdNode(condition, calculate = null, next = null) {
return { condition, calculate, next };
}
// 生成节点树
function createdTree(text) {
const conditionIndex = text.indexOf('?');
const condition = text.substring(0, conditionIndex).trim();
const exprIText = text.substring(conditionIndex + 1).trim();
// { '==': 7, '&&': 11, '||': 12,}
let leftResult, exprIIndex;
if (exprIText.indexOf('(') !== -1) {
const startIndex = exprIText.indexOf('(');
const endIndex = exprIText.indexOf(')');
leftResult = exprIText.substring(startIndex + 1, endIndex).trim();
exprIIndex = endIndex + 2;
} else {
exprIIndex = exprIText.indexOf(':');
leftResult = exprIText.substring(0, exprIIndex).trim();
}
const rightResult = exprIText.substring(exprIIndex + 1).trim();
const node = createdNode(condition, leftResult, rightResult);
if (leftResult.indexOf('?') !== -1) {
node.calculate = createdTree(leftResult, {});
}
if (rightResult.indexOf('?') !== -1) {
node.next = createdTree(rightResult, {});
}
return node;
}
const tree = createdTree(expression_string);
// console.log(tree)
// 逻辑操作运算
function logicalOperation(next) {
const condition = next.condition;
if (equivalenceOperation(condition)) {
if (typeof next.calculate === 'string') {
result = processValue(next.calculate);
}
if (typeof next.calculate === 'object') {
logicalOperation(next.calculate);
}
} else {
if (typeof next.next === 'string') {
result = processValue(next.next);
}
if (typeof next.next === 'object') {
logicalOperation(next.next);
}
}
}
// 等值运算
function equivalenceOperation(condition) {
if (condition.indexOf('==') !== -1) {
if (condition.indexOf('||') !== -1) {
const orGroup = condition.split('||').map(text => text.trim());
const orGroupResult = orGroup.filter(value => {
const [left, right] = value.split('==').map(text => text.trim());
return processValue(left) == processValue(right);
});
return orGroupResult.length !== 0;
}
if (condition.indexOf('&&') !== -1) {
return condition
.split('&&')
.map(text => text.trim())
.reduce((prev, curr) => {
const [left, right] = curr.split('==').map(text => text.trim());
return prev && processValue(left) == processValue(right);
}, true);
}
const [left, right] = condition.split('==').map(text => text.trim());
return processValue(left) == processValue(right);
}
if (condition.indexOf('&&') !== -1 && condition.indexOf('==') === -1) {
return condition
.split('&&')
.map(text => text.trim())
.reduce((prev, curr) => prev && processValue(curr), true);
}
if (condition.indexOf('||') !== -1 && condition.indexOf('==') === -1) {
const orGroup = condition.split('||').map(text => text.trim());
const orGroupResult = orGroup.filter(value => processValue(value));
return orGroupResult.length !== 0;
}
return XTakeAt.getValue(source, condition, false);
}
// 字符串转值
function processValue(value) {
try {
if (['false', 'true'].includes(value.toLocaleLowerCase())) {
const booleanValue = value.toLocaleLowerCase();
return booleanValue === 'true';
} else if (/^[+-]?\d+(\.\d+)?$/.test(value)) {
return +value;
} else if (/(\"|\').*(\"|\')/.test(value)) {
return value.replaceAll(/(\"|\')/g, '');
} else {
return XTakeAt.getValue(source, value);
}
} catch {
return void 0;
}
}
logicalOperation(tree);
return result;
};
// 管道选项值。作用:"value:category_id|category_list[label]" ==> "杭州西湖区"
XTemplateString.matchOptions = function (item, expression_string) {
// console.log("XTemplateString.matchOptions:", expression_string, item)
if (!expression_string) {
return null;
}
const re = /([_.`'"\s\w\-()\[\]\u4e00-\u9fa5]*?)\s*:?\s*([_.`'"\s\w\-()\[\]\u4e00-\u9fa5]*?)\s*\|\s*([_.`'"\s\w\-()\[\]\u4e00-\u9fa5]*?)\[\s*([_.`'"\s\w\-()\[\]\u4e00-\u9fa5]*?)\s*\]/;
// 检查是否
if (!re.test(expression_string)) return null;
// console.log("XTemplateString.matchOptions re:", re.exec(expression_string));
const match = re.exec(expression_string);
const itemKey = match[2];
const valueKey = match[1] || itemKey;
const optionsKey = match[3];
const labelKey = match[4];
const options = XTakeAt.getValue(item, optionsKey, ''); // item[optionsKey];
const value = XTakeAt.getValue(item, itemKey, ''); // item[itemKey];
if (!options || !labelKey || !valueKey) {
return value;
}
// 查找到当前匹配的对象,
let current = options.find(it => it[valueKey] == value);
let label = XTakeAt.getValue(current, labelKey, ''); // current && current[labelKey];
// console.log("XTemplateString.matchOptions label 2:", {key: label});
if (!label) {
return value;
}
return label;
};
// 强制转换值。作用:(Array<url: url, text: uid>)files ==> [{"url": "/images/xx.jpg", "text": "标题"}]
XTemplateString.matchForceType = function (item, expression_string) {
// console.log("XTemplateString.matchForceType:", expression_string, item)
if (!expression_string) {
return null;
}
// const re = (/\(\s*([\w]*?)\s*<\s*([_:,\s\w]*?)\s*>\s*\)\s*([_\w]*?)\s*$/); // 不支持<,>分隔符
// const re = (/\(\s*([\w]*?)\s*<\s*([_:,\s\w]*?)\s*>\s*\)\s*([_\w]*)\s*(?:\<(.*?)\>)?\s*/);
// const re = (/\(\s*([\w]*?)\s*<\s*([_:,.`'"\s\w\[\]]*?)\s*>\s*\)\s*([_\w]*)\s*(?:\<(.*?)\>)?\s*/);
// const re = (/\(\s*([\w]*?)\s*<\s*([_:,.`'"\s\w\[\]]*?)\s*>\s*\)\s*([_:,.`'"\s\w\[\]]*)\s*(?:\<(.*?)\>)?\s*/);
const re = /\(\s*([\w]*?)\s*<\s*([_:,.`'"\s\w\-()\[\]\u4e00-\u9fa5。?!,、;:“ ” ‘ ’()《》【】~]*?)\s*>\s*\)\s*([_:,.`'"\s\w\[\]]*)\s*(?:\<(.*?)\>)?\s*/;
// const re = (/\(\s*([\w]*?)\s*<\s*(.*?)\s*>\s*\)\s*([_:,.`'"\s\w\[\]]*)\s*(?:\<(.*?)\>)?\s*/);
const reString = /^\s*["'`]([\w\s-]*?)["'`]\s*$/;
// 检查是否
if (!re.test(expression_string)) return null;
// console.log("XTemplateString.matchForceType re:", re.exec(expression_string));
const match = re.exec(expression_string);
const type = match[1];
const pairs = match[2];
const key = match[3];
const splitFlag = match[4];
// console.log("matchForceType type:(", type, ") pairs:(", pairs, ") key:(", key, ")");
const value = XTakeAt.getValue(item, key?.trim(), '');
// console.log("matchForceType value:", value)
// 边界检查:如果列表值为空,返回空列表!
if (!value || !value.length || !value[0]) {
return '[]';
}
// 边界检查:如果匹配不完整,退出!
if (!type || type !== 'Array' || !key) {
return (typeof value === 'object' && JSON.stringify(value)) || value;
}
// 生成转换用的键值对。
let pairList = pairs.split(',').map(it => {
// console.log("matchForceType it:", it)
let kv = it.split(':');
return {
targetKey: kv.length >= 1 ? kv[0].trim() : '',
sourceKey: kv.length >= 2 ? kv[1].trim() : kv[0] || '', // 功能细节:如果没有传sourceKey,则代表来源键和目标键同名!
};
});
// console.log("matchForceType pairList:", pairList);
let sourceValue = value;
// 边界检查:如为字符串,逗号分隔为数组
if (typeof sourceValue === 'string') {
sourceValue = sourceValue.split(splitFlag || ';'); // 问:能不能指定分隔符?by Sieyoo
}
// 边界检查:如果不是数组,退出!
if (!Array.isArray(sourceValue)) {
return (typeof value === 'object' && JSON.stringify(value)) || value;
}
// console.log("matchForceType sourceValue:", sourceValue);
let result = sourceValue.map(cur => {
// console.log("matchForceType cur:", cur, );
// 如果没有目标键,则返回一维数组,即数组内是字符串
if (pairList.length && !pairList[0].targetKey) {
return XTakeAt.getValue(cur, pairList[0].sourceKey, cur || '');
// return cur[pairList[0].sourceKey] || cur || '';
}
// 生成新的键值对
let newObj = {};
// 如果来源值是一维(字符串)数组,旧功能:则仅返回第一组键值对(可解释为非贪婪模式?)
// 如果来源值是一维(字符串)数组,新功能:
// console.log("cur:", typeof cur)
if (pairList.length && typeof cur === 'string') {
// return {[pairList[0].sourceKey]: cur};
pairList.forEach(pp => {
// 通过三元表达式判断来源的值还是个字符串,
newObj[pp.targetKey] = reString.test(pp.sourceKey) ? reString.exec(pp.sourceKey)?.[1] : cur;
});
// console.log("newObj:",newObj)
return newObj;
}
pairList.forEach(pp => {
// console.log("matchForceType pairList:", pp.targetKey, pp.sourceKey, XTakeAt.getValue(pp, pp.sourceKey, ''));
// newObj[pp.targetKey] = cur[pp.sourceKey] || '';
newObj[pp.targetKey] = pp.sourceKey === 'true' ? true : pp.sourceKey === 'false' ? false : reString.test(pp.sourceKey) ? reString.exec(pp.sourceKey)?.[1] : XTakeAt.getValue(cur, pp.sourceKey, '');
});
return newObj;
});
// console.log("matchForceType result:", result)
return JSON.stringify(result);
};
export default XTemplateString;