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