• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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