UNPKG

muz-doraemon

Version:

自主开发的UniApp组件——Жидзин(Zidjin)系列组件库。

428 lines (387 loc) 19.2 kB
/** 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;