@mpxjs/webpack-plugin
Version:
mpx compile core
1,806 lines (1,658 loc) • 91.3 kB
JavaScript
const JSON5 = require('json5')
const he = require('he')
const config = require('../config')
const { MPX_ROOT_VIEW, MPX_APP_MODULE_ID, PARENT_MODULE_ID } = require('../utils/const')
const normalize = require('../utils/normalize')
const { normalizeCondition } = require('../utils/match-condition')
const isValidIdentifierStr = require('../utils/is-valid-identifier-str')
const isEmptyObject = require('../utils/is-empty-object')
const getRulesRunner = require('../platform/index')
const addQuery = require('../utils/add-query')
const transDynamicClassExpr = require('./trans-dynamic-class-expr')
const dash2hump = require('../utils/hump-dash').dash2hump
const makeMap = require('../utils/make-map')
const { isNonPhrasingTag } = require('../utils/dom-tag-config')
const setBaseWxml = require('../runtime-render/base-wxml')
const { parseExp } = require('./parse-exps')
const shallowStringify = require('../utils/shallow-stringify')
const { isReact, isWeb } = require('../utils/env')
const no = function () {
return false
}
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
// Regular Expressions for parsing tags and attributes
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'
const startTagOpen = new RegExp(('^<' + qnameCapture))
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(('^<\\/' + qnameCapture + '[^>]*>'))
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!--/
const conditionalComment = /^<!\[/
const specialClassReg = /^mpx-((cover-)?view|button|navigator|picker-view|input|textarea)$/
let IS_REGEX_CAPTURING_BROKEN = false
'x'.replace(/x(.)?/g, function (m, g) {
IS_REGEX_CAPTURING_BROKEN = g === ''
})
// Special Elements (can contain anything)
const isPlainTextElement = makeMap('script,style,textarea', true)
const reCache = {}
// #5992
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = function (tag, html) {
return tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
}
const splitRE = /\r?\n/g
const replaceRE = /./g
const isSpecialTag = makeMap('script,style,template,json', true)
function makeAttrsMap (attrs) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value
}
return map
}
function createASTElement (tag, attrs = [], parent = null) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent: parent,
children: []
}
}
function isForbiddenTag (el) {
return (
el.tag === 'style' ||
(el.tag === 'script' && (
!el.attrsMap.type ||
el.attrsMap.type === 'text/javascript'
))
)
}
// configurable state
// 由于template处理为纯同步过程,采用闭包变量存储各种状态方便全局访问
let warn$1
let error$1
let mode
let env
let defs
let i18n
let srcMode
let ctorType
let moduleId
let isNative
let hasScoped
let hasVirtualHost
let isCustomText
let runtimeCompile
let rulesRunner
let currentEl
let injectNodes = []
let forScopes = []
let forScopesMap = {}
let platformGetTagNamespace
let filePath
let refId
let hasI18n = false
let i18nInjectableComputed = []
let hasOptionalChaining = false
let processingTemplate = false
const rulesResultMap = new Map()
let usingComponents = []
let usingComponentsInfo = {}
let componentGenerics = {}
function updateForScopesMap () {
forScopesMap = {}
forScopes.forEach((scope) => {
forScopesMap[scope.index] = 'index'
forScopesMap[scope.item] = 'item'
})
return forScopesMap
}
function pushForScopes (scope) {
forScopes.push(scope)
updateForScopesMap()
return scope
}
function popForScopes () {
const scope = forScopes.pop()
updateForScopesMap()
return scope
}
const deleteErrorInResultMap = (node) => {
rulesResultMap.delete(node)
Array.isArray(node.children) && node.children.forEach(item => deleteErrorInResultMap(item))
}
function baseWarn (msg) {
console.warn(('[template compiler]: ' + msg))
}
function baseError (msg) {
console.error(('[template compiler]: ' + msg))
}
const decodeMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
''': '\''
}
const encodedRe = /&(?:lt|gt|quot|amp|#39);/g
function decode (value) {
if (value != null) {
return value.replace(encodedRe, function (match) {
return decodeMap[match]
})
}
}
const i18nFuncNames = ['\\$(t)', '\\$(tc)', '\\$(te)', '\\$(tm)', 't', 'tc', 'te', 'tm']
const i18nWxsPath = normalize.lib('runtime/i18n.wxs')
const i18nWxsLoaderPath = normalize.lib('wxs/i18n-loader.js')
// 添加~前缀避免wxs绝对路径在存在projectRoot时被拼接为错误路径
const i18nWxsRequest = '~' + i18nWxsLoaderPath + '!' + i18nWxsPath
const i18nModuleName = '_i'
const stringifyWxsPath = '~' + normalize.lib('runtime/stringify.wxs')
const stringifyModuleName = '_s'
const optionalChainWxsPath = '~' + normalize.lib('runtime/oc.wxs')
const optionalChainWxsName = '_oc' // 改成_oc解决web下_o重名问题
const tagRES = /(\{\{(?:.|\n|\r)+?\}\})(?!})/
const tagRE = /\{\{((?:.|\n|\r)+?)\}\}(?!})/
const tagREG = /\{\{((?:.|\n|\r)+?)\}\}(?!})/g
function decodeInMustache (value) {
const sArr = value.split(tagRES)
const ret = sArr.map((s) => {
if (tagRES.test(s)) {
return decode(s)
}
return s
})
return ret.join('')
}
function parseHTML (html, options) {
const stack = []
const expectHTML = options.expectHTML
const isUnaryTag$$1 = options.isUnaryTag || no
const canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no
let index = 0
let last, lastTag
while (html) {
last = html
// Make sure we're not in a plaintext content element like script/style
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
advance(commentEnd + 3)
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2)
continue
}
}
// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(lastTag, html)) {
advance(1)
}
continue
}
}
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (!endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest)) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) {
break
}
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
advance(textEnd)
}
if (textEnd < 0) {
text = html
html = ''
}
if (options.chars && text) {
options.chars(text)
}
} else {
let endTagLength = 0
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
const rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1)
}
if (options.chars) {
options.chars(text)
}
return ''
})
index += html.length - rest$1.length
html = rest$1
parseEndTag(stackedTag, index - endTagLength, index)
}
if (html === last) {
options.chars && options.chars(html)
if (!stack.length && options.warn) {
options.warn(('Mal-formatted tag at end of template: "' + html + '"'))
}
break
}
}
// Clean up any remaining tags
parseEndTag()
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
const unary = isUnaryTag$$1(tagName) || !!unarySlash
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') {
delete args[3]
}
if (args[4] === '') {
delete args[4]
}
if (args[5] === '') {
delete args[5]
}
}
let value
for (const index of [3, 4, 5]) {
if (args[index] != null) {
value = args[index]
break
}
}
attrs[i] = {
name: args[1],
value: decode(value)
}
}
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
lastTag = tagName
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) {
start = index
}
if (end == null) {
end = index
}
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
}
// Find the closest opened tag of the same type
if (tagName) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i--) {
if ((i > pos || !tagName) && options.warn) {
options.warn(
('tag <' + (stack[i].tag) + '> has no matching end tag.')
)
}
if (options.end) {
options.end(stack[i].tag, start, end)
}
}
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end)
}
if (options.end) {
options.end(tagName, start, end)
}
}
}
}
function parseComponent (content, options) {
mode = options.mode || 'wx'
env = options.env
filePath = options.filePath
const sfc = {
template: null,
script: null,
json: null,
styles: [],
customBlocks: []
}
let depth = 0
let currentBlock = null
function start (tag, attrs, unary, start, end) {
if (depth === 0) {
currentBlock = {
tag,
content: '',
start: end,
attrs: attrs.reduce(function (cumulated, ref) {
const name = ref.name
const value = ref.value
cumulated[name] = value || true
return cumulated
}, {})
}
if (isSpecialTag(tag)) {
checkAttrs(currentBlock, attrs)
// 带mode的fields只有匹配当前编译mode才会编译
if (tag === 'style') {
if (currentBlock.mode && currentBlock.env) {
if (currentBlock.mode === mode && currentBlock.env === env) {
sfc.styles.push(currentBlock)
}
} else if (currentBlock.mode) {
if (currentBlock.mode === mode) {
sfc.styles.push(currentBlock)
}
} else if (currentBlock.env) {
if (currentBlock.env === env) {
sfc.styles.push(currentBlock)
}
} else {
sfc.styles.push(currentBlock)
}
} else {
if (tag === 'script') {
// 支持type写为application\/json5
if (/^application\/json/.test(currentBlock.type) || currentBlock.name === 'json') {
tag = 'json'
}
if (currentBlock.name === 'json') {
currentBlock.useJSONJS = true
}
}
if (currentBlock.mode && currentBlock.env) {
if (currentBlock.mode === mode && currentBlock.env === env) {
currentBlock.priority = 4
}
} else if (currentBlock.mode) {
if (currentBlock.mode === mode) {
currentBlock.priority = 3
}
} else if (currentBlock.env) {
if (currentBlock.env === env) {
currentBlock.priority = 2
}
} else {
currentBlock.priority = 1
}
if (currentBlock.priority) {
if (!sfc[tag] || sfc[tag].priority <= currentBlock.priority) {
sfc[tag] = currentBlock
}
}
}
} else { // custom blocks
sfc.customBlocks.push(currentBlock)
}
}
if (!unary) {
depth++
}
}
function checkAttrs (block, attrs) {
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (attr.name === 'lang') {
block.lang = attr.value
}
if (attr.name === 'type') {
block.type = attr.value
}
if (attr.name === 'scoped') {
block.scoped = true
}
if (attr.name === 'src') {
block.src = attr.value
}
if (attr.name === 'mode') {
block.mode = attr.value
}
if (attr.name === 'name') {
block.name = attr.value
}
if (attr.name === 'env') {
block.env = attr.value
}
if (attr.name === 'setup') {
block.setup = true
}
}
}
function end (tag, start) {
if (depth === 1 && currentBlock) {
currentBlock.end = start
let text = content.slice(currentBlock.start, currentBlock.end)
// pad content so that linters and pre-processors can output correct
// line numbers in errors and warnings
// stylus编译遇到大量空行时会出现栈溢出,故针对stylus不走pad
if (options.pad && !(currentBlock.tag === 'style' && currentBlock.lang === 'stylus')) {
text = padContent(currentBlock, options.pad) + text
}
currentBlock.content = text
currentBlock = null
}
depth--
}
function padContent (block, pad) {
if (pad === 'space') {
return content.slice(0, block.start).replace(replaceRE, ' ')
} else {
const offset = content.slice(0, block.start).split(splitRE).length
const padChar = '\n'
return Array(offset).join(padChar)
}
}
parseHTML(content, {
start: start,
end: end
})
return sfc
}
function parse (template, options) {
// global var init
warn$1 = options.warn || baseWarn
error$1 = options.error || baseError
mode = options.mode || 'wx'
env = options.env
defs = options.defs || {}
srcMode = options.srcMode || mode
ctorType = options.ctorType
moduleId = options.moduleId
isNative = options.isNative
hasScoped = options.hasScoped
hasVirtualHost = options.hasVirtualHost
isCustomText = options.isCustomText
filePath = options.filePath
i18n = options.i18n
runtimeCompile = options.runtimeCompile
platformGetTagNamespace = options.getTagNamespace || no
refId = 0
injectNodes = []
forScopes = []
forScopesMap = {}
hasI18n = false
i18nInjectableComputed = []
hasOptionalChaining = false
processingTemplate = false
rulesResultMap.clear()
componentGenerics = options.componentGenerics || {}
if (typeof options.usingComponentsInfo === 'string') options.usingComponentsInfo = JSON.parse(options.usingComponentsInfo)
usingComponents = Object.keys(options.usingComponentsInfo)
usingComponentsInfo = options.usingComponentsInfo
const _warn = content => {
const currentElementRuleResult = rulesResultMap.get(currentEl) || rulesResultMap.set(currentEl, {
warnArray: [],
errorArray: []
}).get(currentEl)
currentElementRuleResult.warnArray.push(content)
}
const _error = content => {
const currentElementRuleResult = rulesResultMap.get(currentEl) || rulesResultMap.set(currentEl, {
warnArray: [],
errorArray: []
}).get(currentEl)
currentElementRuleResult.errorArray.push(content)
}
rulesRunner = getRulesRunner({
mode,
srcMode,
type: 'template',
testKey: 'tag',
data: {
usingComponents
},
warn: _warn,
error: _error
})
const stack = []
let root
const meta = {}
if (isCustomText) {
meta.options = meta.options || {}
meta.options.isCustomText = true
}
if (hasVirtualHost) {
meta.options = meta.options || {}
meta.options.virtualHost = true
}
let currentParent
let multiRootError
// 用于记录模板用到的组件,匹配引用组件,看是否有冗余
const tagNames = new Set()
function genTempRoot () {
// 使用临时节点作为root,处理multi root的情况
root = currentParent = getVirtualHostRoot(options, meta)
stack.push(root)
}
parseHTML(template, {
warn: warn$1,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldKeepComment: true,
start: function start (tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
const element = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (isForbiddenTag(element)) {
element.forbidden = true
warn$1(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
'<' + tag + '>' + ', as they will not be parsed.'
)
}
// multi root
if (!currentParent) genTempRoot()
currentParent.children.push(element)
element.parent = currentParent
processElement(element, root, options, meta)
tagNames.add(element.tag)
// 统计通过抽象节点方式使用的组件
element.attrsList.forEach((attr) => {
if (genericRE.test(attr.name)) {
tagNames.add(attr.value)
}
})
if (!unary) {
currentParent = element
stack.push(element)
} else {
element.unary = true
closeElement(element, options, meta)
}
},
end: function end () {
// remove trailing whitespace
const element = stack[stack.length - 1]
if (element) {
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
element.children.pop()
}
// pop stack
stack.pop()
currentParent = stack[stack.length - 1]
closeElement(element, options, meta)
}
},
chars: function chars (text) {
if (!currentParent) genTempRoot()
const children = currentParent.children
if (currentParent.tag !== 'text') {
text = text.trim()
} else {
text = text.trim() ? text : ''
}
if ((!config[mode].wxs || currentParent.tag !== config[mode].wxs.tag) && options.decodeHTMLText) {
text = he.decode(text)
}
if (text) {
const el = {
type: 3,
// 支付宝小程序模板解析中未对Mustache进行特殊处理,无论是否decode都会解析失败,无解,只能支付宝侧进行修复
text: decodeInMustache(text),
parent: currentParent
}
children.push(el)
runtimeCompile ? processTextDynamic(el) : processText(el, options, meta)
}
},
comment: function comment (text) {
if (!currentParent) genTempRoot()
if (options.hasComment || /mpx_config_/.test(text)) {
currentParent.children.push({
type: 3,
text: text,
parent: currentParent,
isComment: true
})
}
}
})
if (multiRootError) {
error$1('Template fields should has one single root, considering wrapping your template content with <view> or <text> tag!')
}
if (hasI18n) {
if (i18nInjectableComputed.length) {
meta.computed = (meta.computed || []).concat(i18nInjectableComputed)
} else {
injectWxs(meta, i18nModuleName, i18nWxsRequest)
}
}
if (hasOptionalChaining) {
injectWxs(meta, optionalChainWxsName, optionalChainWxsPath)
}
injectNodes.forEach((node) => {
addChild(root, node, true)
})
rulesResultMap.forEach((val) => {
Array.isArray(val.warnArray) && val.warnArray.forEach(item => warn$1(item))
Array.isArray(val.errorArray) && val.errorArray.forEach(item => error$1(item))
})
if (!tagNames.has('component') && !tagNames.has('template') && options.checkUsingComponents) {
const arr = []
usingComponents.forEach((item) => {
if (!tagNames.has(item) && !options.globalComponents.includes(item) && !options.componentPlaceholder.includes(item)) {
arr.push(item)
}
})
arr.length && warn$1(`\n ${filePath} \n 组件 ${arr.join(' | ')} 注册了,但是未被对应的模板引用,建议删除!`)
}
return {
root,
meta
}
}
function getTempNode () {
return createASTElement('temp-node')
}
function addChild (parent, newChild, before) {
parent.children = parent.children || []
if (before) {
parent.children.unshift(newChild)
} else {
parent.children.push(newChild)
}
newChild.parent = parent
}
function getAndRemoveAttr (el, name, removeFromMap = true) {
let val, has
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
val = list[i].value
has = true
list.splice(i, 1)
break
}
}
if (removeFromMap && val === el.attrsMap[name]) {
delete el.attrsMap[name]
}
return {
has,
val
}
}
function addAttrs (el, attrs) {
const list = el.attrsList
const map = el.attrsMap
for (let i = 0, l = attrs.length; i < l; i++) {
list.push(attrs[i])
map[attrs[i].name] = attrs[i].value
}
}
function modifyAttr (el, name, val) {
el.attrsMap[name] = val
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list[i].value = val
break
}
}
}
function postMoveBaseDirective (target, source, isDelete = true) {
target.for = source.for
target.if = source.if
target.elseif = source.elseif
target.else = source.else
if (isReact(mode)) {
postProcessForReact(target)
postProcessIfReact(target)
} else if (runtimeCompile) {
postProcessForDynamic(target, config[mode])
postProcessIfDynamic(target, config[mode])
} else {
postProcessFor(target)
postProcessIf(target)
}
if (isDelete) {
delete source.for
delete source.if
delete source.elseif
delete source.else
}
}
function stringify (str) {
if (isWeb(mode) && typeof str === 'string') str = str.replace(/'/g, '"')
return JSON.stringify(str)
}
// function processLifecycleHack (el, options) {
// if (isComponentNode(el,options)) {
// if (el.if) {
// el.if = {
// raw: `{{${el.if.exp} && mpxLifecycleHack}}`,
// exp: `${el.if.exp} && mpxLifecycleHack`
// }
// } else if (el.elseif) {
// el.elseif = {
// raw: `{{${el.elseif.exp} && mpxLifecycleHack}}`,
// exp: `${el.elseif.exp} && mpxLifecycleHack`
// }
// } else if (el.else) {
// el.elseif = {
// raw: '{{mpxLifecycleHack}}',
// exp: 'mpxLifecycleHack'
// }
// delete el.else
// } else {
// el.if = {
// raw: '{{mpxLifecycleHack}}',
// exp: 'mpxLifecycleHack'
// }
// }
// }
// }
const genericRE = /^generic:(.+)$/
function processComponentGenerics (el, meta) {
if (componentGenerics && componentGenerics[el.tag]) {
const generic = dash2hump(el.tag)
el.tag = 'component'
addAttrs(el, [{
name: isWeb(mode) ? ':is' : 'is',
value: isWeb(mode) ? `generic${generic}` : `{{generic${generic}}}`
}])
}
let hasGeneric = false
const genericHash = moduleId
const genericAttrs = []
el.attrsList.forEach((attr) => {
if (genericRE.test(attr.name)) {
genericAttrs.push(attr)
hasGeneric = true
addGenericInfo(meta, genericHash, attr.value)
}
})
// 统一处理所有的generic:属性
genericAttrs.forEach((attr) => {
getAndRemoveAttr(el, attr.name)
addAttrs(el, [{
name: attr.name.replace(':', ''),
value: attr.value
}])
})
if (hasGeneric) {
addAttrs(el, [{
name: 'generichash',
value: genericHash
}])
}
}
function addGenericInfo (meta, genericHash, genericValue) {
if (!meta.genericsInfo) {
meta.genericsInfo = {
hash: genericHash,
map: {}
}
}
meta.genericsInfo.map[genericValue] = true
}
function processComponentIs (el, options) {
if (el.tag !== 'component') {
return
}
const range = getAndRemoveAttr(el, 'range').val
const isInRange = makeMap(range || '')
el.components = (usingComponents).filter(i => {
if (!range) return true
return isInRange(i)
})
if (!el.components.length) {
warn$1('Component in which <component> tag is used must have a non blank usingComponents field')
}
const is = getAndRemoveAttr(el, 'is').val
if (is) {
el.is = parseMustacheWithContext(is).result
} else {
warn$1('<component> tag should have attrs[is].')
}
}
const eventIdentifier = '__mpx_event__'
function parseFuncStr (str, extraStr = '') {
const funcRE = /^(.*{{.+}}[^()]*|[^()]+)(\((.*)\))?/
const match = funcRE.exec(str)
if (match) {
const funcName = parseMustacheWithContext(match[1]).result
const hasArgs = !!match[2]
let args = match[3] ? `,${match[3]}` : ''
const ret = /(,|^)\s*(\$event)\s*(,|$)/.exec(args)
if (ret) {
const subIndex = ret[0].indexOf('$event')
if (subIndex) {
const index1 = ret.index + subIndex
const index2 = index1 + 6
args = args.substring(0, index1) + stringify(eventIdentifier) + args.substring(index2)
}
}
return {
hasArgs,
expStr: `[${funcName + args + extraStr}]`
}
}
}
function stringifyWithResolveComputed (modelValue) {
const result = []
let inString = false
const computedStack = []
let fragment = ''
for (let i = 0; i < modelValue.length; i++) {
const char = modelValue[i]
if (inString) {
if (char === inString) {
inString = false
}
} else if (char === '"' || char === '\'') {
inString = char
} else if (char === '[') {
computedStack.push(char)
if (computedStack.length === 1) {
fragment += '.'
result.push(stringify(fragment))
fragment = ''
continue
}
} else if (computedStack.length) {
if (char === ']') {
computedStack.pop()
if (computedStack.length === 0) {
result.push(fragment)
fragment = ''
continue
}
}
}
fragment += char
}
if (fragment !== '') {
result.push(stringify(fragment))
}
return result.join('+')
}
function processStyleReact (el, options) {
// process class/wx:class/style/wx:style/wx:show for react native
const dynamicClass = getAndRemoveAttr(el, config[mode].directive.dynamicClass).val
let staticClass = getAndRemoveAttr(el, 'class').val || ''
staticClass = staticClass.replace(/\s+/g, ' ')
const dynamicStyle = getAndRemoveAttr(el, config[mode].directive.dynamicStyle).val
let staticStyle = getAndRemoveAttr(el, 'style').val || ''
staticStyle = staticStyle.replace(/\s+/g, ' ')
const { val: show, has } = getAndRemoveAttr(el, config[mode].directive.show)
if (has && show === undefined) {
error$1(`Attrs ${config[mode].directive.show} should have a value `)
}
if (dynamicClass || staticClass || dynamicStyle || staticStyle || show) {
const staticClassExp = parseMustacheWithContext(staticClass).result
const dynamicClassExp = parseMustacheWithContext(dynamicClass).result
const staticStyleExp = parseMustacheWithContext(staticStyle).result
const dynamicStyleExp = parseMustacheWithContext(dynamicStyle).result
const showExp = parseMustacheWithContext(show).result
addAttrs(el, [{
name: 'style',
// runtime helper
value: `{{this.__getStyle(${staticClassExp}, ${dynamicClassExp}, ${staticStyleExp}, ${dynamicStyleExp}${show === undefined ? '' : `, !(${showExp})`})}}`
}])
}
if (specialClassReg.test(el.tag)) {
const staticClassNames = ['hover', 'indicator', 'mask', 'placeholder']
staticClassNames.forEach((className) => {
let staticClass = el.attrsMap[className + '-class'] || ''
let staticStyle = getAndRemoveAttr(el, className + '-style').val || ''
staticClass = staticClass.replace(/\s+/g, ' ')
staticStyle = staticStyle.replace(/\s+/g, ' ')
if ((staticClass && staticClass !== 'none') || staticStyle) {
const staticClassExp = parseMustacheWithContext(staticClass).result
const staticStyleExp = parseMustacheWithContext(staticStyle).result
addAttrs(el, [{
name: className + '-style',
value: `{{this.__getStyle(${staticClassExp}, null, ${staticStyleExp})}}`
}])
}
})
}
// 处理externalClasses,将其转换为style作为props传递
if (options.externalClasses) {
options.externalClasses.forEach((className) => {
let externalClass = getAndRemoveAttr(el, className).val || ''
externalClass = externalClass.replace(/\s+/g, ' ')
if (externalClass) {
const externalClassExp = parseMustacheWithContext(externalClass).result
addAttrs(el, [{
name: className,
value: `{{this.__getStyle(${externalClassExp})}}`
}])
}
})
}
}
function getModelConfig (el, match) {
const modelProp = getAndRemoveAttr(el, config[mode].directive.modelProp).val || config[mode].event.defaultModelProp
const modelEvent = getAndRemoveAttr(el, config[mode].directive.modelEvent).val || config[mode].event.defaultModelEvent
const modelValuePathRaw = getAndRemoveAttr(el, config[mode].directive.modelValuePath).val
const modelValuePath = modelValuePathRaw === undefined ? config[mode].event.defaultModelValuePath : modelValuePathRaw
const modelFilter = getAndRemoveAttr(el, config[mode].directive.modelFilter).val
let modelValuePathArr
try {
modelValuePathArr = JSON5.parse(modelValuePath)
} catch (e) {
if (modelValuePath === '') {
modelValuePathArr = []
} else {
modelValuePathArr = modelValuePath.split('.')
}
}
const modelValue = match[1].trim()
const stringifiedModelValue = stringifyWithResolveComputed(modelValue)
return {
modelProp,
modelEvent,
modelFilter,
modelValuePathArr,
stringifiedModelValue
}
}
function processEventWeb (el) {
const eventConfigMap = {}
el.attrsList.forEach(function ({ name, value }) {
if (/^@[a-zA-Z]+$/.test(name)) {
const parsedFunc = parseFuncStr(value)
if (parsedFunc) {
if (!eventConfigMap[name]) {
eventConfigMap[name] = {
configs: []
}
}
eventConfigMap[name].configs.push(
Object.assign({ name, value }, parsedFunc)
)
}
}
})
// let wrapper
for (const name in eventConfigMap) {
const { configs } = eventConfigMap[name]
if (!configs.length) continue
configs.forEach(({ name }) => {
if (name) {
// 清空原始事件绑定
let has
do {
has = getAndRemoveAttr(el, name).has
} while (has)
}
})
const value = `(e)=>__invoke(e, [${configs.map(
(item) => item.expStr
)}])`
addAttrs(el, [
{
name,
value
}
])
}
}
function processEventReact (el, options) {
const eventConfigMap = {}
el.attrsList.forEach(function ({ name, value }) {
const parsedEvent = config[mode].event.parseEvent(name)
if (parsedEvent) {
const type = config[mode].event.getEvent(parsedEvent.eventName, parsedEvent.prefix)
const modifiers = (parsedEvent.modifier || '').split('.')
const parsedFunc = parseFuncStr(value)
if (parsedFunc) {
if (!eventConfigMap[type]) {
eventConfigMap[type] = {
configs: []
}
}
eventConfigMap[type].configs.push(Object.assign({ name, value }, parsedFunc))
if (modifiers.indexOf('proxy') > -1 || options.forceProxyEvent) {
eventConfigMap[type].proxy = true
}
}
}
})
const modelExp = getAndRemoveAttr(el, config[mode].directive.model).val
if (modelExp) {
const match = tagRE.exec(modelExp)
if (match) {
const { modelProp, modelEvent, modelFilter, modelValuePathArr, stringifiedModelValue } = getModelConfig(el, match)
if (!isValidIdentifierStr(modelEvent)) {
warn$1(`EventName ${modelEvent} which is used in ${config[mode].directive.model} must be a valid identifier!`)
return
}
// if (forScopes.length) {
// stringifiedModelValue = stringifyWithResolveComputed(modelValue)
// } else {
// stringifiedModelValue = stringify(modelValue)
// }
// todo 未来可能需要支持类似modelEventPrefix这样的配置来声明model事件的绑定方式
const modelEventType = config[mode].event.getEvent(modelEvent)
if (!eventConfigMap[modelEventType]) {
eventConfigMap[modelEventType] = {
configs: []
}
}
eventConfigMap[modelEventType].configs.unshift({
hasArgs: true,
expStr: `[${stringify('__model')},${stringifiedModelValue},${stringify(eventIdentifier)},${stringify(modelValuePathArr)}${modelFilter ? `,${stringify(modelFilter)}` : ''}]`
})
addAttrs(el, [
{
name: modelProp,
value: modelExp
}
])
}
}
// let wrapper
for (const type in eventConfigMap) {
const { configs, proxy } = eventConfigMap[type]
if (!configs.length) continue
const needBind = proxy || configs.length > 1 || configs[0].hasArgs
if (needBind) {
configs.forEach(({ name }) => {
if (name) {
// 清空原始事件绑定
let has
do {
has = getAndRemoveAttr(el, name).has
} while (has)
}
})
const value = `{{(e)=>this.__invoke(e, [${configs.map(item => item.expStr)}])}}`
addAttrs(el, [
{
name: type,
value
}
])
} else {
const { name, value } = configs[0]
const attrValue = isValidIdentifierStr(value)
? `{{this.${value}}}`
: `{{this[${parseMustacheWithContext(value).result}]}}`
modifyAttr(el, name, attrValue)
}
// 非button的情况下,press/longPress时间需要包裹TouchableWithoutFeedback进行响应,后续可支持配置
// if ((type === 'press' || type === 'longPress') && el.tag !== 'mpx-button') {
// if (!wrapper) {
// wrapper = createASTElement('TouchableWithoutFeedback')
// wrapper.isBuiltIn = true
// processBuiltInComponents(wrapper, meta)
// }
// addAttrs(el, [
// {
// name,
// value
// }
// ])
// } else {
// addAttrs(el, [
// {
// name,
// value
// }
// ])
// }
}
// if (wrapper) {
// replaceNode(el, wrapper, true)
// addChild(wrapper, el)
// processAttrs(wrapper, options)
// postMoveBaseDirective(wrapper, el)
// }
}
function processEvent (el, options) {
const eventConfigMap = {}
el.attrsList.forEach(function ({ name, value }) {
const parsedEvent = config[mode].event.parseEvent(name)
if (parsedEvent) {
const type = parsedEvent.eventName
const modifiers = (parsedEvent.modifier || '').split('.')
const prefix = parsedEvent.prefix
// catch 场景下,下发的 eventconfig 里面包含特殊字符,用以运行时的判断
const extraStr = runtimeCompile && prefix === 'catch' ? `, "__mpx_${prefix}"` : ''
const parsedFunc = parseFuncStr(value, extraStr)
if (parsedFunc) {
if (!eventConfigMap[type]) {
eventConfigMap[type] = {
configs: []
}
}
eventConfigMap[type].configs.push(Object.assign({ name }, parsedFunc))
if (modifiers.indexOf('proxy') > -1 || options.forceProxyEvent) {
eventConfigMap[type].proxy = true
}
}
}
})
const modelExp = getAndRemoveAttr(el, config[mode].directive.model).val
if (modelExp) {
const match = tagRE.exec(modelExp)
if (match) {
const { modelProp, modelEvent, modelFilter, modelValuePathArr, stringifiedModelValue } = getModelConfig(el, match)
if (!isValidIdentifierStr(modelEvent)) {
warn$1(`EventName ${modelEvent} which is used in ${config[mode].directive.model} must be a valid identifier!`)
return
}
// if (forScopes.length) {
// stringifiedModelValue = stringifyWithResolveComputed(modelValue)
// } else {
// stringifiedModelValue = stringify(modelValue)
// }
if (!eventConfigMap[modelEvent]) {
eventConfigMap[modelEvent] = {
configs: []
}
}
eventConfigMap[modelEvent].configs.unshift({
hasArgs: true,
expStr: `[${stringify('__model')},${stringifiedModelValue},${stringify(eventIdentifier)},${stringify(modelValuePathArr)},${stringify(modelFilter)}]`
})
addAttrs(el, [
{
name: modelProp,
value: modelExp
}
])
}
}
for (const type in eventConfigMap) {
let needBind = false
const { configs, proxy } = eventConfigMap[type]
delete eventConfigMap[type]
if (proxy) {
needBind = true
} else if (configs.length > 1) {
needBind = true
} else if (configs.length === 1) {
needBind = !!configs[0].hasArgs
}
const escapedType = dash2hump(type)
// 排除特殊情况
if (!isValidIdentifierStr(escapedType)) {
warn$1(`EventName ${type} which need be framework proxy processed must be a valid identifier!`)
needBind = false
}
if (needBind) {
let resultName
configs.forEach(({ name }) => {
if (name) {
// 清空原始事件绑定
let has
do {
has = getAndRemoveAttr(el, name).has
} while (has)
if (!resultName) {
// 清除修饰符
resultName = name.replace(/\..*/, '')
}
}
})
addAttrs(el, [
{
name: resultName || config[mode].event.getEvent(type),
value: '__invoke'
}
])
eventConfigMap[escapedType] = configs.map((item) => {
return item.expStr
})
}
}
if (!isEmptyObject(eventConfigMap)) {
addAttrs(el, [{
name: 'data-eventconfigs',
value: `{{${shallowStringify(eventConfigMap, true)}}}`
}])
}
}
function processSlotReact (el, meta) {
if (el.tag === 'slot') {
el.slot = {
name: getAndRemoveAttr(el, 'name').val,
slot: getAndRemoveAttr(el, 'slot').val
}
meta.options = meta.options || {}
meta.options.disableMemo = true
}
}
function wrapMustache (val) {
return val && !tagRE.test(val) ? `{{${val}}}` : val
}
function parseOptionalChaining (str) {
const wxsName = `${optionalChainWxsName}.g`
let optionsRes
while (optionsRes = /\?\./.exec(str)) {
const strLength = str.length
const grammarMap = {
init () {
const initKey = [
{
mapKey: '[]',
mapValue: [
{
key: '[',
value: 1
},
{
key: ']',
value: -1
}
]
},
{
mapKey: '()',
mapValue: [
{
key: '(',
value: 1
},
{
key: ')',
value: -1
}
]
}
]
this.count = {}
initKey.forEach(({ mapKey, mapValue }) => {
mapValue.forEach(({ key, value }) => {
this[key] = this.changeState(mapKey, value)
})
})
},
changeState (key, value) {
if (!this.count[key]) {
this.count[key] = 0
}
return () => {
this.count[key] = this.count[key] + value
return this.count[key]
}
},
checkState () {
return Object.values(this.count).find(i => i)
}
}
let leftIndex = optionsRes.index
let rightIndex = leftIndex + 2
let haveNotGetValue = true
let chainValue = ''
let chainKey = ''
let notCheck = false
grammarMap.init()
// 查 ?. 左边值
while (haveNotGetValue && leftIndex > 0) {
const left = str[leftIndex - 1]
const grammar = grammarMap[left]
if (notCheck) {
// 处于表达式内
chainValue = left + chainValue
if (grammar) {
grammar()
if (!grammarMap.checkState()) {
// 表达式结束
notCheck = false
}
}
} else if (~[']', ')'].indexOf(left)) {
// 命中表达式,开始记录表达式
chainValue = left + chainValue
notCheck = true
grammar()
} else if (left !== ' ') {
if (!/[A-Za-z0-9_$.]/.test(left)) {
// 结束
haveNotGetValue = false
leftIndex++
} else {
// 正常语法
chainValue = left + chainValue
}
}
leftIndex--
}
if (grammarMap.checkState() && haveNotGetValue) {
// 值查找结束但是语法未闭合或者处理到边界还未结束,抛异常
throw new Error('[optionChain] option value illegal!!!')
}
haveNotGetValue = true
let keyValue = ''
// 查 ?. 右边key
while (haveNotGetValue && rightIndex < strLength) {
const right = str[rightIndex]
const grammar = grammarMap[right]
if (notCheck) {
// 处于表达式内
if (grammar) {
grammar()
if (grammarMap.checkState()) {
keyValue += right
} else {
// 表达式结束
notCheck = false
chainKey += `,${keyValue}`
keyValue = ''
}
} else {
keyValue += right
}
} else if (~['[', '('].indexOf(right)) {
// 命中表达式,开始记录表达式
grammar()
if (keyValue) {
chainKey += `,'${keyValue}'`
keyValue = ''
}
notCheck = true
} else if (!/[A-Za-z0-9_$.?]/.test(right)) {
// 结束
haveNotGetValue = false
rightIndex--
} else if (right !== '?') {
// 正常语法
if (right === '.') {
if (keyValue) {
chainKey += `,'${keyValue}'`
}
keyValue = ''
} else {
keyValue += right
}
}
rightIndex++
}
if (grammarMap.checkState() && haveNotGetValue) {
// key值查找结束但是语法未闭合或者处理到边界还未结束,抛异常
throw new Error('[optionChain] option key illegal!!!')
}
if (keyValue) {
chainKey += `,'${keyValue}'`
}
str = str.slice(0, leftIndex) + `${wxsName}(${chainValue},[${chainKey.slice(1)}])` + str.slice(rightIndex)
if (!hasOptionalChaining) {
hasOptionalChaining = true
}
}
return str
}
function parseMustacheWithContext (raw = '') {
return parseMustache(raw, (exp) => {
if (defs) {
// eval处理的话,和别的判断条件,比如运行时的判断混用情况下得不到一个结果,还是正则替换
const defKeys = Object.keys(defs)
defKeys.forEach((defKey) => {
const defRE = new RegExp(`\\b${defKey}\\b`)
const defREG = new RegExp(`\\b${defKey}\\b`, 'g')
if (defRE.test(exp)) {
exp = exp.replace(defREG, stringify(defs[defKey]))
}
})
}
// 处理可选链表达式
exp = parseOptionalChaining(exp)
if (i18n) {
for (const i18nFuncName of i18nFuncNames) {
const funcNameRE = new RegExp(`(?<![A-Za-z0-9_$.])${i18nFuncName}\\(`)
const funcNameREG = new RegExp(`(?<![A-Za-z0-9_$.])${i18nFuncName}\\(`, 'g')
if (funcNameRE.test(exp)) {
if (i18n.useComputed || !i18nFuncName.startsWith('\\$')) {
const i18nInjectComputedKey = `_i${i18nInjectableComputed.length + 1}`
i18nInjectableComputed.push(`${i18nInjectComputedKey} () {\nreturn ${exp.trim()}}`)
exp = i18nInjectComputedKey
} else {
exp = exp.replace(funcNameREG, `${i18nModuleName}.$1(null, _l, _fl, `)
}
hasI18n = true
break
}
}
}
return exp
})
}
function parseMustache (raw = '', expHandler = exp => exp, strHandler = str => str) {
let replaced = false
if (tagRE.test(raw)) {
const ret = []
let lastLastIndex = 0
let match
while (match = tagREG.exec(raw)) {
const pre = raw.substring(lastLastIndex, match.index)
if (pre) {
const pre2 = strHandler(pre)
if (pre2 !== pre) replaced = true
if (pre2) ret.push(stringify(pre2))
}
const exp = match[1].trim()
if (exp) {
const exp2 = expHandler(exp)
if (exp2 !== exp) replaced = true
if (exp2) ret.push(`(${exp2})`)
}
lastLastIndex = tagREG.lastIndex
}
const post = raw.substring(lastLastIndex)
if (post) {
const post2 = strHandler(post)
if (post2 !== post) replaced = true
if (post2) ret.push(stringify(post2))
}
let result
if (ret.length === 1) {
result = ret[0]
} else {
result = `(${ret.join('+')})`
}
return {
result,
hasBinding: true,
val: replaced ? `{{${result}}}` : raw,
replaced
}
}
const raw2 = strHandler(raw)
if (raw2 !== raw) replaced = true
return {
result: stringify(raw2),
hasBinding: false,
val: raw2,
replaced
}
}
function addExp (el, exp, isProps, attrName) {
if (exp) {
if (!el.exps) {
el.exps = []
}
el.exps.push({ exp, isProps, attrName })
}
}
function processIf (el) {
let val = getAndRemoveAttr(el, config[mode].directive.if).val
if (val) {
if (mode === 'swan') val = wrapMustache(val)
const parsed = parseMustacheWithContext(val)
el.if = {
raw: parsed.val,
exp: parsed.result
}
} else if (val = getAndRemoveAttr(el, config[mode].directive.elseif).val) {
if (mode === 'swan') val = wrapMustache(val)
const parsed = parseMustacheWithContext(val)
el.elseif = {
raw: parsed.val,
exp: parsed.result
}
} else if (getAndRemoveAttr(el, config[mode].directive.else).has) {
el.else = true
}
}
function processIfWeb (el) {
let val = getAndRemoveAttr(el, config[mode].directive.if).val
if (val) {
el.if = {
raw: val,
exp: val
}
} else if (val = getAndRemoveAttr(el, config[mode].directive.elseif).val) {
el.elseif = {
raw: val,
exp: val
}
} else if (getAndRemoveAttr(el, config[mode].directive.else).has) {
el.else = true
}
}
const swanForInRe = /^\s*(\w+)(?:\s*,\s*(\w+))?\s+in\s+(\S+)(?:\s+trackBy\s+(\S+))?\s*$/
function processFor (el) {
let val = getAndRemoveAttr(el, config[mode].directive.for).val
if (val) {
let matched
if (mode === 'swan' && (matched = swanForInRe.exec(val))) {
el.for = {
raw: val,
exp: matched[3],
item: matched[1] || 'item',
index: matched[2] || 'index'
}
} else {
if (mode === 'swan') val = wrapMustache(val)
const parsed = parseMustacheWithContext(val)
el.for = {
raw: parsed.val,
exp: parsed.result
}
if (val = getAndRemoveAttr(el, config[mode].directive.forIndex).val) {
el.for.index = val
}
if (val = getAndRemoveAttr(el, config[mode].directive.forItem).val) {
el.for.item = val
}
if (val = getAndRemoveAttr(el, config[mode].directive.key).val) {
el.for.key = val
}
}
pushForScopes({
index: el.for.index || 'index',
item: el.for.item || 'item'
})
}
}
function processRefReact (el, meta) {
const { val, has } = getAndRemoveAttr(el, config[mode].directive.ref)
// rn中只有内建组件