Files
developtools_ace-js2bundle/ace-loader/plugin/templater/component_validator.js
T
houhaoyu 0e4779ebd5 houhaoyu
resolve standard mistake

Signed-off-by: houhaoyu <houhaoyu@huawei.com>
Change-Id: Ibf7f4e9346bfe817a3e847e4340d56722aa3ae0a
2021-12-23 15:39:47 +08:00

1325 lines
43 KiB
JavaScript

/*
* Copyright (c) 2021 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const bind = require('./bind')
const styler = require('../styler')
const styleValidator = require('../styler/lib/validator')
const OHOS_THEME_PROP_GROUPS = require('../theme/ohosStyles')
const path = require('path')
const projectPath = process.env.aceModuleRoot || process.cwd()
const transContent = require('./content')
const REG_TAG_DATA_ATTR = /^data-\w+/
const REG_EVENT = /([^(]*)\((.+)\)/
const REG_DATA_BINDING = /{{{(.+?)}}}|{{(.+?)}}/
const { DEVICE_LEVEL, PLATFORM } = require('../../lib/lite/lite-enum')
const { richCommonTag, richNativeTag } = require('./rich_component_map')
const { liteCommonTag, liteNativeTag } = require('./lite_component_map')
const { cardCommonTag, cardNativeTag } = require('./card_component_map')
const card = process.env.DEVICE_LEVEL === DEVICE_LEVEL.CARD
const nativeTag = process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE ? liteNativeTag :
card ? cardNativeTag: richNativeTag
const commonTag = process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE ? liteCommonTag :
card ? cardCommonTag: richCommonTag
const tagSelfClosing = []
const tagWithoutRoot = []
const tagWithoutChild = []
const tagWithTextContent = []
const tagWithAll = []
const tagWithPath = []
const aliasTagMap = {}
const attrTagMap = {}
const funcAttrTagMap = {}
const defaultAttrTagMap = {}
const requireAttrTagMap = {}
const enumAttrTagMap = {}
const rootAttrTagMap = {}
const parentsTagMap = {}
const childrenTagMap = {}
const unSupportedChildren = {}
const eventsTagMap = {}
let elementNames = {}
/**
* prepare criteria for
* checking tag and attribute
*
* @type {{Object}}
*/
;(function initRules() {
for (const tag of Object.keys(nativeTag)) {
// tag with selfClosing attribute
if (nativeTag[tag].selfClosing) {
tagSelfClosing.push(tag)
}
// tag can not be root
if (nativeTag[tag].excludeRoot) {
tagWithoutRoot.push(tag)
}
// tag can not have children
if (nativeTag[tag].atomic) {
tagWithoutChild.push(tag)
}
// tag support text content
if (nativeTag[tag].textContent) {
tagWithTextContent.push(tag)
}
// store all tags
tagWithAll.push(tag)
// tags with other name
if (nativeTag[tag].alias && nativeTag[tag].alias.length) {
for (const alia of nativeTag[tag].alias) {
aliasTagMap[alia] = tag
}
}
// store private and common attr
const attrTagSet = {}
if (nativeTag[tag].uattrs) {
Object.assign(attrTagSet, nativeTag[tag].uattrs, {})
} else {
Object.assign(attrTagSet, nativeTag[tag].attrs, commonTag.attrs)
}
attrTagMap[tag] = attrTagSet
const checkFuncSet = {}
const attrDefaultSet = {}
const attrRequireArr = []
const attrEnumSet = {}
const rootNoAttr = []
for (const attr of Object.keys(attrTagSet)) {
if (attrTagSet[attr].checkFunc) {
checkFuncSet[attr] = attrTagSet[attr].checkFunc
}
if (attrTagSet[attr].def) {
attrDefaultSet[attr] = attrTagSet[attr].def
}
if (attrTagSet[attr].required) {
attrRequireArr.push(attr)
}
if (attrTagSet[attr].enum && attrTagSet[attr].enum.length > 0) {
attrEnumSet[attr] = attrTagSet[attr].enum
attrDefaultSet[attr] = attrTagSet[attr].enum[0]
}
if (attrTagSet[attr].excludeRoot) {
rootNoAttr.push(attr)
}
if (attrTagSet[attr].checkPath) {
tagWithPath.push(attr)
}
}
funcAttrTagMap[tag] = checkFuncSet
defaultAttrTagMap[tag] = attrDefaultSet
requireAttrTagMap[tag] = attrRequireArr
enumAttrTagMap[tag] = attrEnumSet
rootAttrTagMap[tag] = rootNoAttr
if (nativeTag[tag].parents) {
parentsTagMap[tag] = nativeTag[tag].parents.concat(commonTag.parents)
}
if (nativeTag[tag].children) {
childrenTagMap[tag] = nativeTag[tag].children.concat(commonTag.children)
}
if (nativeTag[tag].unSupportedChildren) {
unSupportedChildren[tag] = nativeTag[tag].unSupportedChildren.concat(commonTag.unSupportedChildren)
}
if (nativeTag[tag].uevents) {
eventsTagMap[tag] = nativeTag[tag].uevents.concat(nativeTag[tag].events)
} else {
eventsTagMap[tag] = [].concat(commonTag.events).concat(nativeTag[tag].events)
}
}
})()
/**
* verify the validity of tag
*
* @param {Object} domNode
* @param {Object} out
*/
function validateTagName(domNode, out, relativePath) {
const tagName = domNode.tagName
const children = domNode.childNodes || []
const nodeLoc = domNode.sourceCodeLocation ? {
line: domNode.sourceCodeLocation.startLine,
col: domNode.sourceCodeLocation.endLine
} : {}
const depends = out.deps
const jsonTemplate = out.jsonTemplate
const log = out.log
const oneChildNode = ['dialog', 'popup', 'badge', 'list-item']
const logType = process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE ? 'ERROR' : 'WARNING'
let pos = domNode.__location || domNode.sourceCodeLocation ? {
line: domNode.sourceCodeLocation.startLine,
col: domNode.sourceCodeLocation.endLine
} : {}
const elementNamesInFile = elementNames[relativePath] || []
// validate tag
if (!tagWithAll.includes(tagName) && tagName !== 'img' && !elementNamesInFile.includes(tagName)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: logType + ': The `' + tagName + '` tag is not supported.',
})
}
// validate dialog tag
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH && oneChildNode.includes(tagName)) {
checkOneChild(tagName, children, pos, log)
}
// preprocess attribute
if (!depends[tagName] && typeof tagName === 'string') {
depends.push(tagName)
}
const domNodeAttrs = domNode.attrs || []
const domNodeAttrName = []
for (const attr of domNodeAttrs) {
domNodeAttrName.push(attr.name.toLowerCase())
}
setDebugLine(jsonTemplate, relativePath, nodeLoc.line)
validateAliasTagMap(tagName, jsonTemplate, nodeLoc, log)
validateTagWithoutRoot(domNode, tagName, domNodeAttrName, nodeLoc, log)
validateName(tagName, children, nodeLoc, log)
validateForAttr(tagName, domNodeAttrs, domNodeAttrName, nodeLoc, log)
validatorLite(tagName, domNodeAttrs, domNodeAttrName, log, nodeLoc)
}
function validateName(tagName, children, nodeLoc, log) {
validateAtomicTag(tagName, children, nodeLoc, log)
validateTagWithAll(tagName, children, nodeLoc, log)
}
function validateForAttr(tagName, domNodeAttrs, domNodeAttrName, nodeLoc, log) {
validateAttrTagMap(tagName, domNodeAttrName, nodeLoc, log)
validateDefaultAttrTagMap(tagName, domNodeAttrs, domNodeAttrName, nodeLoc, log)
validateRequireAttrTagMap(tagName, domNodeAttrName, nodeLoc, log)
validateEnumAttrTagMap(tagName, domNodeAttrName, domNodeAttrs, nodeLoc, log)
validateFuncAttrTagMap(tagName, domNodeAttrName, domNodeAttrs, nodeLoc, log)
validateEventsTagMap(tagName, domNodeAttrName, nodeLoc, log)
}
/**
* verify the dialog tag
*
* @param {String} tagName
* @param {Array} children
* @param {Object} nodeLoc
* @param {Array} log
*/
function checkOneChild(name, children, pos, log) {
const oneChild = children.filter((child) => {
return child && child.nodeName !== '#text' && child.nodeName !== '#comment'
})
if (oneChild.length > 1) {
log.push({
line: pos.line || 1,
column: pos.col || 1,
reason: 'ERROR: The `' + name + '` tag can have only one child node.'
})
}
}
/**
* whether it can be root
* or be the root, check the
* attr can not have
*
* @param {Object} domNode
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateTagWithoutRoot(domNode, tagName, domNodeAttrName, nodeLoc, log) {
if (domNode._isroot) {
if (tagWithoutRoot.indexOf(tagName) !== -1) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: component `' + tagName + '` can not as root component.',
})
}
if (rootAttrTagMap[tagName]) {
rootAttrTagMap.forEach(function(attrName) {
if (domNodeAttrName[attrName]) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: root node `' + tagName + '` can not use attr `' + attrName + '`',
})
}
})
}
}
}
/**
* whether tag can have alias name
*
* @param {String} tagName
* @param {Object} result
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateAliasTagMap(tagName, result, nodeLoc, log) {
if (aliasTagMap[tagName]) {
if (tagName !== 'img') {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'NOTE: tag name `' + tagName + '` is automatically changed to `' + aliasTagMap[tagName] + '`',
})
}
tagName = aliasTagMap[tagName]
}
result.type = tagName
}
/**
* whether tag can have children
* how many text child the tag have
*
* @param {String} tagName
* @param {Object} children
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateAtomicTag(tagName, children, nodeLoc, log) {
if (tagWithoutChild.indexOf(tagName) >= 0 && children.length > 0 && !isSupportedSelfClosing(tagName)) {
if (tagWithTextContent.indexOf(tagName) < 0) {
children.every(
function(child) {
return child.nodeName === '#text' || child.nodeName === '#comment' ||
log.push({
line: nodeLoc.Line || 1,
column: nodeLoc.Col || 1,
reason: 'ERROR: tag `' + tagName + '` should just have one text node only',
})
},
)
} else {
children.every(
function(child) {
return child.nodeName === '#text' || child.nodeName === '#comment' ||
log.push({
line: nodeLoc.Line || 1,
column: nodeLoc.Col || 1,
reason: 'ERROR: tag `' + tagName + '` should not have children',
})
},
)
}
}
}
/**
* verify the tag whether
* support its attr
*
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateAttrTagMap(tagName, domNodeAttrName, nodeLoc, log) {
if (tagName === 'img') {
tagName = 'image'
}
if (attrTagMap[tagName]) {
domNodeAttrName.forEach(function(attrKey) {
if (attrKey === 'stroke-width') {
attrKey = 'strokeWidth'
} else if (attrKey === 'fill-opacity') {
attrKey = 'fillOpacity'
} else if (attrKey === 'stroke-dasharray') {
attrKey = 'strokeDasharray'
} else if (attrKey === 'stroke-dashoffset') {
attrKey = 'strokeDashoffset'
} else if (attrKey === 'stroke-linecap') {
attrKey = 'strokeLinecap'
} else if (attrKey === 'fill-rule') {
attrKey = 'fillRule'
} else if (attrKey === 'stroke-linejoin') {
attrKey = 'strokeLinejoin'
} else if (attrKey === 'stroke-miterlimit') {
attrKey = 'strokeMiterlimit'
} else if (attrKey === 'font-size') {
attrKey = 'fontSize'
} else if (attrKey === 'stroke-opacity') {
attrKey = 'strokeOpacity'
}
if (!attrKey.match(EVENT_START_REGEXP) && !(attrKey in attrTagMap[tagName])) {
if (attrKey in commonTag.attrs) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: tag `' + tagName + '` not support attr `' + attrKey + '`',
})
} else if (!validateDataAttr(attrKey)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: tag `' + tagName + '` use customize attr `' + attrKey + '`',
})
}
}
})
}
}
/**
* validate the default attr can be
* supported by the tag
*
* @param {String} tagName
* @param {Object} domNodeAttrs
* @param {Object} domNodeAttrName
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateDefaultAttrTagMap(tagName, domNodeAttrs, domNodeAttrName, nodeLoc, log) {
if (defaultAttrTagMap[tagName]) {
Object.keys(defaultAttrTagMap[tagName]).forEach(function(attrKey) {
const n = domNodeAttrName.indexOf(attrKey)
if (n >= 0 && domNodeAttrs[n].value === '') {
domNodeAttrs[n].value = defaultAttrTagMap[tagName][attrKey]
if (attrKey !== 'else') {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: tag `' + tagName + '` attr `' + attrKey + '`' + 'is null',
})
}
}
})
}
}
/**
* validate whether the required tag
* included in the tag
*
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateRequireAttrTagMap(tagName, domNodeAttrName, nodeLoc, log) {
if (requireAttrTagMap[tagName]) {
requireAttrTagMap[tagName].forEach(function(attrKey) {
if (domNodeAttrName.indexOf(attrKey) < 0) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: tag `' + tagName + '` not define attr `' + attrKey + '`',
})
}
})
}
}
/**
* validate whether the tag include
* illegal enum attr
*
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} nodeAttrs
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateEnumAttrTagMap(tagName, domNodeAttrName, domNodeAttrs, nodeLoc, log) {
if (enumAttrTagMap[tagName]) {
Object.keys(enumAttrTagMap[tagName]).forEach(function(attrKey) {
const index = domNodeAttrName.indexOf(attrKey)
if (index >= 0) {
const v = domNodeAttrs[index].value.trim()
if (!REG_DATA_BINDING.test(v)) {
const attrValueEnum = enumAttrTagMap[tagName][attrKey]
if (attrValueEnum.indexOf(v) < 0) {
domNodeAttrs[index].value = attrValueEnum[0]
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: tag `' + tagName + '` attr `' + attrKey + '` value `' + v + '` is illegal',
})
}
}
}
})
}
}
/**
* verify the validity of func
* attr of the tag
*
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} domNodeAttrs
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateFuncAttrTagMap(tagName, domNodeAttrName, domNodeAttrs, nodeLoc, log) {
if (funcAttrTagMap[tagName]) {
Object.keys(funcAttrTagMap[tagName]).forEach(function(attrKey) {
const n = domNodeAttrName.indexOf(attrKey)
if (n >= 0) {
const v = domNodeAttrs[n].value
if (!REG_DATA_BINDING.test(v)) {
const func = funcAttrTagMap[tagName][attrKey]
const validator = styleValidator.validateFuncMap[func]
if (typeof validator == 'function') {
const res = validator(v)
if (res && res.reason) {
domNodeAttrs[n].value = res.value
const rea = res.reason(attrKey, v, res.value)
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: rea,
})
}
}
}
}
})
}
}
/**
* verify the validity of event
* attr in this tag
*
* @param {String} tagName
* @param {Object} domNodeAttrName
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateEventsTagMap(tagName, domNodeAttrName, nodeLoc, log) {
if (eventsTagMap[tagName]) {
const eventArray = eventsTagMap[tagName]
domNodeAttrName.forEach(function(attrKey) {
if (attrKey.match(EVENT_START_REGEXP)) {
const tempName = attrKey.replace(EVENT_START_REGEXP, '')
const eventName = (tempName.match(TOUCH_CAPTURE_EVENT_REGEXP) && process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH &&
process.env.PLATFORM_VERSION === PLATFORM.VERSION6) ? tempName : tempName.replace(EVENT_END_REGEXP, '')
if (eventArray.indexOf(eventName.toLowerCase()) < 0) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: tag `' + tagName + '` not support event `' + eventName + '`',
})
}
}
})
}
}
/**
* verify validity of tag children
* verify the children is validity
*
* @param {String} tagName
* @param {Object} children
* @param {Object} nodeLoc
* @param {Object} log
*/
function validateTagWithAll(tagName, children, nodeLoc, log) {
if (tagWithAll.indexOf(tagName) >= 0 && children.length > 0) {
const childrenArray = childrenTagMap[tagName]
const unSupportedChildrenArray = unSupportedChildren[tagName]
const isTabs = tagName === 'tabs'
let tabContentCount = void 0
let tabBarCount = void 0
if (isTabs) {
tabBarCount = 0
tabContentCount = 0
}
children.forEach(function(child) {
if (tagWithAll.indexOf(child.nodeName) >= 0) {
const t = parentsTagMap[child.nodeNames]
if ((t && t.indexOf(tagName) < 0) || (childrenArray && childrenArray.indexOf(child.nodeName) < 0) ||
(unSupportedChildrenArray && unSupportedChildrenArray.indexOf(child.nodeName) >= 0)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: tag `' + tagName + '` not support child tag `' + child.nodeName + '`',
})
if (isTabs) {
let count = 0
if (child.nodeName === 'tab-content') {
count = ++tabContentCount
}
if (child.nodeName === 'tab-bar') {
count = ++tabBarCount
}
if (count > 1) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: tag `tabs` child tag `' + child.nodeName + '` support at most one',
})
}
}
}
}
})
}
}
/**
* validate attr of lite device
*
* @param {String} tagName
* @param {Object} domNodeAttrs
* @param {Object} domNodeAttrName
* @param {Object} log
* @param {Object} nodeLoc
*/
function validatorLite(tagName, domNodeAttrs, domNodeAttrName, log, nodeLoc) {
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE) {
inputLite(tagName, domNodeAttrs, domNodeAttrName, log, nodeLoc)
// in lite device, not support writing `if` and `for` in the same component
if (ismatchIfAndFor(domNodeAttrName)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: in tag `' + tagName + '` not support writing `if` and `for` in the same component',
})
}
// in lite device, class selector does not support data binding
classLite(tagName, domNodeAttrs, log, nodeLoc)
}
}
/**
* validate the type when
* event change
*
* @param {String} tagName
* @param {Object} domNodeAttrs
* @param {Object} domNodeAttrName
* @param {Object} log
* @param {Object} nodeLoc
*/
function inputLite(tagName, domNodeAttrs, domNodeAttrName, log, nodeLoc) {
let typeValue
if (tagName=== 'input') {
domNodeAttrs.map(function(item) {
if (item.name === 'type') {
typeValue = item.value
}
})
if (isMatchChange(typeValue, domNodeAttrName)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: tag `' + tagName + '` not support event `change` when the type is not checkbox and radio',
})
}
}
}
function isMatchChange(typeValue, nodeAttrName) {
return !['checkbox', 'radio'].includes(typeValue) && (nodeAttrName.includes('onchange') || nodeAttrName.includes('@change'))
}
function ismatchIfAndFor(nodeAttrName) {
return nodeAttrName.includes('if') && nodeAttrName.includes('for')
}
function classLite(tagName, domNodeAttrs, log, nodeLoc) {
// in lite device, class selector does not support data binding
domNodeAttrs.map(function(item) {
if (item.name === 'class' && bind.containExp(item.value)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: in tag `' + tagName + '` class selector does not support data binding.',
})
}
})
}
/**
* parse class for tag
*
* @param {String} classNames
* @param {Object} out
*/
function validateClass(classNames, out, nodeLoc, relativePath) {
classNames = classNames.trim()
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line, classNames)
if (bind.containExp(classNames)) {
out.jsonTemplate.classList = eval(bind.transExpForList(classNames))
} else {
out.jsonTemplate.classList = classNames.split(/\s+/)
}
}
function preprocessSystemResourceReference(styleContent, cssStyle) {
if (styleContent.length !== 2) {
return false;
}
const key = styleContent[0].trim().replace(/-([a-z])/g, function(s, m) { return m.toUpperCase() })
let value = styleContent[1].trim()
let result
// target format: {{$r("ohos.id_color_background")}} or {{$r('ohos.id_color_background')}}
let ResourceRefReg = /{{\s*\$r\s*\(\s*(['"]ohos\.(?<resName>\w+)['"])\s*\)\s*}}/
result = ResourceRefReg.exec(value);
if (result) {
const resourceName = result.groups['resName']
if (resourceName && OHOS_THEME_PROP_GROUPS[resourceName]) {
cssStyle[key] = "@ohos_id_" + OHOS_THEME_PROP_GROUPS[resourceName]
return true
}
}
// target format: {{$r("sys.type.id_color_background")}} or {{$r('sys.type.id_color_background')}}
// The "type" field can be "float", "color", "string" and so on.
let SysResourceTypeRefReg = /{{\s*\$r\s*\(\s*(['"]sys\.(?<resType>\w+)\.(?<resName>\w+)['"])\s*\)\s*}}/
result = SysResourceTypeRefReg.exec(value);
if (result) {
const resourceName = result.groups['resName']
const resourceType = result.groups['resType']
if (resourceName && resourceType && OHOS_THEME_PROP_GROUPS[resourceName]) {
cssStyle[key] = "@sys." + resourceType + "." + OHOS_THEME_PROP_GROUPS[resourceName]
return true
}
}
// target format: {{$r("app.type.developer_defined_color")}} or {{$r('app.type.developer_defined_color')}}
// The "type" field can be "float", "color", "string" and so on.
let AppResourceTypeRefReg = /{{\s*\$r\s*\(\s*(['"]app\.(?<resType>\w+)\.(?<resName>\w+)['"])\s*\)\s*}}/
result = AppResourceTypeRefReg.exec(value);
if (result) {
const resourceName = result.groups['resName']
const resourceType = result.groups['resType']
if (resourceName && resourceType) {
cssStyle[key] = "@app." + resourceType + "." + resourceName
return true
}
}
return false
}
function validateItem(key, value, nodeLoc, log) {
const valuejsonTemplate = styler.validateItem(key, value)
if (valuejsonTemplate.log) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: valuejsonTemplate.log.reason
})
}
return valuejsonTemplate
}
/**
* parse style for tag
*
* @param {String} css
* @param {Object} out
* @param {Object} nodeLoc
*/
function validateStyle(css, out, nodeLoc, relativePath) {
const log = out.log
if (css) {
const cssStyle = {}
const cssArray = css.split(';')
processCssArray(css, out, nodeLoc, cssStyle, cssArray)
out.jsonTemplate.style = cssStyle
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
} else {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: style attr is null'
})
}
}
function processCssArray(css, out, nodeLoc, cssStyle, cssArray) {
for (let i = 0; i < cssArray.length; i++) {
let styleContent = cssArray[i].trim().split(':')
if (preprocessSystemResourceReference(styleContent, cssStyle)) {
continue;
}
if (styleContent.length === 2) {
const key = styleContent[0].trim().replace(/-([a-z])/g, function(s, m) { return m.toUpperCase() })
let value = bind(styleContent[1].trim(), undefined, true, out, nodeLoc)
const contentValue = styleContent[1].trim().toString();
if (contentValue.match(/^linear-gradient/) && contentValue.match(/\(.*\{\{.*\}\}.*\)/)) {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: `ERROR: can not bind data for linear-gradient in inline style at ${css}`
})
}
if (key === 'flex' && typeof value === 'string') {
expandFlex(key, value, cssStyle, nodeLoc, log)
} else {
const valuejsonTemplate = validateItem(key, value, nodeLoc, log)
value = valuejsonTemplate.value
if (['[object Number]', '[object String]', '[object Function]', '[object Object]', '[object Array]'].includes(
Object.prototype.toString.call(value))) {
expandStyle(key, value, valuejsonTemplate, cssStyle, out, nodeLoc, log)
}
}
}
if (styleContent.length > 2) {
styleContent[1] = styleContent.slice(1).join(':')
styleContent = styleContent.slice(0, 2)
if (REG_DATA_BINDING.test(styleContent[1])) {
styleContent[0] = styleContent[0].trim().replace(/-([a-z])/g, function(s, m) { return m.toUpperCase() })
cssStyle[styleContent[0]] = bind(styleContent[1], undefined, true, out, nodeLoc)
} else {
// handle special cases like "background-image: url('https://xxx.jpg')"
const key = styleContent[0].trim().replace(/-([a-z])/g, function(s, m) { return m.toUpperCase() })
if (key === 'backgroundImage') {
let value = styleContent[1].trim()
const valuejsonTemplate = validateItem(key, value, nodeLoc, log)
cssStyle[key] = valuejsonTemplate.value
}
}
}
}
}
function expandFlex(key, value, cssStyle, nodeLoc, log) {
const valueArray = value.split(/\s+/)
if (valueArray.length === 1) {
expandFlexOne(key, valueArray, cssStyle, nodeLoc, log)
} else if (valueArray.length === 2) {
expandFlexTwo(key, value, valueArray, cssStyle, nodeLoc, log)
} else if (valueArray.length === 3) {
expandFlexThree(key, value, valueArray, cssStyle, nodeLoc, log)
} else {
log.push({
line: nodeLoc.line,
column: nodeLoc.column,
reason: 'ERROR: Value `' + value + '` of the `' + key + '` attribute is incorrect.',
})
}
}
function expandFlexOne(key, valueArray, cssStyle, nodeLoc, log) {
const array = ['none', 'auto', 'initial']
if (array.includes(valueArray[0])) {
cssStyle['flex'] = valueArray[0]
} else if (styler.getUnit(valueArray[0]) === 'px') {
cssStyle['flexBasis'] = valueArray[0]
} else if (styler.getUnit(valueArray[0]) === 'none') {
cssStyle['flexGrow'] = valueArray[0]
} else {
log.push({
line: nodeLoc.line,
column: nodeLoc.column,
reason: 'ERROR: Value `' + valueArray[0] + '` of the `' + key + '` attribute is incorrect.' +
'It must be a number, a number with unit `' + 'px`' + ', none, auto, or initial.',
})
}
}
function expandFlexTwo(key, value, valueArray, cssStyle, nodeLoc, log) {
if (styler.getUnit(valueArray[0]) === 'none') {
cssStyle['flexGrow'] = valueArray[0]
if (styler.getUnit(valueArray[1]) === 'px') {
cssStyle['flexBasis'] = valueArray[1]
} else if (styler.getUnit(valueArray[1]) === 'none') {
cssStyle['flexShrink'] = valueArray[1]
} else {
log.push({
line: nodeLoc.line,
column: nodeLoc.column,
reason: 'ERROR: Value `' + value + '` of the `' + key + '` attribute is incorrect. Value `' +
valueArray[1] + '` must be a number or a number with unit `' + 'px`.',
})
}
} else {
log.push({
line: nodeLoc.line,
column: nodeLoc.column,
reason: 'ERROR: Value `' + value + '` of the `' + key + '` attribute is incorrect. Value `' +
valueArray[0] + '` must be a number.',
})
}
}
function expandFlexThree(key, value, valueArray, cssStyle, nodeLoc, log) {
if (styler.getUnit(valueArray[0]) === 'none' && styler.getUnit(valueArray[1]) === 'none' &&
styler.getUnit(valueArray[2]) === 'px') {
cssStyle['flexGrow'] = valueArray[0]
cssStyle['flexShrink'] = valueArray[1]
cssStyle['flexBasis'] = valueArray[2]
} else {
log.push({
line: nodeLoc.line,
column: nodeLoc.column,
reason: 'ERROR: Value `' + value + '` of the `' + key +
'` attribute is incorrect. It must be in the format of (1, 1, 1px).',
})
}
}
function expandStyle(key, value, valuejsonTemplate, cssStyle, out, nodeLoc, log) {
if (Object.values(styler.util.SPLECIAL_ATTR).includes(key) && typeof value !== 'function') {
styler.expand(valuejsonTemplate, key, cssStyle)
} else if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE && key === 'border' && typeof value === 'function') {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: ' + key + ' shorthand inline style does not support data binding.',
})
} else if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE && out.jsonTemplate.type === 'list' &&
key == 'flexDirection' && typeof value === 'function') {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: `list` tag `flex-direction` inline style does not support data binding.',
})
} else {
cssStyle[key] = value
}
}
/**
* parse if and else expression
*
* @param {String} val
* @param {Object} out
* @param {Boolean} flag
* @param {Object} nodeLoc
*/
function validateIf(val, out, flag, nodeLoc, relativePath) {
if (!REG_DATA_BINDING.test(val)) {
if (val.trim() === 'false') {
val = card ? false : '{{false}}'
} else if (val.trim() === 'true') {
val = card ? true : '{{true}}'
} else {
const log = out.log
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'ERROR: if value cannot be ' + val + '. The default value is true or false.'
})
}
}
if (flag) {
const content = val.replace('{{', '').replace('}}', '')
val = !card ? '{{!(' + content + ')}}' :
REG_DATA_BINDING.test(val) ? '!{{' + content + '}}' : '!' + content
}
const value = bind(val, undefined, false, out, nodeLoc)
const show = card && flag && /\$f/.test(value) ? value.substr(3, value.length - 4) : value
out.jsonTemplate.shown = show
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
}
/**
* parse elif and else expression
*
* @param {String} val
* @param {Object} out
* @param {Boolean} flag
*/
function validateElif(val, out, flag, nodeLoc, relativePath) {
if (!REG_DATA_BINDING.test(val)) {
val = card ? val : '{{' + val + '}}'
}
if (val) {
if (flag) {
const first = val.indexOf('&&')
const content = !card ?
'{{!(' + val.substr(2, first - 2) + ')' :
REG_DATA_BINDING.test(val) ?
'!{{' + val.substr(2, first - 2) :
'!' + val.substr(0, first)
val = content + val.substr(first, val.length)
}
const value = bind(val, undefined, false, out, nodeLoc)
out.jsonTemplate.shown = card && /\$f/.test(value) ? value.substr(3, value.length - 4) : value
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
}
}
/**
*
* parse for expression
*
* @param {String} val
* @param {Object} out
* @param {Object} nodeLoc
*/
function validateFor(val, out, nodeLoc, relativePath) {
const log = out.log
if (val) {
if (val.startsWith('{{') && val.endsWith('}}')) {
val = val.substr(2, val.length - 4)
}
let forMatch
const suffix = val.match(/(?<=in\s)(.*)/)
const prefix = val.match(/(.*)(?=\s+in\s+)/)
if (suffix && prefix) {
const v = prefix[0].match(/\((.*),(.*)\)/)
forMatch = { exp: bind(('{{' + suffix[0].trim() + '}}'), undefined, false, out, nodeLoc) }
if (v) {
forMatch.key = v[1].trim()
forMatch.value = v[2].trim()
} else {
forMatch.value = prefix[0].trim()
}
} else {
forMatch = bind('{{' + val + '}}', undefined, false, out, nodeLoc)
}
out.jsonTemplate.repeat = forMatch
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
} else {
log.push({
line: nodeLoc.line || 1,
column: nodeLoc.col || 1,
reason: 'WARNING: for attr is null',
})
}
}
/**
* parse id for tag
*
* @param {String} id
* @param {Object} out
*/
function validateId(id, out, nodeLoc, relativePath) {
if (id) {
out.jsonTemplate.id = REG_DATA_BINDING.test(id) ? bind(id, undefined, true, out, nodeLoc) : id
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
if (!card) {
out.jsonTemplate.attr = out.jsonTemplate.attr || {}
out.jsonTemplate.attr[styler.util.hyphenedToCamelCase('id')] = bind(id, undefined, true, out, nodeLoc)
}
}
}
/**
* parse append for tag
*
* @param {String} val
* @param {Object} out
*/
function validateAppend(val, out, nodeLoc, relativePath) {
if (val) {
out.jsonTemplate.append = bind(val, undefined, true, out, nodeLoc)
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
}
}
const EVENT_START_REGEXP = /^(on:|on|@|grab:)/;
const EVENT_END_REGEXP = /(\.bubble|\.capture)$/;
const START_CATCH_REGEXP = /^(grab:)/;
const END_CAPTURE_REGEXP = /(\.capture)$/;
const TOUCH_EVENT_REGEXP = /^(touch)/;
const CLICK_EVENT_REGEXP = /^(click)$/;
const TOUCH_CAPTURE_EVENT_REGEXP = /^(?!touch).*?(\.capture)$/;
/**
* parse event for tag
*
* @param {String} eventName
* @param {Object} val
* @param {Object} out
*/
function validateEvent(eventName, val, out, pos, relativePath) {
const tempName = eventName.replace(EVENT_START_REGEXP, '')
const name = (tempName.match(TOUCH_CAPTURE_EVENT_REGEXP) && process.env.PLATFORM_VERSION === PLATFORM.VERSION6 &&
process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH) ? tempName : tempName.replace(EVENT_END_REGEXP, '')
if (name && val) {
if (card && val.match(/(.*)\((.*)\)/)) {
out.log.push({
line: pos.line || 1,
column: pos.col || 1,
reason: 'ERROR: The event `' + val + '` does not support.'
})
return
}
if (REG_DATA_BINDING.test(val)) {
val = bind.removeAllExpFix(val.trim())
}
// empty event param e.g. onclick="test()"
const empty = val.match(/(.*)\(\)$/)
if (empty) {
val = empty[1]
} else {
const content = val.match(REG_EVENT)
if (content) {
const functionName = content[1]
let paramList = content[2]
if (paramList) {
paramList = transContent.parseExpression(paramList, true)
val = eval('(function (evt) {' + bind('{{' + functionName + '(' + paramList + ',evt)}}',
false, true, out, pos) + '})')
}
}
}
distributeEvent(out, eventName, name, val)
setDebugLine(out.jsonTemplate, relativePath, pos.line)
}
}
function distributeEvent(out, eventName, name, val) {
if ((process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE || TOUCH_EVENT_REGEXP.test(name)) &&
process.env.PLATFORM_VERSION !== PLATFORM.VERSION3) {
if (eventName.match(START_CATCH_REGEXP)) {
if (eventName.match(END_CAPTURE_REGEXP)) {
out.jsonTemplate.catchCaptureEvents = out.jsonTemplate.catchCaptureEvents || {}
out.jsonTemplate.catchCaptureEvents[name] = val
} else {
out.jsonTemplate.catchBubbleEvents = out.jsonTemplate.catchBubbleEvents || {}
out.jsonTemplate.catchBubbleEvents[name] = val
}
} else if (eventName.match(END_CAPTURE_REGEXP)) {
out.jsonTemplate.onCaptureEvents = out.jsonTemplate.onCaptureEvents || {}
out.jsonTemplate.onCaptureEvents[name] = val
} else {
out.jsonTemplate.onBubbleEvents = out.jsonTemplate.onBubbleEvents || {}
out.jsonTemplate.onBubbleEvents[name] = val
}
} else if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH && CLICK_EVENT_REGEXP.test(name) &&
!eventName.match(END_CAPTURE_REGEXP) && process.env.PLATFORM_VERSION === PLATFORM.VERSION6) {
if (eventName.match(START_CATCH_REGEXP)) {
out.jsonTemplate.catchBubbleEvents = out.jsonTemplate.catchBubbleEvents || {}
out.jsonTemplate.catchBubbleEvents[name] = val
} else {
out.jsonTemplate.onBubbleEvents = out.jsonTemplate.onBubbleEvents || {}
out.jsonTemplate.onBubbleEvents[name] = val
}
} else {
out.jsonTemplate.events = out.jsonTemplate.events || {}
out.jsonTemplate.events[name] = val
}
}
const transArray = [/\\a/g, /\\b/g, /\\f/g, /\\n/g, /\\r/g, /\\t/g,
/\\v/g, /\\\\/g, /\\'/g, /\\"/g, /\\0/g]
const transReplaceArray = ['\a', '\b', '\f', '\n', '\r', '\t',
'\v', '\\', '\'', '\"', '\0']
function validateAttr(resourcePath, attrName, attrValue, out, tagName, nodeLoc, relativePath) {
if (card && attrName === 'tid' && bind.isExp(attrValue)) {
out.log.push({
line: nodeLoc.line,
column: nodeLoc.col,
reason: `ERROR: The 'tid' does not support the expression.`
})
}
if (attrName === 'value') {
transArray.forEach((trans, index) => {
attrValue = attrValue.replace(trans, transReplaceArray[index])
})
}
if (attrName && (typeof attrValue === 'string' || typeof attrValue === 'number') && attrValue) {
if (attrName === 'value' && tagName === 'text') {
out.log.push({
line: nodeLoc.line,
column: nodeLoc.col,
reason: 'WARNING: `value` content could be written between <text> and </text>',
})
}
// dealing with <image src = "{{$r('sys.media.sys_background_image')}}"></image>
let result
// target format: {{$r('sys.media.sys_background_image')}}
let SysResourceTypeRefReg = /{{\s*\$r\s*\(\s*(['"]sys\.media\.(?<resName>\w+)['"])\s*\)\s*}}/
result = SysResourceTypeRefReg.exec(attrValue);
getAttrValue(result, attrValue, true);
// target format: {{$r('app.media.customized_background_image')}}
let AppResourceTypeRefReg = /{{\s*\$r\s*\(\s*(['"]app\.media\.(?<resName>\w+)['"])\s*\)\s*}}/
result = AppResourceTypeRefReg.exec(attrValue);
getAttrValue(result, attrValue, false);
if (tagWithPath.indexOf(attrName) >= 0 && attrValue.match(/^\.\.\/|^\.\//)) {
if (!resourcePath) {
return
}
if (attrValue.indexOf('./') >= 0) {
resourcePath = resourcePath.substring(0, resourcePath.lastIndexOf(path.sep) + 1)
attrValue = path.resolve(resourcePath, attrValue)
}
attrValue = attrValue.replace(projectPath, '').replace(/\\/g, '/')
}
out.jsonTemplate.attr = out.jsonTemplate.attr || {}
out.jsonTemplate.attr[attrName.replace(/-([a-z])/g, function (s, m) { return m.toUpperCase() })] =
bind(attrValue, undefined, true, out, nodeLoc)
setDebugLine(out.jsonTemplate, relativePath, nodeLoc.line)
}
}
function getAttrValue(result, attrValue, isSys) {
if (result && isSys) {
const resourceName = result.groups['resName']
if (resourceName && OHOS_THEME_PROP_GROUPS[resourceName]) {
attrValue = "@sys.media." + OHOS_THEME_PROP_GROUPS[resourceName]
}
} else if (result && !isSys) {
const resourceName = result.groups['resName']
if (resourceName) {
attrValue = "@app.media." + resourceName
}
}
}
/**
* parse elif attr
*
* @param {Object} preNode
* @param {String} attrValue
* @param {Object} hmlAttrs
* @param {Number} i
* @param {Object} out
*/
function validateAttrElif(preNode, attrValue, hmlAttrs, i, out, nodeLoc, relativePath) {
if (preNode && preNode.attrs) {
for (let j = 0; j < preNode.attrs.length; j++) {
const prop = preNode.attrs[j]
const newAttrValue = attrValue.replace('{{', '').replace('}}', '')
if (prop.name === 'if') {
const newPropValue = prop.value.replace('{{', '').replace('}}', '')
attrValue = !card ?
'{{' + newAttrValue + '&&!(' + newPropValue + ')}}' :
REG_DATA_BINDING.test(attrValue) ?
'{{' + newAttrValue + '}}' + '&&!{{' + newPropValue + '}}' :
newAttrValue + ' && !' + newPropValue
validateElif(attrValue, out, false, nodeLoc, relativePath)
}
if (prop.name === 'elif') {
const first = prop.value.indexOf('&&')
const firstPropValue = prop.value.substr(0, first).replace("{{", '')
const lengthPropValue = prop.value.substr(first, prop.value.length)
attrValue = !card ?
'{{' + newAttrValue + '&&!(' + firstPropValue + ')' + lengthPropValue :
REG_DATA_BINDING.test(attrValue) ?
'{{' + newAttrValue + '}}' + '&&!{{' + firstPropValue + lengthPropValue :
newAttrValue + ' && !' + firstPropValue + lengthPropValue
validateElif(attrValue, out, false, nodeLoc, relativePath)
}
}
}
hmlAttrs[i].value = attrValue
}
/**
* parse else attr
*
* @param {Object} preNode
* @param {Object} out
* @param {Object} nodeLoc
*/
function validateAttrElse(preNode, out, nodeLoc, relativePath) {
if (preNode && preNode.attrs) {
for (let j = 0; j < preNode.attrs.length; j++) {
const prop = preNode.attrs[j]
if (prop.name === 'if') {
validateIf(prop.value, out, true, nodeLoc, relativePath)
}
if (prop.name === 'elif') {
validateElif(prop.value, out, true, nodeLoc, relativePath)
}
}
}
}
function parseDataAttr(name, value, out) {
const childName = name.replace('data-', '')
out.jsonTemplate.attr = out.jsonTemplate.attr || {}
out.jsonTemplate.attr.$data = out.jsonTemplate.attr.$data || {}
out.jsonTemplate.attr.$data[childName] = bind(value, undefined, true, out)
}
function isSupportedSelfClosing(tagName) {
if (tagName && typeof tagName === 'string') {
const n = aliasTagMap[tagName]
if (n) {
if (nativeTag[n] && tagSelfClosing.indexOf(n) !== -1) {
return true
}
} else {
if (nativeTag[tagName] && tagSelfClosing.indexOf(tagName) !== -1) {
return true
}
}
}
return false
}
function validateDataAttr(e) {
return REG_TAG_DATA_ATTR.test(e)
}
function isReservedTag(tagName) {
return tagWithAll.indexOf(tagName) !== -1
}
function setDebugLine(jsonTemplate, relativePath, line, className) {
jsonTemplate.attr = jsonTemplate.attr || {}
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH && process.env.buildMode === 'debug') {
jsonTemplate.attr['debugLine'] = relativePath + ':' + line
if (className) {
jsonTemplate.attr['className'] = className
}
}
}
module.exports = {
validateTagName: validateTagName,
validateId: validateId,
validateClass: validateClass,
validateStyle: validateStyle,
validateIf: validateIf,
validateElif: validateElif,
validateFor: validateFor,
validateAppend: validateAppend,
validateEvent: validateEvent,
validateAttr: validateAttr,
validateAttrElse: validateAttrElse,
validateAttrElif: validateAttrElif,
parseDataAttr: parseDataAttr,
isReservedTag: isReservedTag,
isSupportedSelfClosing: isSupportedSelfClosing,
elementNames: elementNames
}