• 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
16const componentValidator = require('./component_validator')
17const parse5 = require('../parse/index')
18const path = require('path')
19let compileResult
20const EVENT_START_REGEXP = /^(on:|on|@|grab:)/
21const REGEXP_TEXT = /^#/
22const REGEXP_DATA = /^data-/
23
24/**
25 * Compile html file into ast object.
26 * @param {String} source Hml file content.
27 * @param {Function} operate The second number.
28 * @param {String} filePath File resource path.
29 */
30function parse(source, operate, filePath) {
31  const relativePath = replaceAll(path.sep, '/', path.relative(
32    process.env.aceModuleRoot || process.cwd(), filePath)).replace(path.parse(filePath).ext, '');
33  const result = { jsonTemplate: {}, deps: [], log: [] }
34  compileResult = result
35  const template = hmlParse(source, {
36    sourceCodeLocationInfo: true,
37    componentValidator,
38    compileResult,
39  })
40  if (checkNullNode(template, operate) || checkRootNode(template, operate, relativePath)) {
41    return
42  }
43  const rootArray = template.childNodes.filter(function(currentValue) {
44    return currentValue.nodeName.indexOf('#') === -1
45  })
46  let rootIndex = 0
47  rootArray.forEach((root, index) => {
48    if(root.tagName !== 'element') {
49      rootIndex = index
50    }
51  })
52  generate(rootArray[rootIndex], filePath, undefined, relativePath)
53  operate(null, compileResult)
54}
55
56
57/**
58 * Use parse5 to get html conversion results.
59 * @param {String} code Hml file content.
60 * @param {Object} config parse5 parseFragment function config.
61 * @return {Object} html conversion results.
62 */
63function hmlParse(code, config) {
64  const res = parse5.parseFragment(code, config)
65  return res
66}
67
68/**
69 * Check if the hml file does not contain nodes.
70 * @param {String} template hml conversion results.
71 * @return {Boolean} Check result.
72 */
73function checkNullNode(template, operate) {
74  let errorFlag = false
75  if (template.childNodes.length === 0) {
76    compileResult.log.push({ reason: 'ERROR: parsing hml file failed' })
77    operate(null, compileResult)
78    errorFlag = true
79  }
80  return errorFlag
81}
82
83/**
84 * Check if the root node is legal.
85 * @param {String} template hml conversion results.
86 * @return {Boolean} Check result.
87 */
88function checkRootNode(template, operate, relativePath) {
89  let errorFlag = false
90  const rootArray = template.childNodes.filter(function(currentValue) {
91    return currentValue.nodeName.indexOf('#') === -1
92  })
93  let rootNum = 0
94  rootArray.forEach(root => {
95    if (root.nodeName !== 'element') {
96      rootNum ++
97    } else if (root.attrs && root.attrs.length) {
98      for (let index = 0; index < root.attrs.length; index++) {
99        const element = root.attrs[index];
100        if (element.name === 'name') {
101          componentValidator.elementNames[relativePath] = componentValidator.elementNames[relativePath] || []
102          componentValidator.elementNames[relativePath].push(element.value.toLowerCase())
103          break
104        }
105      }
106    }
107  })
108  if (rootNum !== 1) {
109    if (!rootNum) {
110      compileResult.log.push({
111        reason: 'ERROR: need a legal root node',
112        line: 1,
113        column: 1,
114      })
115    }
116    if (rootNum > 1) {
117      compileResult.log.push({
118        reason: 'ERROR: there can only be one root node',
119        line: 1,
120        column: 1,
121      })
122    }
123    operate(null, compileResult)
124    errorFlag = true
125  }
126  return errorFlag
127}
128/**
129 * Recursively parse every node.
130 * @param {Object} node Nodes that need to be resolved.
131 * @param {String} filePath File resource path.
132 * @param {Object} preNode the previous node.
133 */
134function generate(node, filePath, preNode, relativePath) {
135  componentValidator.validateTagName(node, compileResult, relativePath)
136  if (node.attrs && node.attrs.length !== 0) {
137    checkNodeAttrs(node, filePath, preNode, relativePath)
138  }
139  if (node.childNodes && node.childNodes.length !== 0) {
140    checkNodeChildren(node, filePath, relativePath)
141  }
142}
143
144/**
145 * Parse node properties.
146 * @param {Object} node Nodes that need to be resolved.
147 * @param {String} filePath File resource path.
148 * @param {Object} preNode the previous node.
149 */
150function checkNodeAttrs(node, filePath, preNode, relativePath) {
151  const attributes = node.attrs
152  const pos = {
153    line: node.sourceCodeLocation.startLine,
154    col: node.sourceCodeLocation.endLine,
155  }
156  attributes.forEach((attr, index) => {
157    const attrName = attr.name
158    const attrValue = attr.value
159    switch (attrName) {
160      case 'style':
161        componentValidator.validateStyle(attrValue, compileResult, pos, relativePath)
162        break
163      case 'class':
164        componentValidator.validateClass(attrValue, compileResult, pos, relativePath)
165        break
166      case 'id':
167        componentValidator.validateId(attrValue, compileResult, pos, relativePath)
168        break
169      case 'for':
170        checkAttrFor(node, attributes, pos);
171        componentValidator.validateFor(attrValue, compileResult, pos, relativePath)
172        break
173      case 'if':
174        componentValidator.validateIf(attrValue, compileResult, false, pos, relativePath)
175        break
176      case 'elif':
177        componentValidator.validateAttrElif(preNode, attrValue, attributes, index,
178          compileResult, pos, relativePath)
179        break
180      case 'else':
181        componentValidator.validateAttrElse(preNode, compileResult, pos, relativePath)
182        break
183      case 'append':
184        componentValidator.validateAppend(attrValue, compileResult, pos, relativePath)
185        break
186      default:
187        if (EVENT_START_REGEXP.test(attrName)) {
188          componentValidator.validateEvent(attrName, attrValue, compileResult, pos, relativePath)
189        } else if (REGEXP_DATA.test(attrName)) {
190          componentValidator.parseDataAttr(attrName, attrValue, compileResult, pos, relativePath)
191        } else {
192          componentValidator.validateAttr(
193              filePath,
194              attrName,
195              attrValue,
196              compileResult,
197              node.tagName,
198              pos,
199              relativePath
200          )
201        }
202    }
203  })
204}
205
206function checkAttrFor(node, attributes, pos) {
207  if (process.env.DEVICE_LEVEL === 'card') {
208    let tidExists = false
209    let ifExists = false
210    attributes.forEach(element => {
211      if(element.name === 'tid') {
212        tidExists = true
213      }
214      if(element.name === 'if' || element.name === 'elif' || element.name === 'else') {
215        ifExists = true
216      }
217    })
218    if (!tidExists) {
219      compileResult.log.push({
220        line: pos.line || 1,
221        column: pos.col || 1,
222        reason: `WARNING: The 'tid' is recommended here.`,
223      })
224    }
225    if (ifExists) {
226      compileResult.log.push({
227        line: pos.line || 1,
228        column: pos.col || 1,
229        reason: `ERROR: The 'for' and 'if' cannot be used in the same node.`,
230      })
231    }
232    let isParentFor = false
233    if (node && node.parentNode && node.parentNode.attrs) {
234      node.parentNode.attrs.forEach(element => {
235        if (element.name === 'for') {
236          isParentFor = true
237        }
238      })
239    }
240    if (isParentFor) {
241      compileResult.log.push({
242        line: pos.line || 1,
243        column: pos.col || 1,
244        reason: `ERROR: The nested 'for' is not supported.`,
245      })
246    }
247  }
248}
249
250function replaceAll(find, replace, str) {
251  find = find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
252  return str.replace(new RegExp(find, 'g'), replace);
253}
254
255/**
256 * Parse node children.
257 * @param {Object} node Nodes that need to be resolved.
258 * @param {String} filePath File resource path.
259 * @param {Object} preNode the previous node.
260 */
261function checkNodeChildren(node, filePath, relativePath) {
262  const children = node.childNodes.filter((child) => {
263    return (
264      (child.nodeName === '#text' && child.value.trim()) ||
265      !REGEXP_TEXT.test(child.nodeName)
266    )
267  })
268  const temp=compileResult.jsonTemplate
269  for (let i = 0; i < children.length; i++) {
270    const child = children[i]
271    let preNode
272    if (i > 0) {
273      preNode = children[i - 1]
274    }
275    if (!REGEXP_TEXT.test(child.nodeName)) {
276      compileResult.jsonTemplate={}
277      temp.children=temp.children||[]
278      temp.children.push(compileResult.jsonTemplate)
279      generate(child, filePath, preNode, relativePath)
280    } else {
281      const pos = {
282        line: child.sourceCodeLocation.startLine,
283        col: child.sourceCodeLocation.endLine,
284      }
285      if (node.nodeName === 'option') {
286        componentValidator.validateAttr(filePath, 'content', child.value,
287          compileResult, child.tagName, pos, relativePath)
288        continue
289      }
290      componentValidator.validateAttr(filePath, 'value', child.value,
291        compileResult, child.tagName, pos, relativePath)
292    }
293  }
294  compileResult.jsonTemplate=temp
295}
296
297module.exports = { parse: parse }
298