ast/generateElementId.js

  1. /**
  2. * ast 编译相关
  3. * @module ast
  4. */
  5. const { parse } = require('@babel/parser')
  6. const generate = require('@babel/generator').default
  7. const traverse = require('@babel/traverse').default
  8. /**
  9. * @static
  10. * @description 解析获得特定字符之间的表达式,例如 "{{}}", "{}"
  11. * @example
  12. * getExpression("{{a+1}}{{b+1}}", "{{", "}}")
  13. * // ['a+1','b+1']
  14. * @param {String} content
  15. * @param {String} startFlag
  16. * @param {String} endFlag
  17. * @return {Array} 表达式数组
  18. */
  19. function getExpression (content, startFlag, endFlag) {
  20. let isSingleQuotes = false
  21. let isDoubleQuotes = false
  22. let isTemplateStr = false
  23. const singleQuotes = '\''
  24. const doubleQuotes = '"'
  25. const templateStr = '`'
  26. const canStart = (open) =>
  27. open === startFlag && !isSingleQuotes && !isDoubleQuotes && !isTemplateStr
  28. const canEnd = (close) =>
  29. close === endFlag && !isSingleQuotes && !isDoubleQuotes && !isTemplateStr
  30. const isQuotes = function (s) {
  31. return [singleQuotes, doubleQuotes, templateStr].includes(s)
  32. }
  33. const expressions = []
  34. let current = ''
  35. let isExpression = false
  36. for (let i = 0; i < content.length; i++) {
  37. const c = content[i]
  38. const n = content[i + 1]
  39. if (c === '\\' && isQuotes(n)) {
  40. i++
  41. if (isExpression) {
  42. current += c + n
  43. }
  44. continue
  45. } else if (c === singleQuotes) {
  46. isSingleQuotes = !isSingleQuotes
  47. } else if (c === doubleQuotes) {
  48. isDoubleQuotes = !isDoubleQuotes
  49. } else if (c === templateStr) {
  50. isTemplateStr = !isTemplateStr
  51. } else if (canStart(c + n)) {
  52. isExpression = true
  53. i++
  54. continue
  55. } else if (canEnd(c + n)) {
  56. expressions.push(current)
  57. current = ''
  58. isExpression = false
  59. i++
  60. }
  61. if (isExpression) {
  62. current += c
  63. }
  64. }
  65. return expressions
  66. }
  67. /**
  68. * @static
  69. * @ignore
  70. * @description 替换 js 表达式中的 Identifier 节点 name
  71. * @example
  72. * replaceIdentifierName("a+b+1", [a, b], "[c, d]")
  73. * // c+d+1
  74. * @param {string} expression js 表达式
  75. * @param {array} nflgas 新 Identifier 节点名称集合
  76. * @param {array} oflags 旧 Identifier 节点名称集合
  77. * @return {string} 替换后的 js 表达式
  78. */
  79. function replaceIdentifierName (expression, nflgas, oflags) {
  80. if (!nflgas || !nflgas.length) {
  81. return
  82. }
  83. let ast = null
  84. // ast树
  85. try {
  86. ast = parse(expression)
  87. } catch (error) {
  88. throw new Error(`表达式 ${expression} 解析出错`)
  89. }
  90. traverse(ast, {
  91. enter (path) {
  92. // 是对象属性时跳过该节点
  93. if (path.isMemberExpression() && !path.node.computed) {
  94. path.skip()
  95. }
  96. nflgas.forEach((nflag, index) => {
  97. const oflag = oflags[index]
  98. if (path.isIdentifier({
  99. name: oflag
  100. })) {
  101. path.node.name = nflag
  102. }
  103. })
  104. }
  105. })
  106. // 对于微信小程序来说
  107. // 输出时,否则小程序将解析失败
  108. return generate(ast, {
  109. compact: true
  110. }).code.slice(0, -1)
  111. }
  112. function handleExpression (text, nIndexs, oIndexs) {
  113. if (!text || !nIndexs || !nIndexs.length) {
  114. return text
  115. }
  116. const changedNewIndexs = []
  117. const changedOldIndex = []
  118. const lowerIndexs = []
  119. for (let i = nIndexs.length - 1; i >= 0; i--) {
  120. const nIndex = nIndexs[i]
  121. const oIndex = oIndexs[i]
  122. if (nIndex !== oIndex && !lowerIndexs.includes(nIndex)) {
  123. changedNewIndexs.push(nIndex)
  124. changedOldIndex.push(oIndex)
  125. }
  126. lowerIndexs.push(nIndex)
  127. }
  128. if (!changedNewIndexs.length) {
  129. return text
  130. }
  131. const expressions = getExpression(text, '{{', '}}')
  132. if (!expressions.length) {
  133. return text
  134. }
  135. let resText = text
  136. expressions.forEach((expression) => {
  137. const exp = replaceIdentifierName(
  138. expression,
  139. changedNewIndexs,
  140. changedOldIndex,
  141. text,
  142. expressions
  143. )
  144. // exp 中不要带 $
  145. // 在 replace 函数中,替代字符串中 $ 有特殊含义
  146. resText = resText.replace(expression, exp)
  147. })
  148. return resText
  149. }
  150. class HandleAttribs {
  151. constructor (attribs) {
  152. this.attribs = attribs || {}
  153. }
  154. get (key) {
  155. return this.attribs[key]
  156. }
  157. delete (key) {
  158. delete this.attribs[key]
  159. }
  160. push (obj) {
  161. Object.assign(this.attribs, obj)
  162. }
  163. set (key, value) {
  164. this.attribs[key] = value
  165. }
  166. }
  167. // 获取节点在父节点下的索引
  168. function getElementIndex (element) {
  169. let index = 0
  170. let prev = element.prev
  171. while (prev) {
  172. if (prev.type === 'tag') {
  173. index++
  174. }
  175. prev = prev.prev
  176. }
  177. return index
  178. }
  179. // 获取节点在父节点下的唯一标志
  180. function getlementUniqueFlagInParent (element, uniqueFlagAttr) {
  181. const parent = element.parent
  182. let parentUniqueFlag = ''
  183. if (parent) {
  184. parentUniqueFlag = parent.attribs[uniqueFlagAttr]
  185. }
  186. const index = getElementIndex(element)
  187. function getName () {
  188. return element.name || ''
  189. }
  190. // 父节点的 uniqueFlag + tagName + element 在父节点下的索引
  191. if (parentUniqueFlag) {
  192. return `${parentUniqueFlag}--${getName()}${index}`
  193. }
  194. // tagName + 索引
  195. return `${getName()}${index}`
  196. }
  197. function assembleUniqueId (keyElement) {
  198. return keyElement.reduce((prev, key) => {
  199. if (key) {
  200. if (prev) {
  201. return prev + '_' + key
  202. }
  203. return prev + key
  204. }
  205. return prev
  206. }, '')
  207. }
  208. /**
  209. * @static
  210. * @description
  211. * 为微信小程序 dom 节点生成唯一标志,存储在特定 data-[name] 下。
  212. * 节点唯一标志 = 父节点唯一标志 + 在父节点下的索引 + 标签名 + 节点本身id
  213. * 根节点唯一标志 = 节点唯一标志 + 页面path
  214. * wx:for 节点唯一标志 = 节点唯一标志 + index
  215. * @example
  216. * generateElementUniqueFlag(dom, {
  217. indexPrefix: 'index',
  218. flagKey: 'uid',
  219. filePath: 'C:\\Users\xx\Desktop\project\src\util\page.wxml'
  220. })
  221. * @param {Object} dom htmlParser2 解析后得到的 dom ast 树
  222. * @param {Object} options 配置项
  223. * @param {string} [options.indexPrefix=index] wx:for-index 值得前缀,会主动给 wx:for 节点设置 wx:for-index
  224. * @param {string} [options.flagKey=uflag] data-[flagKey] 存储唯一 id
  225. * @param {string} [options.filePath=''] 文件路径,多个文件是,需要加上文件路径才能保证每个节点 id 唯一
  226. */
  227. function generateElementUniqueFlag (dom, options = {}) {
  228. const defalutOptions = {
  229. indexPrefix: 'index',
  230. flagKey: 'uflag',
  231. filePath: ''
  232. }
  233. const currentOptions = Object.assign({}, defalutOptions, options)
  234. const { indexPrefix, flagKey, filePath } = currentOptions
  235. // 存储嵌套index的栈
  236. const nIndexs = []
  237. // 存储wx:for节点的栈
  238. const loopNodes = []
  239. // 存储旧的index的栈
  240. const oIndexs = []
  241. let currentIndex = ''
  242. let deep = 0
  243. let isRoot = true
  244. function travel (dom) {
  245. dom.forEach((element) => {
  246. const attribs = element.attribs
  247. if (attribs) {
  248. const attrs = new HandleAttribs(attribs)
  249. if (attrs.get('wx:for')) {
  250. if (attrs.get('wx:for-index')) {
  251. const index = attrs.get('wx:for-index')
  252. nIndexs.push(index)
  253. oIndexs.push(index)
  254. currentIndex = index
  255. } else {
  256. const index = `${indexPrefix}_${deep}`
  257. // 主动设置wx:for-index
  258. attrs.set('wx:for-index', index)
  259. nIndexs.push(index)
  260. oIndexs.push('index')
  261. currentIndex = index
  262. }
  263. loopNodes.push(element)
  264. deep++
  265. }
  266. // 为每个节点注入唯一标志
  267. const attr = `data-${flagKey}`
  268. // 在父节点下的唯一标志(与父节点唯一标志、标签名、在父节点下的位置索引有关)
  269. const uniqueFlagInParent = getlementUniqueFlagInParent(
  270. element,
  271. attr
  272. )
  273. // wx:for 组件内唯一标志与上层所有 index 有关
  274. // 当发现 wx:for 时,该节点唯一标志与 index 有关
  275. // 这样其所有子节点唯一标志都将与这个 index 有关
  276. // 这样即使是嵌套循环,也能保证内部节点唯一标志与上层每个 wx:for 相关
  277. const indexsStr = currentIndex ? `{{${currentIndex}}}` : ''
  278. let keys = []
  279. // 多个wxml文件时,还需在根节点加上文件路径,才能确保每个元素生成的标志唯一
  280. if (isRoot) {
  281. keys.push(filePath)
  282. }
  283. keys = [
  284. ...keys,
  285. uniqueFlagInParent,
  286. indexsStr,
  287. attrs.get('id')
  288. ]
  289. const uniqueFlag = assembleUniqueId(keys)
  290. const obj = {
  291. [attr]: uniqueFlag
  292. }
  293. attrs.push(obj)
  294. // 处理for+template标签组合
  295. if (element.name === 'template') {
  296. // 太麻烦,暂未处理
  297. // doSomething
  298. } else {
  299. Object.keys(attribs).forEach((key) => {
  300. attribs[key] = handleExpression(
  301. attribs[key],
  302. nIndexs,
  303. oIndexs
  304. )
  305. })
  306. }
  307. } else if (element.type === 'text') {
  308. element.data = handleExpression(element.data, nIndexs, oIndexs)
  309. }
  310. if (element.children) {
  311. isRoot = false
  312. currentIndex = ''
  313. travel(element.children)
  314. }
  315. // 回溯到wx:for节点时
  316. if (element === loopNodes[loopNodes.length - 1]) {
  317. nIndexs.pop()
  318. oIndexs.pop()
  319. loopNodes.pop()
  320. deep--
  321. }
  322. })
  323. }
  324. travel(dom)
  325. }
  326. module.exports = {
  327. generateElementUniqueFlag,
  328. getExpression
  329. }