ast/generateElementId.js

/**
 * ast 编译相关
 * @module ast
 */

const { parse } = require('@babel/parser')
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default

/**
 * @static
 * @description 解析获得特定字符之间的表达式,例如 "{{}}", "{}"
 * @example
 * getExpression("{{a+1}}{{b+1}}", "{{", "}}")
 * // ['a+1','b+1']
 * @param {String} content
 * @param {String} startFlag
 * @param {String} endFlag
 * @return {Array} 表达式数组
 */

function getExpression (content, startFlag, endFlag) {
    let isSingleQuotes = false
    let isDoubleQuotes = false
    let isTemplateStr = false
    const singleQuotes = '\''
    const doubleQuotes = '"'
    const templateStr = '`'

    const canStart = (open) =>
        open === startFlag && !isSingleQuotes && !isDoubleQuotes && !isTemplateStr
    const canEnd = (close) =>
        close === endFlag && !isSingleQuotes && !isDoubleQuotes && !isTemplateStr

    const isQuotes = function (s) {
        return [singleQuotes, doubleQuotes, templateStr].includes(s)
    }

    const expressions = []
    let current = ''
    let isExpression = false

    for (let i = 0; i < content.length; i++) {
        const c = content[i]
        const n = content[i + 1]

        if (c === '\\' && isQuotes(n)) {
            i++
            if (isExpression) {
                current += c + n
            }
            continue
        } else if (c === singleQuotes) {
            isSingleQuotes = !isSingleQuotes
        } else if (c === doubleQuotes) {
            isDoubleQuotes = !isDoubleQuotes
        } else if (c === templateStr) {
            isTemplateStr = !isTemplateStr
        } else if (canStart(c + n)) {
            isExpression = true
            i++
            continue
        } else if (canEnd(c + n)) {
            expressions.push(current)
            current = ''
            isExpression = false
            i++
        }

        if (isExpression) {
            current += c
        }
    }

    return expressions
}

/**
 * @static
 * @ignore
 * @description 替换 js 表达式中的 Identifier 节点 name
 * @example
 * replaceIdentifierName("a+b+1", [a, b], "[c, d]")
 * // c+d+1
 * @param {string} expression js 表达式
 * @param {array} nflgas 新 Identifier 节点名称集合
 * @param {array} oflags 旧 Identifier 节点名称集合
 * @return {string} 替换后的 js 表达式
 */

function replaceIdentifierName (expression, nflgas, oflags) {
    if (!nflgas || !nflgas.length) {
        return
    }

    let ast = null

    // ast树
    try {
        ast = parse(expression)
    } catch (error) {
        throw new Error(`表达式 ${expression} 解析出错`)
    }

    traverse(ast, {
        enter (path) {
            // 是对象属性时跳过该节点
            if (path.isMemberExpression() && !path.node.computed) {
                path.skip()
            }

            nflgas.forEach((nflag, index) => {
                const oflag = oflags[index]

                if (path.isIdentifier({
                    name: oflag
                })) {
                    path.node.name = nflag
                }
            })
        }
    })

    // 对于微信小程序来说
    // 输出时,否则小程序将解析失败
    return generate(ast, {
        compact: true
    }).code.slice(0, -1)
}

function handleExpression (text, nIndexs, oIndexs) {
    if (!text || !nIndexs || !nIndexs.length) {
        return text
    }

    const changedNewIndexs = []
    const changedOldIndex = []
    const lowerIndexs = []

    for (let i = nIndexs.length - 1; i >= 0; i--) {
        const nIndex = nIndexs[i]
        const oIndex = oIndexs[i]

        if (nIndex !== oIndex && !lowerIndexs.includes(nIndex)) {
            changedNewIndexs.push(nIndex)
            changedOldIndex.push(oIndex)
        }
        lowerIndexs.push(nIndex)
    }

    if (!changedNewIndexs.length) {
        return text
    }

    const expressions = getExpression(text, '{{', '}}')

    if (!expressions.length) {
        return text
    }

    let resText = text

    expressions.forEach((expression) => {
        const exp = replaceIdentifierName(
            expression,
            changedNewIndexs,
            changedOldIndex,
            text,
            expressions
        )

        // exp 中不要带 $
        // 在 replace 函数中,替代字符串中 $ 有特殊含义
        resText = resText.replace(expression, exp)
    })

    return resText
}

class HandleAttribs {
    constructor (attribs) {
        this.attribs = attribs || {}
    }

    get (key) {
        return this.attribs[key]
    }

    delete (key) {
        delete this.attribs[key]
    }

    push (obj) {
        Object.assign(this.attribs, obj)
    }

    set (key, value) {
        this.attribs[key] = value
    }
}

// 获取节点在父节点下的索引
function getElementIndex (element) {
    let index = 0
    let prev = element.prev

    while (prev) {
        if (prev.type === 'tag') {
            index++
        }
        prev = prev.prev
    }

    return index
}

// 获取节点在父节点下的唯一标志
function getlementUniqueFlagInParent (element, uniqueFlagAttr) {
    const parent = element.parent
    let parentUniqueFlag = ''

    if (parent) {
        parentUniqueFlag = parent.attribs[uniqueFlagAttr]
    }

    const index = getElementIndex(element)

    function getName () {
        return element.name || ''
    }

    // 父节点的 uniqueFlag + tagName + element 在父节点下的索引
    if (parentUniqueFlag) {
        return `${parentUniqueFlag}--${getName()}${index}`
    }

    // tagName + 索引
    return `${getName()}${index}`
}

function assembleUniqueId (keyElement) {
    return keyElement.reduce((prev, key) => {
        if (key) {
            if (prev) {
                return prev + '_' + key
            }

            return prev + key
        }
        return prev

    }, '')
}

/**
 * @static
 * @description
 * 为微信小程序 dom 节点生成唯一标志,存储在特定 data-[name] 下。
 * 节点唯一标志 = 父节点唯一标志 + 在父节点下的索引 + 标签名 + 节点本身id
 * 根节点唯一标志 = 节点唯一标志 + 页面path
 * wx:for 节点唯一标志 = 节点唯一标志 + index
 * @example
 * generateElementUniqueFlag(dom, {
        indexPrefix: 'index',
        flagKey: 'uid',
        filePath: 'C:\\Users\xx\Desktop\project\src\util\page.wxml'
    })
 * @param {Object} dom htmlParser2 解析后得到的 dom ast 树
 * @param {Object} options 配置项
 * @param {string} [options.indexPrefix=index] wx:for-index 值得前缀,会主动给 wx:for 节点设置 wx:for-index
 * @param {string} [options.flagKey=uflag] data-[flagKey] 存储唯一 id
 * @param {string} [options.filePath=''] 文件路径,多个文件是,需要加上文件路径才能保证每个节点 id 唯一
 */

function generateElementUniqueFlag (dom, options = {}) {
    const defalutOptions = {
        indexPrefix: 'index',
        flagKey: 'uflag',
        filePath: ''
    }
    const currentOptions = Object.assign({}, defalutOptions, options)
    const { indexPrefix, flagKey, filePath } = currentOptions
    // 存储嵌套index的栈
    const nIndexs = []
    // 存储wx:for节点的栈
    const loopNodes = []
    // 存储旧的index的栈
    const oIndexs = []
    let currentIndex = ''
    let deep = 0
    let isRoot = true

    function travel (dom) {
        dom.forEach((element) => {
            const attribs = element.attribs

            if (attribs) {
                const attrs = new HandleAttribs(attribs)

                if (attrs.get('wx:for')) {
                    if (attrs.get('wx:for-index')) {
                        const index = attrs.get('wx:for-index')

                        nIndexs.push(index)
                        oIndexs.push(index)
                        currentIndex = index
                    } else {
                        const index = `${indexPrefix}_${deep}`

                        // 主动设置wx:for-index
                        attrs.set('wx:for-index', index)
                        nIndexs.push(index)
                        oIndexs.push('index')
                        currentIndex = index
                    }
                    loopNodes.push(element)
                    deep++
                }

                // 为每个节点注入唯一标志
                const attr = `data-${flagKey}`
                // 在父节点下的唯一标志(与父节点唯一标志、标签名、在父节点下的位置索引有关)
                const uniqueFlagInParent = getlementUniqueFlagInParent(
                    element,
                    attr
                )
                // wx:for 组件内唯一标志与上层所有 index 有关
                // 当发现 wx:for 时,该节点唯一标志与 index 有关
                // 这样其所有子节点唯一标志都将与这个 index 有关
                // 这样即使是嵌套循环,也能保证内部节点唯一标志与上层每个 wx:for 相关
                const indexsStr = currentIndex ? `{{${currentIndex}}}` : ''
                let keys = []

                // 多个wxml文件时,还需在根节点加上文件路径,才能确保每个元素生成的标志唯一
                if (isRoot) {
                    keys.push(filePath)
                }
                keys = [
                    ...keys,
                    uniqueFlagInParent,
                    indexsStr,
                    attrs.get('id')
                ]
                const uniqueFlag = assembleUniqueId(keys)
                const obj = {
                    [attr]: uniqueFlag
                }

                attrs.push(obj)

                // 处理for+template标签组合
                if (element.name === 'template') {
                    // 太麻烦,暂未处理
                    // doSomething
                } else {
                    Object.keys(attribs).forEach((key) => {
                        attribs[key] = handleExpression(
                            attribs[key],
                            nIndexs,
                            oIndexs
                        )
                    })
                }
            } else if (element.type === 'text') {
                element.data = handleExpression(element.data, nIndexs, oIndexs)
            }

            if (element.children) {
                isRoot = false
                currentIndex = ''
                travel(element.children)
            }

            // 回溯到wx:for节点时
            if (element === loopNodes[loopNodes.length - 1]) {
                nIndexs.pop()
                oIndexs.pop()
                loopNodes.pop()
                deep--
            }
        })
    }

    travel(dom)
}

module.exports = {
    generateElementUniqueFlag,
    getExpression
}