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