UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

1,806 lines (1,658 loc) 91.3 kB
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 = { '&lt;': '<', '&gt;': '>', '&quot;': '"', '&amp;': '&', '&#39;': '\'' } 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中只有内建组件