1/* 2 * Copyright (c) 2021 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16/* 17 * Customize the compiled template code into a render function. There are some detailed rules to explain: 18 * 1. Convert all numeric strings to numbers, such as:"32px" convert to number 32; "11" convert to number 11; 19 * 2. Convert all hex color to decimal number; 20 * 3. Convert all boolean strings to boolean type; 21 * 4. compile events, the value of event Cannot be enclosed in double quotes; 22 */ 23const { isFunction, isObject, isUndefined } = require('./lite-utils'); 24const { 25 SPECIAL_STYLE, 26 REGEXP_NUMBER_PX, 27 REGEXP_COLOR, 28 REGEXP_UNIT, 29 REGXP_QUOTES, 30 REGXP_LANGUAGE, 31 REGXP_LANGUAGE_KEY, 32 REGXP_FUNC_RETURN, 33} = require('./lite-enum'); 34const parameterArray = []; 35let parameter1 = ''; 36let parameter2 = ''; 37let i18nMapping = {}; 38const ATTRBUTES = 'attrs'; 39const EVENTS_ON_FUNC = 'on'; 40const KEY = 'key'; 41const AST_KEY = { 42 ATTR: 'attr', 43 CLASSLIST: 'classList', 44 STYLE: 'style', 45 EVENTS: 'events', 46 TYPE: 'type', 47 CHILDREN: 'children', 48 KEY: 'key', 49}; 50const EVENT_KEY = [ 51 'onBubbleEvents', 52 'catchBubbleEvents', 53 'onCaptureEvents', 54 'catchCaptureEvents', 55]; 56 57const optionRules = { 58 [AST_KEY.ATTR]: function(dataContent, node, key) { 59 if (Object.keys(node.attr).length !== 0) { 60 dataContent += `'${ATTRBUTES}' : ${transformProps(node.attr)},`; 61 } 62 return dataContent; 63 }, 64 [AST_KEY.CLASSLIST]: function(dataContent, node, key) { 65 dataContent += sortClass(node[key]); 66 return dataContent; 67 }, 68 [AST_KEY.STYLE]: function(dataContent, node, key) { 69 dataContent += sortStyle(node[key]); 70 return dataContent; 71 }, 72 [AST_KEY.EVENTS]: function(dataContent, node, key) { 73 dataContent += `'${EVENTS_ON_FUNC}' : ${transformEvents(node.events)},`; 74 return dataContent; 75 }, 76 [AST_KEY.KEY]: function(dataContent, node, key) { 77 dataContent += `'${KEY}' : ${node.key},`; 78 return dataContent; 79 }, 80}; 81 82const styleRules = [ 83 { 84 match: function(key, value) { 85 return key === SPECIAL_STYLE.ANIMATION_DELAY || key === SPECIAL_STYLE.ANIMATION_DURATION; 86 }, 87 action: function(staticStyle, key, value) { 88 staticStyle += `'${key}' : ${JSON.stringify(value)},`; 89 return staticStyle; 90 }, 91 }, 92 { 93 match: function(key, value) { 94 return key === SPECIAL_STYLE.ANIMATION_ITERATION_COUNT; 95 }, 96 action: function(staticStyle, key, value) { 97 if (value === -1) { 98 value = 'infinite'; 99 } 100 staticStyle += `'${key}' : ${JSON.stringify(value)},`; 101 return staticStyle; 102 }, 103 }, 104 { 105 match: function(key, value) { 106 return key === SPECIAL_STYLE.BACKGROUND_IMAGE; 107 }, 108 action: function(staticStyle, key, value) { 109 staticStyle += `'${key}' : ${checkType(value.replace(REGXP_QUOTES, ''))},`; 110 return staticStyle; 111 }, 112 }, 113 { 114 match: function(key, value) { 115 return true; 116 }, 117 action: function(staticStyle, key, value) { 118 staticStyle += `'${key}' : ${checkType(value)},`; 119 return staticStyle; 120 }, 121 }, 122]; 123 124(function() { 125 EVENT_KEY.map(function(event) { 126 optionRules[event] = function(dataContent, node, key) { 127 dataContent += `'${event}' : ${transformEvents(node[event])},`; 128 return dataContent; 129 }; 130 }); 131})(); 132 133/** 134 * Compile the ast object into an executable function. 135 * @param {Object} value template object compiled ast. 136 * @return {String} template string. 137 */ 138function transformTemplate(value) { 139 const ast = Function(`return ${value}`)(); 140 let template = isObject(ast) ? transformNode(ast) : `_c('div')`; 141 template = template.replace(/,$/, ''); 142 const cachedI18nPushStrings = Object.values(i18nMapping); 143 const I18nContect = cachedI18nPushStrings.length === 0 ? '' : ` var i18ns = []; ${cachedI18nPushStrings.join(';')};`; 144 const res = `function (vm) { var _vm = vm || this;${I18nContect} return ${template} }`; 145 i18nMapping = {}; 146 return res; 147} 148 149/** 150 * Divided into if\for\ordinary three kinds node transform. 151 * @param {Object} node template object compiled ast. 152 * @return {String} template string. 153 */ 154function transformNode(node) { 155 if (node.repeat && !node.forCompiled) { 156 return transformFor(node); 157 } else if (node.shown && !node.ifCompiled) { 158 return transformIf(node); 159 } else { 160 return transformNodeDetail(node); 161 } 162} 163 164/** 165 * Divide node into type/child/data three parts and compiled separately. 166 * @param {Object} node ordinary node. 167 * @return {String} ordinary node string. 168 */ 169function transformNodeDetail(node) { 170 const type = node.type; 171 const options = transformOptions(node); 172 const children = transformChildren(node); 173 const render = `_c('${type}'${options ? `, ${options} ` : ``}${children ? `, ${children} ` : ``}),`; 174 return render; 175} 176 177/** 178 * Compile node all key-value data. 179 * @param {Object} node ordinary node. 180 * @return {String} ordinary node attributes string. 181 */ 182function transformOptions(node) { 183 let dataContent = ''; 184 parameter2 = parameterArray[parameterArray.length - 1]; 185 if (node.attr && node.attr.tid && parameter2 !== '') { 186 node['key'] = `${parameter2}.${node.attr['tid']}`; 187 delete node.attr.tid; 188 } 189 for (const key of Object.keys(node)) { 190 if (key !== AST_KEY.TYPE && key !== AST_KEY.CHILDREN) { 191 if (optionRules[key]) { 192 dataContent = optionRules[key](dataContent, node, key); 193 } 194 } 195 } 196 if (dataContent === '') { 197 return null; 198 } 199 dataContent = '{' + dataContent.replace(/,$/, '') + '}'; 200 return dataContent; 201} 202 203/** 204 * Compile node classList, divided into dynamicClass and staticClass. 205 * @param {Function|String} classList the object of class list. 206 * @return {String} class list string. 207 */ 208function sortClass(classList) { 209 let classStr = ''; 210 const DYNAMIC_CLASS = 'dynamicClass'; 211 const STATIC_CLASS = 'staticClass'; 212 const value = checkType(classList); 213 // Divid into two parts dynamicClass and staticClass depending on whether it is a method type 214 if ((isFunction(classList) || isUndefined(classList)) && !REGXP_LANGUAGE.test(classList)) { 215 classStr += `'${DYNAMIC_CLASS}' : ${formatForFunc(value)},`; 216 } else { 217 classStr += `'${STATIC_CLASS}' : ${value},`; 218 } 219 return classStr; 220} 221 222/** 223 * Compile node style, divided into staticStyle and staticStyle. 224 * @param {Object} props the object of style. 225 * @return {String} style list string. 226 */ 227function sortStyle(props) { 228 let staticStyle = ''; 229 let dynamicStyle = ''; 230 const STASTIC_STYLE = 'staticStyle'; 231 const DYNAMIC_STYLE = 'dynamicStyle'; 232 for (const key of Object.keys(props)) { 233 const value = props[key]; 234 // Divid into two parts staticStyle and dynamicStyle depending on whether it is a method type 235 if (isFunction(value) && !REGXP_LANGUAGE.test(value)) { 236 dynamicStyle += `'${key}' : ${formatForFunc(value)},`; 237 } else { 238 for (let i = 0; i < styleRules.length; i++) { 239 if (styleRules[i].match(key, value)) { 240 staticStyle = styleRules[i].action(staticStyle, key, value); 241 break; 242 } 243 } 244 } 245 } 246 if (staticStyle !== '') { 247 staticStyle = `'${STASTIC_STYLE}' : {${staticStyle.replace(/,$/, '')}}, `; 248 } 249 if (dynamicStyle !== '') { 250 dynamicStyle = `'${DYNAMIC_STYLE}' :{${dynamicStyle.replace(/,$/, '')}},`; 251 } 252 return staticStyle + dynamicStyle; 253} 254 255/** 256 * general method ,judge type and compile, There are some special rules defined here, 257 * such as:"32px" convert to number 32; "11" convert to number 11; "#ffffff" convert to number 16777215. 258 * @param {*} value Value to be formatted. 259 * @return {*} Formatted value. 260 */ 261function checkType(value) { 262 if (isFunction(value) || isUndefined(value)) { 263 return formatForFunc(value); 264 // Use recursive conversion of object type values 265 } else if (isObject(value)) { 266 return transformProps(value); 267 // Convert all numeric strings to numbers 268 } else if (!isNaN(Number(value))) { 269 return Number(value); 270 } else if (REGEXP_NUMBER_PX.test(value)) { 271 return parseInt(value.replace(REGEXP_UNIT, ''), 10); 272 // Convert all colors to numbers 273 } else if (REGEXP_COLOR.test(value)) { 274 return parseInt(value.slice(1), 16); 275 // Convert all boolean strings to boolean type 276 } else if (value === 'true') { 277 return true; 278 } else if (value === 'false') { 279 return false; 280 } else { 281 return JSON.stringify(value); 282 } 283} 284 285/** 286 * general method, compile data of object type, and compile node attributes. 287 * apart from The case where key is "value". 288 * @param {Object} props Value to be formatted. 289 * @return {String} Formatted value. 290 */ 291function transformProps(props) { 292 let propContent = ''; 293 const VALUE = 'value'; 294 for (const key of Object.keys(props)) { 295 const propValue = props[key]; 296 // value is used to display, except for method types, no conversion is required 297 if (key === VALUE) { 298 if (isFunction(propValue) || isUndefined(propValue)) { 299 propContent += `'${key}' : ${formatForFunc(props[key])},`; 300 } else { 301 propContent += `'${key}' : ${JSON.stringify(props[key])},`; 302 } 303 } else { 304 propContent += `'${key}' : ${checkType(props[key])},`; 305 } 306 } 307 propContent = `{${propContent.replace(/,$/, '')}}`; 308 return propContent; 309} 310 311/** 312 * compile events, divided into two types of conversion methods and string types. 313 * @param {Object} props Value of events to be formatted. 314 * @return {String} Formatted Value of events. 315 */ 316function transformEvents(props) { 317 let eventContent = ''; 318 for (const key of Object.keys(props)) { 319 if (isFunction(props[key])) { 320 /** 321 * Method contains parameters and will be compiled into a method. 322 * such as: onclick = "test(value)" => "click": function(evt){this.test(this.value, evt)} 323 */ 324 eventContent += `'${key}' : ${formatForFunc(props[key])},`; 325 } else { 326 /** 327 * The method contains no parameters and will be compiled into a string. 328 * such as: onclick = "test" => "click": "test" 329 */ 330 eventContent += `'${key}' : ${formatForString(props[key])},`; 331 } 332 } 333 eventContent = `{${eventContent.replace(/,$/, '')}}`; 334 return eventContent; 335} 336 337/** 338 * Compile events of type string, add `_vm.` in front of ordinary events, such as: onclick="test" => "click": "_vm.test" 339 * do nothing for the data in the `for` loop, such as: onclick="{{$item.click}}" => "click": "$item.click" 340 * @param {Object} value string type of events to be formatted. 341 * @return {String} Formatted Value of events. 342 */ 343function formatForString(value) { 344 let forCompiled = false; 345 for (const parameter of parameterArray) { 346 // '$' Needs to be escaped in regular expressions. The parameter in the for instruction may be '$idx' and '$item' 347 const escape = parameter.charAt(0) === '$' ? '\\' : ''; 348 const itRE = new RegExp(escape + parameter); 349 // Match the variable name in the stack, to determine whether it is ordinary event or an event in the for 350 if (itRE.test(value)) { 351 forCompiled = true; 352 break; 353 } 354 } 355 const res = forCompiled ? value : '_vm.' + value; 356 return res; 357} 358 359/** 360 * compile "for" direct, return the _l function. 361 * @param {Object} node node object with "for" directive. 362 * @return {String} string of _l function. 363 */ 364function transformFor(node) { 365 let exp = node.repeat.exp ? node.repeat.exp : node.repeat; 366 parameterArray.push(node.repeat.key ? node.repeat.key : '$idx'); 367 parameterArray.push(node.repeat.value ? node.repeat.value : '$item'); 368 node.forCompiled = true; 369 // Set context and stack to convert "this.index" to "index" in the for function 370 exp = formatForFunc(exp); 371 const children = transformNode(node).replace(/,$/, ''); 372 parameter2 = parameterArray[parameterArray.length - 1]; 373 parameter1 = parameterArray[parameterArray.length - 2]; 374 const comma = parameter1 !== '' && parameter2 !== '' ? ',' : ''; 375 parameterArray.pop(); 376 parameterArray.pop(); 377 return '_l' + '((' + exp + '),' + 'function(' + parameter2 + comma + parameter1 + '){' + 'return ' + children + '}),'; 378} 379 380/** 381 * compile "if" direct, return the _i function. 382 * @param {Object} node node object with "if" directive. 383 * @return {String} string of _i function. 384 */ 385function transformIf(node) { 386 node.ifCompiled = true; 387 const children = transformNode(node).replace(/,$/, ''); 388 return '_i' + '((' + formatForFunc(node.shown) + '),' + 'function(){return ' + children + '}),'; 389} 390 391/** 392 * convert "this.index" to "index" in the for function. if the element is not in the for function, 393 * there will be no value in parameterArray 394 * @param {Object} value Value of function to be formatted. 395 * @return {String} Formatted Value of events. 396 */ 397function formatForFunc(value) { 398 let func = value.toString(); 399 for (const parameter of parameterArray) { 400 // '$' Needs to be escaped in regular expressions. The parameter in the for instruction may be '$idx' and '$item' 401 const escape = parameter.charAt(0) === '$' ? '\\' : ''; 402 const itRE = new RegExp('this.' + escape + parameter + '\\b', 'g'); 403 /** 404 * If it is a parameter in the for instruction, remove 'this'. 405 * such as: {"value": function () {return this.$item.name}} => {"value": function () {return $item.name}} 406 */ 407 func = func.replace(itRE, parameter); 408 } 409 // Replace all "this" to "_vm" 410 func = func.replace(/this\./g, '_vm.'); 411 // Internationalization performance optimization 412 func = cacheI18nTranslation(func); 413 return func; 414} 415 416/** 417 * There is only one $t internationalizations in the processing function. 418 * @param {String} i18nExpression match the value of $t in the internationalization method. 419 * @param {Array} cachedI18nExpressions all keys of i18n Mapping object. 420 * @return {String} treated internationalization method. 421 */ 422function handleLangSingle(i18nExpression, cachedI18nExpressions) { 423 let res = ''; 424 if (cachedI18nExpressions.includes(i18nExpression)) { 425 // The i18nExpression already exists in cachedI18nExpressions 426 const cachedI18nPushStrings = Object.values(i18nMapping); 427 const cachedI18nPushString = i18nMapping[i18nExpression]; 428 res = `i18ns[${cachedI18nPushStrings.lastIndexOf(cachedI18nPushString)}]`; 429 } else { 430 // The i18nExpression does not exist in cachedI18nExpressions 431 i18nMapping[ 432 i18nExpression 433 ] = `i18ns.push( ${i18nExpression} )`; 434 res = `i18ns[${Object.keys(i18nMapping).length - 1}]`; 435 } 436 return res; 437} 438 439/** 440 * There are multiple $t internationalizations in the processing function.. 441 * @param {Array} i18nExpressions match the value of $t in the internationalization method. 442 * @param {Array} cachedI18nExpressions all keys of i18n Mapping object. 443 * @param {String} funcExpression return value in the internationalization method. 444 * @param {String} func internationalization method. 445 * @return {String} treated internationalization method. 446 */ 447function handleLangMulti(i18nExpressions, cachedI18nExpressions, funcExpression, func) { 448 let res = func; 449 // The funcExpression already exists in cachedI18nExpressions 450 if (cachedI18nExpressions.includes(funcExpression)) { 451 const cachedI18nPushStrings = Object.values(i18nMapping); 452 const cachedI18nPushString = i18nMapping[funcExpression]; 453 res = `i18ns[${cachedI18nPushStrings.lastIndexOf(cachedI18nPushString)}]`; 454 // The funcExpression does not exist in cachedI18nExpressions 455 } else { 456 for (let i = 0; i < i18nExpressions.length; i++) { 457 const i18nExpression = i18nExpressions[i]; 458 // The i18nExpression already exists in cachedI18nExpressions 459 if (cachedI18nExpressions.includes(i18nExpression)) { 460 const cachedI18nPushStrings = Object.values(i18nMapping); 461 const cachedI18nPushString = i18nMapping[i18nExpression]; 462 res = res.replace( 463 i18nExpression, 464 `i18ns[${cachedI18nPushStrings.lastIndexOf(cachedI18nPushString)}]`, 465 ); 466 // The i18nExpression does not exists in cachedI18nExpressions 467 } else { 468 i18nMapping[ 469 i18nExpression 470 ] = `i18ns.push( ${i18nExpression} )`; 471 res = res.replace( 472 i18nExpression, 473 `i18ns[${Object.keys(i18nMapping).length - 1}]`, 474 ); 475 } 476 // For the last $t, replace the func value 477 if (i === i18nExpressions.length - 1 && !res.includes('_vm.')) { 478 const funcReturnMatches = REGXP_FUNC_RETURN.exec(res); 479 REGXP_FUNC_RETURN.lastIndex = 0; 480 const funcReturnMatch = funcReturnMatches[1].trim(); 481 i18nMapping[funcExpression] = `i18ns.push( ${funcReturnMatch} )`; 482 res = `i18ns[${Object.keys(i18nMapping).length - 1}]`; 483 } 484 } 485 } 486 return res; 487} 488 489/** 490 * Internationalization performance optimization operation. 491 * @param {String} func string for globalization method. 492 * @return {String} Whether to use the parameters in 'for' instruction. 493 */ 494function cacheI18nTranslation(func) { 495 if (!REGXP_LANGUAGE.test(func)) { 496 return func; 497 } 498 const i18nExpressions = func.match(REGXP_LANGUAGE_KEY); 499 const cachedI18nExpressions = Object.keys(i18nMapping); 500 const funcExpressions = REGXP_FUNC_RETURN.exec(func); 501 REGXP_FUNC_RETURN.lastIndex = 0; 502 const funcExpression = funcExpressions[1].trim(); 503 // If the 'for' parameter is used in $t, nothing will be done 504 if (isUseForInstrucParam(funcExpression)) { 505 return func; 506 } 507 // There is only one $t internationalization in the function. 508 // such as:function () { return ( _vm.$t('i18n.text.value1')) } 509 if (i18nExpressions.length === 1 && i18nExpressions[0] === funcExpression) { 510 const i18nExpression = i18nExpressions[0]; 511 func = handleLangSingle(i18nExpression, cachedI18nExpressions); 512 // There are multiple $t internationalization in the function. 513 // such as: function () { return ( _vm.$t('i18n.text.value1') - _vm.$t('i18n.text.value2')); } 514 } else { 515 func = handleLangMulti(i18nExpressions, cachedI18nExpressions, funcExpression, func); 516 } 517 return func; 518} 519 520/** 521 * Determine whether the parameters in the 'for' instruction are used in the globalization method. 522 * @param {String} value string for globalization method. 523 * @return {Blooean} Whether to use the parameters in 'for' instruction. 524 */ 525function isUseForInstrucParam(value) { 526 let isUseParam = false; 527 for (const parameter of parameterArray) { 528 const escape = parameter.charAt(0) === '$' ? '\\' : ''; 529 const itRE = new RegExp(escape + parameter); 530 // Match the variable name in the stack, to determine whether it is ordinary event or an event in the for 531 if (itRE.test(value)) { 532 isUseParam = true; 533 break; 534 } 535 } 536 return isUseParam; 537} 538 539/** 540 * compile node children. 541 * @param {Object} node ordinary node. 542 * @return {String} ordinary node children string. 543 */ 544function transformChildren(node) { 545 const children = node.children; 546 if (!children) { 547 return null; 548 } 549 let childContent = ''; 550 for (let i = 0; i < children.length; i++) { 551 childContent += transformNode(children[i]); 552 } 553 childContent = '[' + childContent.replace(/,$/, '') + ']'; 554 return childContent; 555} 556 557exports.transformTemplate = transformTemplate; 558