hy-app 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/attributes.json +1 -1
  2. package/components/hy-address-picker/hy-address-picker.vue +249 -249
  3. package/components/hy-address-picker/props.ts +103 -103
  4. package/components/hy-button/hy-button.vue +320 -289
  5. package/components/hy-button/props.ts +143 -143
  6. package/components/hy-button/typing.d.ts +43 -35
  7. package/components/hy-calendar/header.vue +58 -58
  8. package/components/hy-calendar/hy-calendar.vue +8 -6
  9. package/components/hy-calendar/month.vue +402 -402
  10. package/components/hy-calendar/props.ts +169 -169
  11. package/components/hy-calendar/typing.d.ts +47 -45
  12. package/components/hy-cell-item/hy-cell-item.vue +161 -161
  13. package/components/hy-cell-item/props.ts +59 -59
  14. package/components/hy-check-button/hy-check-button.vue +135 -135
  15. package/components/hy-code-input/hy-code-input.vue +231 -231
  16. package/components/hy-code-input/props.ts +90 -90
  17. package/components/hy-config-provider/hy-config-provider.vue +53 -53
  18. package/components/hy-config-provider/props.ts +30 -30
  19. package/components/hy-coupon/hy-coupon.vue +183 -183
  20. package/components/hy-coupon/props.ts +108 -108
  21. package/components/hy-datetime-picker/hy-datetime-picker.vue +41 -55
  22. package/components/hy-datetime-picker/props.ts +144 -144
  23. package/components/hy-datetime-picker/typing.d.ts +2 -0
  24. package/components/hy-divider/props.ts +83 -83
  25. package/components/hy-empty/icon.ts +72 -72
  26. package/components/hy-folding-panel/hy-folding-panel-group.vue +162 -162
  27. package/components/hy-form/hy-form.vue +220 -220
  28. package/components/hy-icon/hy-icon.vue +112 -112
  29. package/components/hy-index-bar/hy-index-bar.vue +185 -185
  30. package/components/hy-index-bar/index.scss +64 -64
  31. package/components/hy-index-bar/props.ts +94 -94
  32. package/components/hy-index-bar/typing.d.ts +36 -36
  33. package/components/hy-input/hy-input.vue +333 -333
  34. package/components/hy-input/props.ts +186 -186
  35. package/components/hy-modal/hy-modal.vue +211 -211
  36. package/components/hy-modal/props.ts +94 -94
  37. package/components/hy-modal/typing.d.ts +16 -16
  38. package/components/hy-notice-bar/hy-row-notice.vue +121 -121
  39. package/components/hy-notify/hy-notify.vue +174 -174
  40. package/components/hy-number-step/hy-number-step.vue +367 -367
  41. package/components/hy-overlay/hy-overlay.vue +61 -61
  42. package/components/hy-overlay/props.ts +38 -38
  43. package/components/hy-pagination/hy-pagination.vue +136 -136
  44. package/components/hy-pagination/props.ts +58 -58
  45. package/components/hy-parse/hy-parse.vue +550 -550
  46. package/components/hy-parse/node/node.vue +781 -781
  47. package/components/hy-parse/parser.js +1455 -1455
  48. package/components/hy-parse/props.ts +19 -19
  49. package/components/hy-parse/typing.d.ts +68 -68
  50. package/components/hy-picker/hy-picker.vue +435 -435
  51. package/components/hy-picker/props.ts +122 -122
  52. package/components/hy-picker/typing.d.ts +38 -38
  53. package/components/hy-qrcode/props.ts +72 -72
  54. package/components/hy-qrcode/qrcode.js.bak +1433 -1433
  55. package/components/hy-radio/props.ts +97 -97
  56. package/components/hy-read-more/props.ts +48 -48
  57. package/components/hy-search/props.ts +133 -133
  58. package/components/hy-signature/canvasHelper.ts +51 -51
  59. package/components/hy-signature/props.ts +121 -121
  60. package/components/hy-skeleton/hy-skeleton.vue +142 -142
  61. package/components/hy-skeleton/props.ts +46 -46
  62. package/components/hy-skeleton/typing.d.ts +31 -31
  63. package/components/hy-steps/hy-steps.vue +275 -275
  64. package/components/hy-steps/typing.d.ts +25 -25
  65. package/components/hy-swiper/hy-swiper.vue +3 -3
  66. package/components/hy-swiper/index.scss +5 -5
  67. package/components/hy-swiper/props.ts +0 -1
  68. package/components/hy-table/hy-table.vue +630 -630
  69. package/components/hy-table/props.ts +62 -62
  70. package/components/hy-table/typing.d.ts +29 -29
  71. package/components/hy-tabs/hy-tabs.vue +336 -335
  72. package/components/hy-tabs/props.ts +84 -77
  73. package/components/hy-tag/hy-tag.vue +173 -173
  74. package/components/hy-tag/props.ts +89 -89
  75. package/components/hy-text/hy-text.vue +237 -237
  76. package/components/hy-text/props.ts +115 -115
  77. package/components/hy-textarea/hy-textarea.vue +198 -198
  78. package/components/hy-toast/hy-toast.vue +200 -200
  79. package/components/hy-toast/props.ts +3 -3
  80. package/components/hy-transition/hy-transition.vue +157 -157
  81. package/components/hy-transition/props.ts +32 -32
  82. package/components/hy-upload/hy-upload.vue +384 -384
  83. package/components/hy-watermark/hy-watermark.vue +1058 -1058
  84. package/components/hy-watermark/props.ts +109 -109
  85. package/global.d.ts +94 -94
  86. package/libs/api/http.ts +119 -119
  87. package/libs/composables/index.ts +8 -8
  88. package/libs/composables/useMessage.ts +149 -149
  89. package/libs/composables/useToast.ts +45 -45
  90. package/libs/composables/useTranslate.ts +10 -10
  91. package/libs/css/_config.scss +5 -5
  92. package/libs/index.ts +8 -8
  93. package/libs/locale/index.ts +32 -32
  94. package/libs/locale/lang/en-US.ts +84 -84
  95. package/libs/locale/lang/zh-CN.ts +87 -87
  96. package/libs/typing/index.ts +2 -2
  97. package/libs/typing/modules/common.d.ts +139 -139
  98. package/libs/typing/modules/form.ts +176 -176
  99. package/libs/typing/modules/http.d.ts +19 -19
  100. package/libs/typing/modules/index.d.ts +12 -12
  101. package/libs/utils/inside.ts +340 -340
  102. package/libs/utils/inspect.ts +140 -140
  103. package/libs/utils/utils.ts +525 -525
  104. package/package.json +81 -81
  105. package/tags.json +1 -1
  106. package/web-types.json +1 -1
@@ -1,1455 +1,1455 @@
1
- /**
2
- * @fileoverview html 解析器
3
- */
4
-
5
- // 配置
6
- const config = {
7
- // 信任的标签(保持标签名不变)
8
- trustTags: makeMap(
9
- 'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
10
- ),
11
-
12
- // 块级标签(转为 div,其他的非信任标签转为 span)
13
- blockTags: makeMap(
14
- 'address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'
15
- ),
16
-
17
- // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
18
- // 行内标签
19
- inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
20
- // #endif
21
-
22
- // 要移除的标签
23
- ignoreTags: makeMap(
24
- 'area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'
25
- ),
26
-
27
- // 自闭合的标签
28
- voidTags: makeMap(
29
- 'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
30
- ),
31
-
32
- // html 实体
33
- entities: {
34
- lt: '<',
35
- gt: '>',
36
- quot: '"',
37
- apos: "'",
38
- ensp: '\u2002',
39
- emsp: '\u2003',
40
- nbsp: '\xA0',
41
- semi: ';',
42
- ndash: '–',
43
- mdash: '—',
44
- middot: '·',
45
- lsquo: '‘',
46
- rsquo: '’',
47
- ldquo: '“',
48
- rdquo: '”',
49
- bull: '•',
50
- hellip: '…',
51
- larr: '←',
52
- uarr: '↑',
53
- rarr: '→',
54
- darr: '↓'
55
- },
56
-
57
- // 默认的标签样式
58
- tagStyle: {
59
- // #ifndef APP-PLUS-NVUE
60
- address: 'font-style:italic',
61
- big: 'display:inline;font-size:1.2em',
62
- caption: 'display:table-caption;text-align:center',
63
- center: 'text-align:center',
64
- cite: 'font-style:italic',
65
- dd: 'margin-left:40px',
66
- mark: 'background-color:yellow',
67
- pre: 'font-family:monospace;white-space:pre',
68
- s: 'text-decoration:line-through',
69
- small: 'display:inline;font-size:0.8em',
70
- strike: 'text-decoration:line-through',
71
- u: 'text-decoration:underline'
72
- // #endif
73
- },
74
-
75
- // svg 大小写对照表
76
- svgDict: {
77
- animatetransform: 'animateTransform',
78
- lineargradient: 'linearGradient',
79
- viewbox: 'viewBox',
80
- attributename: 'attributeName',
81
- repeatcount: 'repeatCount',
82
- repeatdur: 'repeatDur'
83
- }
84
- }
85
- const tagSelector = {}
86
- // #ifdef APP || H5 || MP-WEIXIN
87
- const { windowWidth } = uni.getWindowInfo()
88
- const { system } = uni.getDeviceInfo()
89
- // #endif
90
- // #ifndef APP || H5 || MP-WEIXIN
91
- const { windowWidth, system } = uni.getSystemInfoSync()
92
- // #endif
93
- const blankChar = makeMap(' ,\r,\n,\t,\f')
94
- let idIndex = 0
95
-
96
- // #ifdef H5 || APP-PLUS
97
- config.ignoreTags.iframe = undefined
98
- config.trustTags.iframe = true
99
- config.ignoreTags.embed = undefined
100
- config.trustTags.embed = true
101
- // #endif
102
- // #ifdef APP-PLUS-NVUE
103
- config.ignoreTags.source = undefined
104
- config.ignoreTags.style = undefined
105
- // #endif
106
-
107
- /**
108
- * @description 创建 map
109
- * @param {String} str 逗号分隔
110
- */
111
- function makeMap(str) {
112
- const map = Object.create(null)
113
- const list = str.split(',')
114
- for (let i = list.length; i--; ) {
115
- map[list[i]] = true
116
- }
117
- return map
118
- }
119
-
120
- /**
121
- * @description 解码 html 实体
122
- * @param {String} str 要解码的字符串
123
- * @param {Boolean} amp 要不要解码 &amp;
124
- * @returns {String} 解码后的字符串
125
- */
126
- function decodeEntity(str, amp) {
127
- let i = str.indexOf('&')
128
- while (i !== -1) {
129
- const j = str.indexOf(';', i + 3)
130
- let code
131
- if (j === -1) break
132
- if (str[i + 1] === '#') {
133
- // &#123; 形式的实体
134
- code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
135
- if (!isNaN(code)) {
136
- str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
137
- }
138
- } else {
139
- // &nbsp; 形式的实体
140
- code = str.substring(i + 1, j)
141
- if (config.entities[code] || (code === 'amp' && amp)) {
142
- str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
143
- }
144
- }
145
- i = str.indexOf('&', i + 1)
146
- }
147
- return str
148
- }
149
-
150
- /**
151
- * @description 合并多个块级标签,加快长内容渲染
152
- * @param {Array} nodes 要合并的标签数组
153
- */
154
- function mergeNodes(nodes) {
155
- let i = nodes.length - 1
156
- for (let j = i; j >= -1; j--) {
157
- if (
158
- j === -1 ||
159
- nodes[j].c ||
160
- !nodes[j].name ||
161
- (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') ||
162
- (nodes[j].attrs.style || '').includes('inline')
163
- ) {
164
- if (i - j >= 5) {
165
- nodes.splice(j + 1, i - j, {
166
- name: 'div',
167
- attrs: {},
168
- children: nodes.slice(j + 1, i + 1)
169
- })
170
- }
171
- i = j - 1
172
- }
173
- }
174
- }
175
-
176
- /**
177
- * @description html 解析器
178
- * @param {Object} vm 组件实例
179
- */
180
- function Parser(vm) {
181
- this.options = vm || {}
182
- this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
183
- this.imgList = vm.imgList || []
184
- this.imgList._unloadimgs = 0
185
- this.plugins = vm.plugins || []
186
- this.attrs = Object.create(null)
187
- this.stack = []
188
- this.nodes = []
189
- this.pre =
190
- (this.options.containerStyle || '').includes('white-space') &&
191
- this.options.containerStyle.includes('pre')
192
- ? 2
193
- : 0
194
- }
195
-
196
- /**
197
- * @description 执行解析
198
- * @param {String} content 要解析的文本
199
- */
200
- Parser.prototype.parse = function (content) {
201
- // 插件处理
202
- for (let i = this.plugins.length; i--; ) {
203
- if (this.plugins[i].onUpdate) {
204
- content = this.plugins[i].onUpdate(content, config) || content
205
- }
206
- }
207
-
208
- new Lexer(this).parse(content)
209
- // 出栈未闭合的标签
210
- while (this.stack.length) {
211
- this.popNode()
212
- }
213
- if (this.nodes.length > 50) {
214
- mergeNodes(this.nodes)
215
- }
216
- return this.nodes
217
- }
218
-
219
- /**
220
- * @description 将标签暴露出来(不被 rich-text 包含)
221
- */
222
- Parser.prototype.expose = function () {
223
- // #ifndef APP-PLUS-NVUE
224
- for (let i = this.stack.length; i--; ) {
225
- const item = this.stack[i]
226
- if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
227
- item.c = 1
228
- }
229
- // #endif
230
- }
231
-
232
- /**
233
- * @description 处理插件
234
- * @param {Object} node 要处理的标签
235
- * @returns {Boolean} 是否要移除此标签
236
- */
237
- Parser.prototype.hook = function (node) {
238
- for (let i = this.plugins.length; i--; ) {
239
- if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
240
- return false
241
- }
242
- }
243
- return true
244
- }
245
-
246
- /**
247
- * @description 将链接拼接上主域名
248
- * @param {String} url 需要拼接的链接
249
- * @returns {String} 拼接后的链接
250
- */
251
- Parser.prototype.getUrl = function (url) {
252
- const domain = this.options.domain
253
- if (url[0] === '/') {
254
- if (url[1] === '/') {
255
- // // 开头的补充协议名
256
- url = (domain ? domain.split('://')[0] : 'http') + ':' + url
257
- } else if (domain) {
258
- // 否则补充整个域名
259
- url = domain + url
260
- } /* #ifdef APP-PLUS */ else {
261
- url = plus.io.convertLocalFileSystemURL(url)
262
- } /* #endif */
263
- } else if (!url.includes('data:') && !url.includes('://')) {
264
- if (domain) {
265
- url = domain + '/' + url
266
- } /* #ifdef APP-PLUS */ else {
267
- url = plus.io.convertLocalFileSystemURL(url)
268
- } /* #endif */
269
- }
270
- return url
271
- }
272
-
273
- /**
274
- * @description 解析样式表
275
- * @param {Object} node 标签
276
- * @returns {Object}
277
- */
278
- Parser.prototype.parseStyle = function (node) {
279
- const attrs = node.attrs
280
- const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
281
- const styleObj = {}
282
- let tmp = ''
283
-
284
- if (attrs.id && !this.xml) {
285
- // 暴露锚点
286
- if (this.options.useAnchor) {
287
- this.expose()
288
- } else if (
289
- node.name !== 'img' &&
290
- node.name !== 'a' &&
291
- node.name !== 'video' &&
292
- node.name !== 'audio'
293
- ) {
294
- attrs.id = undefined
295
- }
296
- }
297
-
298
- // 转换 width 和 height 属性
299
- if (attrs.width) {
300
- styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
301
- attrs.width = undefined
302
- }
303
- if (attrs.height) {
304
- styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
305
- attrs.height = undefined
306
- }
307
-
308
- for (let i = 0, len = list.length; i < len; i++) {
309
- const info = list[i].split(':')
310
- if (info.length < 2) continue
311
- const key = info.shift().trim().toLowerCase()
312
- let value = info.join(':').trim()
313
- if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
314
- // 兼容性的 css 不压缩
315
- tmp += `;${key}:${value}`
316
- } else if (
317
- !styleObj[key] ||
318
- value.includes('import') ||
319
- !styleObj[key].includes('import')
320
- ) {
321
- // 重复的样式进行覆盖
322
- if (value.includes('url')) {
323
- // 填充链接
324
- let j = value.indexOf('(') + 1
325
- if (j) {
326
- while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
327
- j++
328
- }
329
- value = value.substr(0, j) + this.getUrl(value.substr(j))
330
- }
331
- } else if (value.includes('rpx')) {
332
- // 转换 rpx(rich-text 内部不支持 rpx)
333
- value = value.replace(
334
- /[0-9.]+\s*rpx/g,
335
- ($) => (parseFloat($) * windowWidth) / 750 + 'px'
336
- )
337
- }
338
- styleObj[key] = value
339
- }
340
- }
341
-
342
- node.attrs.style = tmp
343
- return styleObj
344
- }
345
-
346
- /**
347
- * @description 解析到标签名
348
- * @param {String} name 标签名
349
- * @private
350
- */
351
- Parser.prototype.onTagName = function (name) {
352
- this.tagName = this.xml ? name : name.toLowerCase()
353
- if (this.tagName === 'svg') {
354
- this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
355
- }
356
- }
357
-
358
- /**
359
- * @description 解析到属性名
360
- * @param {String} name 属性名
361
- * @private
362
- */
363
- Parser.prototype.onAttrName = function (name) {
364
- name = this.xml ? name : name.toLowerCase()
365
- if (name.substr(0, 5) === 'data-') {
366
- if (name === 'data-src' && !this.attrs.src) {
367
- // data-src 自动转为 src
368
- this.attrName = 'src'
369
- } else if (this.tagName === 'img' || this.tagName === 'a') {
370
- // a 和 img 标签保留 data- 的属性,可以在 imgTap 和 linkTap 事件中使用
371
- this.attrName = name
372
- } else {
373
- // 剩余的移除以减小大小
374
- this.attrName = undefined
375
- }
376
- } else {
377
- this.attrName = name
378
- this.attrs[name] = 'T' // boolean 型属性缺省设置
379
- }
380
- }
381
-
382
- /**
383
- * @description 解析到属性值
384
- * @param {String} val 属性值
385
- * @private
386
- */
387
- Parser.prototype.onAttrVal = function (val) {
388
- const name = this.attrName || ''
389
- if (name === 'style' || name === 'href') {
390
- // 部分属性进行实体解码
391
- this.attrs[name] = decodeEntity(val, true)
392
- } else if (name.includes('src')) {
393
- // 拼接主域名
394
- this.attrs[name] = this.getUrl(decodeEntity(val, true))
395
- } else if (name) {
396
- this.attrs[name] = val
397
- }
398
- }
399
-
400
- /**
401
- * @description 解析到标签开始
402
- * @param {Boolean} selfClose 是否有自闭合标识 />
403
- * @private
404
- */
405
- Parser.prototype.onOpenTag = function (selfClose) {
406
- // 拼装 node
407
- const node = Object.create(null)
408
- node.name = this.tagName
409
- node.attrs = this.attrs
410
- // 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
411
- if (this.options.nodes.length) {
412
- node.type = 'node'
413
- }
414
- this.attrs = Object.create(null)
415
-
416
- const attrs = node.attrs
417
- const parent = this.stack[this.stack.length - 1]
418
- const siblings = parent ? parent.children : this.nodes
419
- const close = this.xml ? selfClose : config.voidTags[node.name]
420
-
421
- // 替换标签名选择器
422
- if (tagSelector[node.name]) {
423
- attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
424
- }
425
-
426
- // 转换 embed 标签
427
- if (node.name === 'embed') {
428
- // #ifndef H5 || APP-PLUS
429
- const src = attrs.src || ''
430
- // 按照后缀名和 type 将 embed 转为 video 或 audio
431
- if (
432
- src.includes('.mp4') ||
433
- src.includes('.3gp') ||
434
- src.includes('.m3u8') ||
435
- (attrs.type || '').includes('video')
436
- ) {
437
- node.name = 'video'
438
- } else if (
439
- src.includes('.mp3') ||
440
- src.includes('.wav') ||
441
- src.includes('.aac') ||
442
- src.includes('.m4a') ||
443
- (attrs.type || '').includes('audio')
444
- ) {
445
- node.name = 'audio'
446
- }
447
- if (attrs.autostart) {
448
- attrs.autoplay = 'T'
449
- }
450
- attrs.controls = 'T'
451
- // #endif
452
- // #ifdef H5 || APP-PLUS
453
- this.expose()
454
- // #endif
455
- }
456
-
457
- // #ifndef APP-PLUS-NVUE
458
- // 处理音视频
459
- if (node.name === 'video' || node.name === 'audio') {
460
- // 设置 id 以便获取 context
461
- if (node.name === 'video' && !attrs.id) {
462
- attrs.id = 'v' + idIndex++
463
- }
464
- // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
465
- if (!attrs.controls && !attrs.autoplay) {
466
- attrs.controls = 'T'
467
- }
468
- // 用数组存储所有可用的 source
469
- node.src = []
470
- if (attrs.src) {
471
- node.src.push(attrs.src)
472
- attrs.src = undefined
473
- }
474
- this.expose()
475
- }
476
- // #endif
477
-
478
- // 处理自闭合标签
479
- if (close) {
480
- if (!this.hook(node) || config.ignoreTags[node.name]) {
481
- // 通过 base 标签设置主域名
482
- if (node.name === 'base' && !this.options.domain) {
483
- this.options.domain = attrs.href
484
- } /* #ifndef APP-PLUS-NVUE */ else if (
485
- node.name === 'source' &&
486
- parent &&
487
- (parent.name === 'video' || parent.name === 'audio') &&
488
- attrs.src
489
- ) {
490
- // 设置 source 标签(仅父节点为 video 或 audio 时有效)
491
- parent.src.push(attrs.src)
492
- } /* #endif */
493
- return
494
- }
495
-
496
- // 解析 style
497
- const styleObj = this.parseStyle(node)
498
-
499
- // 处理图片
500
- if (node.name === 'img') {
501
- if (attrs.src) {
502
- // 标记 webp
503
- if (attrs.src.includes('webp')) {
504
- node.webp = 'T'
505
- }
506
- // data url 图片如果没有设置 original-src 默认为不可预览的小图片
507
- if (attrs.src.includes('data:') && !attrs['original-src']) {
508
- attrs.ignore = 'T'
509
- }
510
- if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
511
- for (let i = this.stack.length; i--; ) {
512
- const item = this.stack[i]
513
- if (item.name === 'a') {
514
- node.a = item.attrs
515
- }
516
- if (
517
- item.name === 'table' &&
518
- !node.webp &&
519
- !attrs.src.includes('cloud://')
520
- ) {
521
- if (!styleObj.display || styleObj.display.includes('inline')) {
522
- node.t = 'inline-block'
523
- } else {
524
- node.t = styleObj.display
525
- }
526
- styleObj.display = undefined
527
- }
528
- // #ifndef H5 || APP-PLUS
529
- const style = item.attrs.style || ''
530
- if (
531
- style.includes('flex:') &&
532
- !style.includes('flex:0') &&
533
- !style.includes('flex: 0') &&
534
- (!styleObj.width || parseInt(styleObj.width) > 100)
535
- ) {
536
- styleObj.width = '100% !important'
537
- styleObj.height = ''
538
- for (let j = i + 1; j < this.stack.length; j++) {
539
- this.stack[j].attrs.style = (
540
- this.stack[j].attrs.style || ''
541
- ).replace('inline-', '')
542
- }
543
- } else if (style.includes('flex') && styleObj.width === '100%') {
544
- for (let j = i + 1; j < this.stack.length; j++) {
545
- const style = this.stack[j].attrs.style || ''
546
- if (
547
- !style.includes(';width') &&
548
- !style.includes(' width') &&
549
- style.indexOf('width') !== 0
550
- ) {
551
- styleObj.width = ''
552
- break
553
- }
554
- }
555
- } else if (style.includes('inline-block')) {
556
- if (
557
- styleObj.width &&
558
- styleObj.width[styleObj.width.length - 1] === '%'
559
- ) {
560
- item.attrs.style += ';max-width:' + styleObj.width
561
- styleObj.width = ''
562
- } else {
563
- item.attrs.style += ';max-width:100%'
564
- }
565
- }
566
- // #endif
567
- item.c = 1
568
- }
569
- attrs.i = this.imgList.length.toString()
570
- let src = attrs['original-src'] || attrs.src
571
- // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
572
- if (this.imgList.includes(src)) {
573
- // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
574
- let i = src.indexOf('://')
575
- if (i !== -1) {
576
- i += 3
577
- let newSrc = src.substr(0, i)
578
- for (; i < src.length; i++) {
579
- if (src[i] === '/') break
580
- newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
581
- }
582
- newSrc += src.substr(i)
583
- src = newSrc
584
- }
585
- }
586
- // #endif
587
- this.imgList.push(src)
588
- if (!node.t) {
589
- this.imgList._unloadimgs += 1
590
- }
591
- // #ifdef H5 || APP-PLUS
592
- if (this.options.lazyLoad) {
593
- attrs['data-src'] = attrs.src
594
- attrs.src = undefined
595
- }
596
- // #endif
597
- }
598
- }
599
- if (styleObj.display === 'inline') {
600
- styleObj.display = ''
601
- }
602
- // #ifndef APP-PLUS-NVUE
603
- if (attrs.ignore) {
604
- styleObj['max-width'] = styleObj['max-width'] || '100%'
605
- attrs.style += ';-webkit-touch-callout:none'
606
- }
607
- // #endif
608
- // 设置的宽度超出屏幕,为避免变形,高度转为自动
609
- if (parseInt(styleObj.width) > windowWidth) {
610
- styleObj.height = undefined
611
- }
612
- // 记录是否设置了宽高
613
- if (!isNaN(parseInt(styleObj.width))) {
614
- node.w = 'T'
615
- }
616
- if (
617
- !isNaN(parseInt(styleObj.height)) &&
618
- (!styleObj.height.includes('%') ||
619
- (parent && (parent.attrs.style || '').includes('height')))
620
- ) {
621
- node.h = 'T'
622
- }
623
- } else if (node.name === 'svg') {
624
- siblings.push(node)
625
- this.stack.push(node)
626
- this.popNode()
627
- return
628
- }
629
- for (const key in styleObj) {
630
- if (styleObj[key]) {
631
- attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
632
- }
633
- }
634
- attrs.style = attrs.style.substr(1) || undefined
635
- // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
636
- if (!attrs.style) {
637
- delete attrs.style
638
- }
639
- // #endif
640
- } else {
641
- if (
642
- (node.name === 'pre' ||
643
- ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) &&
644
- this.pre !== 2
645
- ) {
646
- this.pre = node.pre = 1
647
- }
648
- node.children = []
649
- this.stack.push(node)
650
- }
651
-
652
- // 加入节点树
653
- siblings.push(node)
654
- }
655
-
656
- /**
657
- * @description 解析到标签结束
658
- * @param {String} name 标签名
659
- * @private
660
- */
661
- Parser.prototype.onCloseTag = function (name) {
662
- // 依次出栈到匹配为止
663
- name = this.xml ? name : name.toLowerCase()
664
- let i
665
- for (i = this.stack.length; i--; ) {
666
- if (this.stack[i].name === name) break
667
- }
668
- if (i !== -1) {
669
- while (this.stack.length > i) {
670
- this.popNode()
671
- }
672
- } else if (name === 'p' || name === 'br') {
673
- const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
674
- siblings.push({
675
- name,
676
- attrs: {
677
- class: tagSelector[name] || '',
678
- style: this.tagStyle[name] || ''
679
- }
680
- })
681
- }
682
- }
683
-
684
- /**
685
- * @description 处理标签出栈
686
- * @private
687
- */
688
- Parser.prototype.popNode = function () {
689
- const node = this.stack.pop()
690
- let attrs = node.attrs
691
- const children = node.children
692
- const parent = this.stack[this.stack.length - 1]
693
- const siblings = parent ? parent.children : this.nodes
694
-
695
- if (!this.hook(node) || config.ignoreTags[node.name]) {
696
- // 获取标题
697
- if (
698
- node.name === 'title' &&
699
- children.length &&
700
- children[0].type === 'text' &&
701
- this.options.setTitle
702
- ) {
703
- uni.setNavigationBarTitle({
704
- title: children[0].text
705
- })
706
- }
707
- siblings.pop()
708
- return
709
- }
710
-
711
- if (node.pre && this.pre !== 2) {
712
- // 是否合并空白符标识
713
- this.pre = node.pre = undefined
714
- for (let i = this.stack.length; i--; ) {
715
- if (this.stack[i].pre) {
716
- this.pre = 1
717
- }
718
- }
719
- }
720
-
721
- const styleObj = {}
722
-
723
- // 转换 svg
724
- if (node.name === 'svg') {
725
- if (this.xml > 1) {
726
- // 多层 svg 嵌套
727
- this.xml--
728
- return
729
- }
730
- // #ifdef APP-PLUS-NVUE
731
- ;(function traversal(node) {
732
- if (node.name) {
733
- // 调整 svg 的大小写
734
- node.name = config.svgDict[node.name] || node.name
735
- for (const item in node.attrs) {
736
- if (config.svgDict[item]) {
737
- node.attrs[config.svgDict[item]] = node.attrs[item]
738
- node.attrs[item] = undefined
739
- }
740
- }
741
- for (let i = 0; i < (node.children || []).length; i++) {
742
- traversal(node.children[i])
743
- }
744
- }
745
- })(node)
746
- // #endif
747
- // #ifndef APP-PLUS-NVUE
748
- let src = ''
749
- const style = attrs.style
750
- attrs.style = ''
751
- attrs.xmlns = 'http://www.w3.org/2000/svg'
752
- ;(function traversal(node) {
753
- if (node.type === 'text') {
754
- src += node.text
755
- return
756
- }
757
- const name = config.svgDict[node.name] || node.name
758
- src += '<' + name
759
- for (const item in node.attrs) {
760
- const val = node.attrs[item]
761
- if (val) {
762
- src += ` ${config.svgDict[item] || item}="${val}"`
763
- }
764
- }
765
- if (!node.children) {
766
- src += '/>'
767
- } else {
768
- src += '>'
769
- for (let i = 0; i < node.children.length; i++) {
770
- traversal(node.children[i])
771
- }
772
- src += '</' + name + '>'
773
- }
774
- })(node)
775
- node.name = 'img'
776
- node.attrs = {
777
- src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
778
- style,
779
- ignore: 'T'
780
- }
781
- node.children = undefined
782
- // #endif
783
- this.xml = false
784
- return
785
- }
786
-
787
- // #ifndef APP-PLUS-NVUE
788
- // 转换 align 属性
789
- if (attrs.align) {
790
- if (node.name === 'table') {
791
- if (attrs.align === 'center') {
792
- styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
793
- } else {
794
- styleObj.float = attrs.align
795
- }
796
- } else {
797
- styleObj['text-align'] = attrs.align
798
- }
799
- attrs.align = undefined
800
- }
801
-
802
- // 转换 dir 属性
803
- if (attrs.dir) {
804
- styleObj.direction = attrs.dir
805
- attrs.dir = undefined
806
- }
807
-
808
- // 转换 font 标签的属性
809
- if (node.name === 'font') {
810
- if (attrs.color) {
811
- styleObj.color = attrs.color
812
- attrs.color = undefined
813
- }
814
- if (attrs.face) {
815
- styleObj['font-family'] = attrs.face
816
- attrs.face = undefined
817
- }
818
- if (attrs.size) {
819
- let size = parseInt(attrs.size)
820
- if (!isNaN(size)) {
821
- if (size < 1) {
822
- size = 1
823
- } else if (size > 7) {
824
- size = 7
825
- }
826
- styleObj['font-size'] = [
827
- 'x-small',
828
- 'small',
829
- 'medium',
830
- 'large',
831
- 'x-large',
832
- 'xx-large',
833
- 'xxx-large'
834
- ][size - 1]
835
- }
836
- attrs.size = undefined
837
- }
838
- }
839
- // #endif
840
-
841
- // 一些编辑器的自带 class
842
- if ((attrs.class || '').includes('align-center')) {
843
- styleObj['text-align'] = 'center'
844
- }
845
-
846
- Object.assign(styleObj, this.parseStyle(node))
847
-
848
- if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
849
- styleObj['max-width'] = '100%'
850
- styleObj['box-sizing'] = 'border-box'
851
- }
852
-
853
- // #ifndef APP-PLUS-NVUE
854
- if (config.blockTags[node.name]) {
855
- node.name = 'div'
856
- } else if (!config.trustTags[node.name] && !this.xml) {
857
- // 未知标签转为 span,避免无法显示
858
- node.name = 'span'
859
- }
860
-
861
- if (
862
- node.name === 'a' ||
863
- node.name === 'ad' ||
864
- // #ifdef H5 || APP-PLUS
865
- node.name === 'iframe' // eslint-disable-line
866
- // #endif
867
- ) {
868
- this.expose()
869
- } else if (node.name === 'video') {
870
- if ((styleObj.height || '').includes('auto')) {
871
- styleObj.height = undefined
872
- }
873
- /* #ifdef APP-PLUS */
874
- let str = '<video style="width:100%;height:100%"'
875
- for (const item in attrs) {
876
- if (attrs[item]) {
877
- str += ' ' + item + '="' + attrs[item] + '"'
878
- }
879
- }
880
- if (this.options.pauseVideo) {
881
- str +=
882
- " onplay=\"this.dispatchEvent(new CustomEvent('vplay',{bubbles:!0}));for(var e=document.getElementsByTagName('video'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()\""
883
- }
884
- str += '>'
885
- for (let i = 0; i < node.src.length; i++) {
886
- str += '<source src="' + node.src[i] + '">'
887
- }
888
- str += '</video>'
889
- node.html = str
890
- /* #endif */
891
- } else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
892
- // 列表处理
893
- const types = {
894
- a: 'lower-alpha',
895
- A: 'upper-alpha',
896
- i: 'lower-roman',
897
- I: 'upper-roman'
898
- }
899
- if (types[attrs.type]) {
900
- attrs.style += ';list-style-type:' + types[attrs.type]
901
- attrs.type = undefined
902
- }
903
- for (let i = children.length; i--; ) {
904
- if (children[i].name === 'li') {
905
- children[i].c = 1
906
- }
907
- }
908
- } else if (node.name === 'table') {
909
- // 表格处理
910
- // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
911
- let padding = parseFloat(attrs.cellpadding)
912
- let spacing = parseFloat(attrs.cellspacing)
913
- const border = parseFloat(attrs.border)
914
- const bordercolor = styleObj['border-color']
915
- const borderstyle = styleObj['border-style']
916
- if (node.c) {
917
- // padding 和 spacing 默认 2
918
- if (isNaN(padding)) {
919
- padding = 2
920
- }
921
- if (isNaN(spacing)) {
922
- spacing = 2
923
- }
924
- }
925
- if (border) {
926
- attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
927
- }
928
- if (node.flag && node.c) {
929
- // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
930
- styleObj.display = 'grid'
931
- if (spacing) {
932
- styleObj['grid-gap'] = spacing + 'px'
933
- styleObj.padding = spacing + 'px'
934
- } else if (border) {
935
- // 无间隔的情况下避免边框重叠
936
- attrs.style += ';border-left:0;border-top:0'
937
- }
938
-
939
- const width = [] // 表格的列宽
940
- const trList = [] // tr 列表
941
- const cells = [] // 保存新的单元格
942
- const map = {} // 被合并单元格占用的格子
943
-
944
- ;(function traversal(nodes) {
945
- for (let i = 0; i < nodes.length; i++) {
946
- if (nodes[i].name === 'tr') {
947
- trList.push(nodes[i])
948
- } else {
949
- traversal(nodes[i].children || [])
950
- }
951
- }
952
- })(children)
953
-
954
- for (let row = 1; row <= trList.length; row++) {
955
- let col = 1
956
- for (let j = 0; j < trList[row - 1].children.length; j++) {
957
- const td = trList[row - 1].children[j]
958
- if (td.name === 'td' || td.name === 'th') {
959
- // 这个格子被上面的单元格占用,则列号++
960
- while (map[row + '.' + col]) {
961
- col++
962
- }
963
- let style = td.attrs.style || ''
964
- let start = style.indexOf('width') ? style.indexOf(';width') : 0
965
- // 提取出 td 的宽度
966
- if (start !== -1) {
967
- let end = style.indexOf(';', start + 6)
968
- if (end === -1) {
969
- end = style.length
970
- }
971
- if (!td.attrs.colspan) {
972
- width[col] = style.substring(start ? start + 7 : 6, end)
973
- }
974
- style = style.substr(0, start) + style.substr(end)
975
- }
976
- // 设置竖直对齐
977
- style += ';display:flex'
978
- start = style.indexOf('vertical-align')
979
- if (start !== -1) {
980
- const val = style.substr(start + 15, 10)
981
- if (val.includes('middle')) {
982
- style += ';align-items:center'
983
- } else if (val.includes('bottom')) {
984
- style += ';align-items:flex-end'
985
- }
986
- } else {
987
- style += ';align-items:center'
988
- }
989
- // 设置水平对齐
990
- start = style.indexOf('text-align')
991
- if (start !== -1) {
992
- const val = style.substr(start + 11, 10)
993
- if (val.includes('center')) {
994
- style += ';justify-content: center'
995
- } else if (val.includes('right')) {
996
- style += ';justify-content: right'
997
- }
998
- }
999
- style =
1000
- (border
1001
- ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` +
1002
- (spacing ? '' : ';border-right:0;border-bottom:0')
1003
- : '') +
1004
- (padding ? `;padding:${padding}px` : '') +
1005
- ';' +
1006
- style
1007
- // 处理列合并
1008
- if (td.attrs.colspan) {
1009
- style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
1010
- if (!td.attrs.rowspan) {
1011
- style += `;grid-row-start:${row};grid-row-end:${row + 1}`
1012
- }
1013
- col += parseInt(td.attrs.colspan) - 1
1014
- }
1015
- // 处理行合并
1016
- if (td.attrs.rowspan) {
1017
- style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
1018
- if (!td.attrs.colspan) {
1019
- style += `;grid-column-start:${col};grid-column-end:${col + 1}`
1020
- }
1021
- // 记录下方单元格被占用
1022
- for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
1023
- for (
1024
- let colspan = 0;
1025
- colspan < (td.attrs.colspan || 1);
1026
- colspan++
1027
- ) {
1028
- map[row + rowspan + '.' + (col - colspan)] = 1
1029
- }
1030
- }
1031
- }
1032
- if (style) {
1033
- td.attrs.style = style
1034
- }
1035
- cells.push(td)
1036
- col++
1037
- }
1038
- }
1039
- if (row === 1) {
1040
- let temp = ''
1041
- for (let i = 1; i < col; i++) {
1042
- temp += (width[i] ? width[i] : 'auto') + ' '
1043
- }
1044
- styleObj['grid-template-columns'] = temp
1045
- }
1046
- }
1047
- node.children = cells
1048
- } else {
1049
- // 没有使用合并单元格的表格通过 table 布局实现
1050
- if (node.c) {
1051
- styleObj.display = 'table'
1052
- }
1053
- if (!isNaN(spacing)) {
1054
- styleObj['border-spacing'] = spacing + 'px'
1055
- }
1056
- if (border || padding) {
1057
- // 遍历
1058
- ;(function traversal(nodes) {
1059
- for (let i = 0; i < nodes.length; i++) {
1060
- const td = nodes[i]
1061
- if (td.name === 'th' || td.name === 'td') {
1062
- if (border) {
1063
- td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
1064
- }
1065
- if (padding) {
1066
- td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
1067
- }
1068
- } else if (td.children) {
1069
- traversal(td.children)
1070
- }
1071
- }
1072
- })(children)
1073
- }
1074
- }
1075
- // 给表格添加一个单独的横向滚动层
1076
- if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
1077
- const table = Object.assign({}, node)
1078
- node.name = 'div'
1079
- node.attrs = {
1080
- style: 'overflow:auto'
1081
- }
1082
- node.children = [table]
1083
- attrs = table.attrs
1084
- }
1085
- } else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
1086
- for (let i = this.stack.length; i--; ) {
1087
- if (this.stack[i].name === 'table') {
1088
- this.stack[i].flag = 1 // 指示含有合并单元格
1089
- break
1090
- }
1091
- }
1092
- } else if (node.name === 'ruby') {
1093
- // 转换 ruby
1094
- node.name = 'span'
1095
- for (let i = 0; i < children.length - 1; i++) {
1096
- if (children[i].type === 'text' && children[i + 1].name === 'rt') {
1097
- children[i] = {
1098
- name: 'div',
1099
- attrs: {
1100
- style: 'display:inline-block;text-align:center'
1101
- },
1102
- children: [
1103
- {
1104
- name: 'div',
1105
- attrs: {
1106
- style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
1107
- },
1108
- children: children[i + 1].children
1109
- },
1110
- children[i]
1111
- ]
1112
- }
1113
- children.splice(i + 1, 1)
1114
- }
1115
- }
1116
- } else if (node.c) {
1117
- ;(function traversal(node) {
1118
- node.c = 2
1119
- for (let i = node.children.length; i--; ) {
1120
- const child = node.children[i]
1121
- // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
1122
- if (
1123
- child.name &&
1124
- (config.inlineTags[child.name] ||
1125
- ((child.attrs.style || '').includes('inline') && child.children)) &&
1126
- !child.c
1127
- ) {
1128
- traversal(child)
1129
- }
1130
- // #endif
1131
- if (!child.c || child.name === 'table') {
1132
- node.c = 1
1133
- }
1134
- }
1135
- })(node)
1136
- }
1137
-
1138
- if ((styleObj.display || '').includes('flex') && !node.c) {
1139
- for (let i = children.length; i--; ) {
1140
- const item = children[i]
1141
- if (item.f) {
1142
- item.attrs.style = (item.attrs.style || '') + item.f
1143
- item.f = undefined
1144
- }
1145
- }
1146
- }
1147
- // flex 布局时部分样式需要提取到 rich-text 外层
1148
- const flex =
1149
- parent &&
1150
- ((parent.attrs.style || '').includes('flex') ||
1151
- (parent.attrs.style || '').includes('grid')) &&
1152
- // #ifdef MP-WEIXIN
1153
- // 检查基础库版本 virtualHost 是否可用
1154
- !(node.c && wx.getNFCAdapter) && // eslint-disable-line
1155
- // #endif
1156
- // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
1157
- !node.c // eslint-disable-line
1158
- // #endif
1159
- if (flex) {
1160
- node.f = ';max-width:100%'
1161
- }
1162
-
1163
- if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
1164
- mergeNodes(children)
1165
- }
1166
- // #endif
1167
-
1168
- for (const key in styleObj) {
1169
- if (styleObj[key]) {
1170
- const val = `;${key}:${styleObj[key].replace(' !important', '')}`
1171
- /* #ifndef APP-PLUS-NVUE */
1172
- if (
1173
- flex &&
1174
- ((key.includes('flex') && key !== 'flex-direction') ||
1175
- key === 'align-self' ||
1176
- key.includes('grid') ||
1177
- styleObj[key][0] === '-' ||
1178
- (key.includes('width') && val.includes('%')))
1179
- ) {
1180
- node.f += val
1181
- if (key === 'width') {
1182
- attrs.style += ';width:100%'
1183
- }
1184
- } else /* #endif */ {
1185
- attrs.style += val
1186
- }
1187
- }
1188
- }
1189
- attrs.style = attrs.style.substr(1) || undefined
1190
- // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
1191
- for (const key in attrs) {
1192
- if (!attrs[key]) {
1193
- delete attrs[key]
1194
- }
1195
- }
1196
- // #endif
1197
- }
1198
-
1199
- /**
1200
- * @description 解析到文本
1201
- * @param {String} text 文本内容
1202
- */
1203
- Parser.prototype.onText = function (text) {
1204
- if (!this.pre) {
1205
- // 合并空白符
1206
- let trim = ''
1207
- let flag
1208
- for (let i = 0, len = text.length; i < len; i++) {
1209
- if (!blankChar[text[i]]) {
1210
- trim += text[i]
1211
- } else {
1212
- if (trim[trim.length - 1] !== ' ') {
1213
- trim += ' '
1214
- }
1215
- if (text[i] === '\n' && !flag) {
1216
- flag = true
1217
- }
1218
- }
1219
- }
1220
- // 去除含有换行符的空串
1221
- if (trim === ' ') {
1222
- if (flag) return
1223
- // #ifdef VUE3
1224
- else {
1225
- const parent = this.stack[this.stack.length - 1]
1226
- if (parent && parent.name[0] === 't') return
1227
- }
1228
- // #endif
1229
- }
1230
- text = trim
1231
- }
1232
- const node = Object.create(null)
1233
- node.type = 'text'
1234
- // #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
1235
- node.attrs = {}
1236
- // #endif
1237
- node.text = decodeEntity(text)
1238
- if (this.hook(node)) {
1239
- // #ifdef MP-WEIXIN
1240
- if (
1241
- this.options.selectable === 'force' &&
1242
- system.includes('iOS') &&
1243
- !uni.canIUse('rich-text.user-select')
1244
- ) {
1245
- this.expose()
1246
- }
1247
- // #endif
1248
- const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
1249
- siblings.push(node)
1250
- }
1251
- }
1252
-
1253
- /**
1254
- * @description html 词法分析器
1255
- * @param {Object} handler 高层处理器
1256
- */
1257
- function Lexer(handler) {
1258
- this.handler = handler
1259
- }
1260
-
1261
- /**
1262
- * @description 执行解析
1263
- * @param {String} content 要解析的文本
1264
- */
1265
- Lexer.prototype.parse = function (content) {
1266
- this.content = content || ''
1267
- this.i = 0 // 标记解析位置
1268
- this.start = 0 // 标记一个单词的开始位置
1269
- this.state = this.text // 当前状态
1270
- for (let len = this.content.length; this.i !== -1 && this.i < len; ) {
1271
- this.state()
1272
- }
1273
- }
1274
-
1275
- /**
1276
- * @description 检查标签是否闭合
1277
- * @param {String} method 如果闭合要进行的操作
1278
- * @returns {Boolean} 是否闭合
1279
- * @private
1280
- */
1281
- Lexer.prototype.checkClose = function (method) {
1282
- const selfClose = this.content[this.i] === '/'
1283
- if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
1284
- if (method) {
1285
- this.handler[method](this.content.substring(this.start, this.i))
1286
- }
1287
- this.i += selfClose ? 2 : 1
1288
- this.start = this.i
1289
- this.handler.onOpenTag(selfClose)
1290
- if (this.handler.tagName === 'script') {
1291
- this.i = this.content.indexOf('</', this.i)
1292
- if (this.i !== -1) {
1293
- this.i += 2
1294
- this.start = this.i
1295
- }
1296
- this.state = this.endTag
1297
- } else {
1298
- this.state = this.text
1299
- }
1300
- return true
1301
- }
1302
- return false
1303
- }
1304
-
1305
- /**
1306
- * @description 文本状态
1307
- * @private
1308
- */
1309
- Lexer.prototype.text = function () {
1310
- this.i = this.content.indexOf('<', this.i) // 查找最近的标签
1311
- if (this.i === -1) {
1312
- // 没有标签了
1313
- if (this.start < this.content.length) {
1314
- this.handler.onText(this.content.substring(this.start, this.content.length))
1315
- }
1316
- return
1317
- }
1318
- const c = this.content[this.i + 1]
1319
- if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
1320
- // 标签开头
1321
- if (this.start !== this.i) {
1322
- this.handler.onText(this.content.substring(this.start, this.i))
1323
- }
1324
- this.start = ++this.i
1325
- this.state = this.tagName
1326
- } else if (c === '/' || c === '!' || c === '?') {
1327
- if (this.start !== this.i) {
1328
- this.handler.onText(this.content.substring(this.start, this.i))
1329
- }
1330
- const next = this.content[this.i + 2]
1331
- if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
1332
- // 标签结尾
1333
- this.i += 2
1334
- this.start = this.i
1335
- this.state = this.endTag
1336
- return
1337
- }
1338
- // 处理注释
1339
- let end = '-->'
1340
- if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
1341
- end = '>'
1342
- }
1343
- this.i = this.content.indexOf(end, this.i)
1344
- if (this.i !== -1) {
1345
- this.i += end.length
1346
- this.start = this.i
1347
- }
1348
- } else {
1349
- this.i++
1350
- }
1351
- }
1352
-
1353
- /**
1354
- * @description 标签名状态
1355
- * @private
1356
- */
1357
- Lexer.prototype.tagName = function () {
1358
- if (blankChar[this.content[this.i]]) {
1359
- // 解析到标签名
1360
- this.handler.onTagName(this.content.substring(this.start, this.i))
1361
- while (blankChar[this.content[++this.i]]);
1362
- if (this.i < this.content.length && !this.checkClose()) {
1363
- this.start = this.i
1364
- this.state = this.attrName
1365
- }
1366
- } else if (!this.checkClose('onTagName')) {
1367
- this.i++
1368
- }
1369
- }
1370
-
1371
- /**
1372
- * @description 属性名状态
1373
- * @private
1374
- */
1375
- Lexer.prototype.attrName = function () {
1376
- let c = this.content[this.i]
1377
- if (blankChar[c] || c === '=') {
1378
- // 解析到属性名
1379
- this.handler.onAttrName(this.content.substring(this.start, this.i))
1380
- let needVal = c === '='
1381
- const len = this.content.length
1382
- while (++this.i < len) {
1383
- c = this.content[this.i]
1384
- if (!blankChar[c]) {
1385
- if (this.checkClose()) return
1386
- if (needVal) {
1387
- // 等号后遇到第一个非空字符
1388
- this.start = this.i
1389
- this.state = this.attrVal
1390
- return
1391
- }
1392
- if (this.content[this.i] === '=') {
1393
- needVal = true
1394
- } else {
1395
- this.start = this.i
1396
- this.state = this.attrName
1397
- return
1398
- }
1399
- }
1400
- }
1401
- } else if (!this.checkClose('onAttrName')) {
1402
- this.i++
1403
- }
1404
- }
1405
-
1406
- /**
1407
- * @description 属性值状态
1408
- * @private
1409
- */
1410
- Lexer.prototype.attrVal = function () {
1411
- const c = this.content[this.i]
1412
- const len = this.content.length
1413
- if (c === '"' || c === "'") {
1414
- // 有冒号的属性
1415
- this.start = ++this.i
1416
- this.i = this.content.indexOf(c, this.i)
1417
- if (this.i === -1) return
1418
- this.handler.onAttrVal(this.content.substring(this.start, this.i))
1419
- } else {
1420
- // 没有冒号的属性
1421
- for (; this.i < len; this.i++) {
1422
- if (blankChar[this.content[this.i]]) {
1423
- this.handler.onAttrVal(this.content.substring(this.start, this.i))
1424
- break
1425
- } else if (this.checkClose('onAttrVal')) return
1426
- }
1427
- }
1428
- while (blankChar[this.content[++this.i]]);
1429
- if (this.i < len && !this.checkClose()) {
1430
- this.start = this.i
1431
- this.state = this.attrName
1432
- }
1433
- }
1434
-
1435
- /**
1436
- * @description 结束标签状态
1437
- * @returns {String} 结束的标签名
1438
- * @private
1439
- */
1440
- Lexer.prototype.endTag = function () {
1441
- const c = this.content[this.i]
1442
- if (blankChar[c] || c === '>' || c === '/') {
1443
- this.handler.onCloseTag(this.content.substring(this.start, this.i))
1444
- if (c !== '>') {
1445
- this.i = this.content.indexOf('>', this.i)
1446
- if (this.i === -1) return
1447
- }
1448
- this.start = ++this.i
1449
- this.state = this.text
1450
- } else {
1451
- this.i++
1452
- }
1453
- }
1454
-
1455
- export default Parser
1
+ /**
2
+ * @fileoverview html 解析器
3
+ */
4
+
5
+ // 配置
6
+ const config = {
7
+ // 信任的标签(保持标签名不变)
8
+ trustTags: makeMap(
9
+ 'a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'
10
+ ),
11
+
12
+ // 块级标签(转为 div,其他的非信任标签转为 span)
13
+ blockTags: makeMap(
14
+ 'address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'
15
+ ),
16
+
17
+ // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
18
+ // 行内标签
19
+ inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),
20
+ // #endif
21
+
22
+ // 要移除的标签
23
+ ignoreTags: makeMap(
24
+ 'area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'
25
+ ),
26
+
27
+ // 自闭合的标签
28
+ voidTags: makeMap(
29
+ 'area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'
30
+ ),
31
+
32
+ // html 实体
33
+ entities: {
34
+ lt: '<',
35
+ gt: '>',
36
+ quot: '"',
37
+ apos: "'",
38
+ ensp: '\u2002',
39
+ emsp: '\u2003',
40
+ nbsp: '\xA0',
41
+ semi: ';',
42
+ ndash: '–',
43
+ mdash: '—',
44
+ middot: '·',
45
+ lsquo: '‘',
46
+ rsquo: '’',
47
+ ldquo: '“',
48
+ rdquo: '”',
49
+ bull: '•',
50
+ hellip: '…',
51
+ larr: '←',
52
+ uarr: '↑',
53
+ rarr: '→',
54
+ darr: '↓'
55
+ },
56
+
57
+ // 默认的标签样式
58
+ tagStyle: {
59
+ // #ifndef APP-PLUS-NVUE
60
+ address: 'font-style:italic',
61
+ big: 'display:inline;font-size:1.2em',
62
+ caption: 'display:table-caption;text-align:center',
63
+ center: 'text-align:center',
64
+ cite: 'font-style:italic',
65
+ dd: 'margin-left:40px',
66
+ mark: 'background-color:yellow',
67
+ pre: 'font-family:monospace;white-space:pre',
68
+ s: 'text-decoration:line-through',
69
+ small: 'display:inline;font-size:0.8em',
70
+ strike: 'text-decoration:line-through',
71
+ u: 'text-decoration:underline'
72
+ // #endif
73
+ },
74
+
75
+ // svg 大小写对照表
76
+ svgDict: {
77
+ animatetransform: 'animateTransform',
78
+ lineargradient: 'linearGradient',
79
+ viewbox: 'viewBox',
80
+ attributename: 'attributeName',
81
+ repeatcount: 'repeatCount',
82
+ repeatdur: 'repeatDur'
83
+ }
84
+ }
85
+ const tagSelector = {}
86
+ // #ifdef APP || H5 || MP-WEIXIN
87
+ const { windowWidth } = uni.getWindowInfo()
88
+ const { system } = uni.getDeviceInfo()
89
+ // #endif
90
+ // #ifndef APP || H5 || MP-WEIXIN
91
+ const { windowWidth, system } = uni.getSystemInfoSync()
92
+ // #endif
93
+ const blankChar = makeMap(' ,\r,\n,\t,\f')
94
+ let idIndex = 0
95
+
96
+ // #ifdef H5 || APP-PLUS
97
+ config.ignoreTags.iframe = undefined
98
+ config.trustTags.iframe = true
99
+ config.ignoreTags.embed = undefined
100
+ config.trustTags.embed = true
101
+ // #endif
102
+ // #ifdef APP-PLUS-NVUE
103
+ config.ignoreTags.source = undefined
104
+ config.ignoreTags.style = undefined
105
+ // #endif
106
+
107
+ /**
108
+ * @description 创建 map
109
+ * @param {String} str 逗号分隔
110
+ */
111
+ function makeMap(str) {
112
+ const map = Object.create(null)
113
+ const list = str.split(',')
114
+ for (let i = list.length; i--; ) {
115
+ map[list[i]] = true
116
+ }
117
+ return map
118
+ }
119
+
120
+ /**
121
+ * @description 解码 html 实体
122
+ * @param {String} str 要解码的字符串
123
+ * @param {Boolean} amp 要不要解码 &amp;
124
+ * @returns {String} 解码后的字符串
125
+ */
126
+ function decodeEntity(str, amp) {
127
+ let i = str.indexOf('&')
128
+ while (i !== -1) {
129
+ const j = str.indexOf(';', i + 3)
130
+ let code
131
+ if (j === -1) break
132
+ if (str[i + 1] === '#') {
133
+ // &#123; 形式的实体
134
+ code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
135
+ if (!isNaN(code)) {
136
+ str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)
137
+ }
138
+ } else {
139
+ // &nbsp; 形式的实体
140
+ code = str.substring(i + 1, j)
141
+ if (config.entities[code] || (code === 'amp' && amp)) {
142
+ str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)
143
+ }
144
+ }
145
+ i = str.indexOf('&', i + 1)
146
+ }
147
+ return str
148
+ }
149
+
150
+ /**
151
+ * @description 合并多个块级标签,加快长内容渲染
152
+ * @param {Array} nodes 要合并的标签数组
153
+ */
154
+ function mergeNodes(nodes) {
155
+ let i = nodes.length - 1
156
+ for (let j = i; j >= -1; j--) {
157
+ if (
158
+ j === -1 ||
159
+ nodes[j].c ||
160
+ !nodes[j].name ||
161
+ (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') ||
162
+ (nodes[j].attrs.style || '').includes('inline')
163
+ ) {
164
+ if (i - j >= 5) {
165
+ nodes.splice(j + 1, i - j, {
166
+ name: 'div',
167
+ attrs: {},
168
+ children: nodes.slice(j + 1, i + 1)
169
+ })
170
+ }
171
+ i = j - 1
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * @description html 解析器
178
+ * @param {Object} vm 组件实例
179
+ */
180
+ function Parser(vm) {
181
+ this.options = vm || {}
182
+ this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)
183
+ this.imgList = vm.imgList || []
184
+ this.imgList._unloadimgs = 0
185
+ this.plugins = vm.plugins || []
186
+ this.attrs = Object.create(null)
187
+ this.stack = []
188
+ this.nodes = []
189
+ this.pre =
190
+ (this.options.containerStyle || '').includes('white-space') &&
191
+ this.options.containerStyle.includes('pre')
192
+ ? 2
193
+ : 0
194
+ }
195
+
196
+ /**
197
+ * @description 执行解析
198
+ * @param {String} content 要解析的文本
199
+ */
200
+ Parser.prototype.parse = function (content) {
201
+ // 插件处理
202
+ for (let i = this.plugins.length; i--; ) {
203
+ if (this.plugins[i].onUpdate) {
204
+ content = this.plugins[i].onUpdate(content, config) || content
205
+ }
206
+ }
207
+
208
+ new Lexer(this).parse(content)
209
+ // 出栈未闭合的标签
210
+ while (this.stack.length) {
211
+ this.popNode()
212
+ }
213
+ if (this.nodes.length > 50) {
214
+ mergeNodes(this.nodes)
215
+ }
216
+ return this.nodes
217
+ }
218
+
219
+ /**
220
+ * @description 将标签暴露出来(不被 rich-text 包含)
221
+ */
222
+ Parser.prototype.expose = function () {
223
+ // #ifndef APP-PLUS-NVUE
224
+ for (let i = this.stack.length; i--; ) {
225
+ const item = this.stack[i]
226
+ if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return
227
+ item.c = 1
228
+ }
229
+ // #endif
230
+ }
231
+
232
+ /**
233
+ * @description 处理插件
234
+ * @param {Object} node 要处理的标签
235
+ * @returns {Boolean} 是否要移除此标签
236
+ */
237
+ Parser.prototype.hook = function (node) {
238
+ for (let i = this.plugins.length; i--; ) {
239
+ if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {
240
+ return false
241
+ }
242
+ }
243
+ return true
244
+ }
245
+
246
+ /**
247
+ * @description 将链接拼接上主域名
248
+ * @param {String} url 需要拼接的链接
249
+ * @returns {String} 拼接后的链接
250
+ */
251
+ Parser.prototype.getUrl = function (url) {
252
+ const domain = this.options.domain
253
+ if (url[0] === '/') {
254
+ if (url[1] === '/') {
255
+ // // 开头的补充协议名
256
+ url = (domain ? domain.split('://')[0] : 'http') + ':' + url
257
+ } else if (domain) {
258
+ // 否则补充整个域名
259
+ url = domain + url
260
+ } /* #ifdef APP-PLUS */ else {
261
+ url = plus.io.convertLocalFileSystemURL(url)
262
+ } /* #endif */
263
+ } else if (!url.includes('data:') && !url.includes('://')) {
264
+ if (domain) {
265
+ url = domain + '/' + url
266
+ } /* #ifdef APP-PLUS */ else {
267
+ url = plus.io.convertLocalFileSystemURL(url)
268
+ } /* #endif */
269
+ }
270
+ return url
271
+ }
272
+
273
+ /**
274
+ * @description 解析样式表
275
+ * @param {Object} node 标签
276
+ * @returns {Object}
277
+ */
278
+ Parser.prototype.parseStyle = function (node) {
279
+ const attrs = node.attrs
280
+ const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))
281
+ const styleObj = {}
282
+ let tmp = ''
283
+
284
+ if (attrs.id && !this.xml) {
285
+ // 暴露锚点
286
+ if (this.options.useAnchor) {
287
+ this.expose()
288
+ } else if (
289
+ node.name !== 'img' &&
290
+ node.name !== 'a' &&
291
+ node.name !== 'video' &&
292
+ node.name !== 'audio'
293
+ ) {
294
+ attrs.id = undefined
295
+ }
296
+ }
297
+
298
+ // 转换 width 和 height 属性
299
+ if (attrs.width) {
300
+ styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')
301
+ attrs.width = undefined
302
+ }
303
+ if (attrs.height) {
304
+ styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')
305
+ attrs.height = undefined
306
+ }
307
+
308
+ for (let i = 0, len = list.length; i < len; i++) {
309
+ const info = list[i].split(':')
310
+ if (info.length < 2) continue
311
+ const key = info.shift().trim().toLowerCase()
312
+ let value = info.join(':').trim()
313
+ if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {
314
+ // 兼容性的 css 不压缩
315
+ tmp += `;${key}:${value}`
316
+ } else if (
317
+ !styleObj[key] ||
318
+ value.includes('import') ||
319
+ !styleObj[key].includes('import')
320
+ ) {
321
+ // 重复的样式进行覆盖
322
+ if (value.includes('url')) {
323
+ // 填充链接
324
+ let j = value.indexOf('(') + 1
325
+ if (j) {
326
+ while (value[j] === '"' || value[j] === "'" || blankChar[value[j]]) {
327
+ j++
328
+ }
329
+ value = value.substr(0, j) + this.getUrl(value.substr(j))
330
+ }
331
+ } else if (value.includes('rpx')) {
332
+ // 转换 rpx(rich-text 内部不支持 rpx)
333
+ value = value.replace(
334
+ /[0-9.]+\s*rpx/g,
335
+ ($) => (parseFloat($) * windowWidth) / 750 + 'px'
336
+ )
337
+ }
338
+ styleObj[key] = value
339
+ }
340
+ }
341
+
342
+ node.attrs.style = tmp
343
+ return styleObj
344
+ }
345
+
346
+ /**
347
+ * @description 解析到标签名
348
+ * @param {String} name 标签名
349
+ * @private
350
+ */
351
+ Parser.prototype.onTagName = function (name) {
352
+ this.tagName = this.xml ? name : name.toLowerCase()
353
+ if (this.tagName === 'svg') {
354
+ this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感
355
+ }
356
+ }
357
+
358
+ /**
359
+ * @description 解析到属性名
360
+ * @param {String} name 属性名
361
+ * @private
362
+ */
363
+ Parser.prototype.onAttrName = function (name) {
364
+ name = this.xml ? name : name.toLowerCase()
365
+ if (name.substr(0, 5) === 'data-') {
366
+ if (name === 'data-src' && !this.attrs.src) {
367
+ // data-src 自动转为 src
368
+ this.attrName = 'src'
369
+ } else if (this.tagName === 'img' || this.tagName === 'a') {
370
+ // a 和 img 标签保留 data- 的属性,可以在 imgTap 和 linkTap 事件中使用
371
+ this.attrName = name
372
+ } else {
373
+ // 剩余的移除以减小大小
374
+ this.attrName = undefined
375
+ }
376
+ } else {
377
+ this.attrName = name
378
+ this.attrs[name] = 'T' // boolean 型属性缺省设置
379
+ }
380
+ }
381
+
382
+ /**
383
+ * @description 解析到属性值
384
+ * @param {String} val 属性值
385
+ * @private
386
+ */
387
+ Parser.prototype.onAttrVal = function (val) {
388
+ const name = this.attrName || ''
389
+ if (name === 'style' || name === 'href') {
390
+ // 部分属性进行实体解码
391
+ this.attrs[name] = decodeEntity(val, true)
392
+ } else if (name.includes('src')) {
393
+ // 拼接主域名
394
+ this.attrs[name] = this.getUrl(decodeEntity(val, true))
395
+ } else if (name) {
396
+ this.attrs[name] = val
397
+ }
398
+ }
399
+
400
+ /**
401
+ * @description 解析到标签开始
402
+ * @param {Boolean} selfClose 是否有自闭合标识 />
403
+ * @private
404
+ */
405
+ Parser.prototype.onOpenTag = function (selfClose) {
406
+ // 拼装 node
407
+ const node = Object.create(null)
408
+ node.name = this.tagName
409
+ node.attrs = this.attrs
410
+ // 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示
411
+ if (this.options.nodes.length) {
412
+ node.type = 'node'
413
+ }
414
+ this.attrs = Object.create(null)
415
+
416
+ const attrs = node.attrs
417
+ const parent = this.stack[this.stack.length - 1]
418
+ const siblings = parent ? parent.children : this.nodes
419
+ const close = this.xml ? selfClose : config.voidTags[node.name]
420
+
421
+ // 替换标签名选择器
422
+ if (tagSelector[node.name]) {
423
+ attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')
424
+ }
425
+
426
+ // 转换 embed 标签
427
+ if (node.name === 'embed') {
428
+ // #ifndef H5 || APP-PLUS
429
+ const src = attrs.src || ''
430
+ // 按照后缀名和 type 将 embed 转为 video 或 audio
431
+ if (
432
+ src.includes('.mp4') ||
433
+ src.includes('.3gp') ||
434
+ src.includes('.m3u8') ||
435
+ (attrs.type || '').includes('video')
436
+ ) {
437
+ node.name = 'video'
438
+ } else if (
439
+ src.includes('.mp3') ||
440
+ src.includes('.wav') ||
441
+ src.includes('.aac') ||
442
+ src.includes('.m4a') ||
443
+ (attrs.type || '').includes('audio')
444
+ ) {
445
+ node.name = 'audio'
446
+ }
447
+ if (attrs.autostart) {
448
+ attrs.autoplay = 'T'
449
+ }
450
+ attrs.controls = 'T'
451
+ // #endif
452
+ // #ifdef H5 || APP-PLUS
453
+ this.expose()
454
+ // #endif
455
+ }
456
+
457
+ // #ifndef APP-PLUS-NVUE
458
+ // 处理音视频
459
+ if (node.name === 'video' || node.name === 'audio') {
460
+ // 设置 id 以便获取 context
461
+ if (node.name === 'video' && !attrs.id) {
462
+ attrs.id = 'v' + idIndex++
463
+ }
464
+ // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
465
+ if (!attrs.controls && !attrs.autoplay) {
466
+ attrs.controls = 'T'
467
+ }
468
+ // 用数组存储所有可用的 source
469
+ node.src = []
470
+ if (attrs.src) {
471
+ node.src.push(attrs.src)
472
+ attrs.src = undefined
473
+ }
474
+ this.expose()
475
+ }
476
+ // #endif
477
+
478
+ // 处理自闭合标签
479
+ if (close) {
480
+ if (!this.hook(node) || config.ignoreTags[node.name]) {
481
+ // 通过 base 标签设置主域名
482
+ if (node.name === 'base' && !this.options.domain) {
483
+ this.options.domain = attrs.href
484
+ } /* #ifndef APP-PLUS-NVUE */ else if (
485
+ node.name === 'source' &&
486
+ parent &&
487
+ (parent.name === 'video' || parent.name === 'audio') &&
488
+ attrs.src
489
+ ) {
490
+ // 设置 source 标签(仅父节点为 video 或 audio 时有效)
491
+ parent.src.push(attrs.src)
492
+ } /* #endif */
493
+ return
494
+ }
495
+
496
+ // 解析 style
497
+ const styleObj = this.parseStyle(node)
498
+
499
+ // 处理图片
500
+ if (node.name === 'img') {
501
+ if (attrs.src) {
502
+ // 标记 webp
503
+ if (attrs.src.includes('webp')) {
504
+ node.webp = 'T'
505
+ }
506
+ // data url 图片如果没有设置 original-src 默认为不可预览的小图片
507
+ if (attrs.src.includes('data:') && !attrs['original-src']) {
508
+ attrs.ignore = 'T'
509
+ }
510
+ if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
511
+ for (let i = this.stack.length; i--; ) {
512
+ const item = this.stack[i]
513
+ if (item.name === 'a') {
514
+ node.a = item.attrs
515
+ }
516
+ if (
517
+ item.name === 'table' &&
518
+ !node.webp &&
519
+ !attrs.src.includes('cloud://')
520
+ ) {
521
+ if (!styleObj.display || styleObj.display.includes('inline')) {
522
+ node.t = 'inline-block'
523
+ } else {
524
+ node.t = styleObj.display
525
+ }
526
+ styleObj.display = undefined
527
+ }
528
+ // #ifndef H5 || APP-PLUS
529
+ const style = item.attrs.style || ''
530
+ if (
531
+ style.includes('flex:') &&
532
+ !style.includes('flex:0') &&
533
+ !style.includes('flex: 0') &&
534
+ (!styleObj.width || parseInt(styleObj.width) > 100)
535
+ ) {
536
+ styleObj.width = '100% !important'
537
+ styleObj.height = ''
538
+ for (let j = i + 1; j < this.stack.length; j++) {
539
+ this.stack[j].attrs.style = (
540
+ this.stack[j].attrs.style || ''
541
+ ).replace('inline-', '')
542
+ }
543
+ } else if (style.includes('flex') && styleObj.width === '100%') {
544
+ for (let j = i + 1; j < this.stack.length; j++) {
545
+ const style = this.stack[j].attrs.style || ''
546
+ if (
547
+ !style.includes(';width') &&
548
+ !style.includes(' width') &&
549
+ style.indexOf('width') !== 0
550
+ ) {
551
+ styleObj.width = ''
552
+ break
553
+ }
554
+ }
555
+ } else if (style.includes('inline-block')) {
556
+ if (
557
+ styleObj.width &&
558
+ styleObj.width[styleObj.width.length - 1] === '%'
559
+ ) {
560
+ item.attrs.style += ';max-width:' + styleObj.width
561
+ styleObj.width = ''
562
+ } else {
563
+ item.attrs.style += ';max-width:100%'
564
+ }
565
+ }
566
+ // #endif
567
+ item.c = 1
568
+ }
569
+ attrs.i = this.imgList.length.toString()
570
+ let src = attrs['original-src'] || attrs.src
571
+ // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
572
+ if (this.imgList.includes(src)) {
573
+ // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
574
+ let i = src.indexOf('://')
575
+ if (i !== -1) {
576
+ i += 3
577
+ let newSrc = src.substr(0, i)
578
+ for (; i < src.length; i++) {
579
+ if (src[i] === '/') break
580
+ newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]
581
+ }
582
+ newSrc += src.substr(i)
583
+ src = newSrc
584
+ }
585
+ }
586
+ // #endif
587
+ this.imgList.push(src)
588
+ if (!node.t) {
589
+ this.imgList._unloadimgs += 1
590
+ }
591
+ // #ifdef H5 || APP-PLUS
592
+ if (this.options.lazyLoad) {
593
+ attrs['data-src'] = attrs.src
594
+ attrs.src = undefined
595
+ }
596
+ // #endif
597
+ }
598
+ }
599
+ if (styleObj.display === 'inline') {
600
+ styleObj.display = ''
601
+ }
602
+ // #ifndef APP-PLUS-NVUE
603
+ if (attrs.ignore) {
604
+ styleObj['max-width'] = styleObj['max-width'] || '100%'
605
+ attrs.style += ';-webkit-touch-callout:none'
606
+ }
607
+ // #endif
608
+ // 设置的宽度超出屏幕,为避免变形,高度转为自动
609
+ if (parseInt(styleObj.width) > windowWidth) {
610
+ styleObj.height = undefined
611
+ }
612
+ // 记录是否设置了宽高
613
+ if (!isNaN(parseInt(styleObj.width))) {
614
+ node.w = 'T'
615
+ }
616
+ if (
617
+ !isNaN(parseInt(styleObj.height)) &&
618
+ (!styleObj.height.includes('%') ||
619
+ (parent && (parent.attrs.style || '').includes('height')))
620
+ ) {
621
+ node.h = 'T'
622
+ }
623
+ } else if (node.name === 'svg') {
624
+ siblings.push(node)
625
+ this.stack.push(node)
626
+ this.popNode()
627
+ return
628
+ }
629
+ for (const key in styleObj) {
630
+ if (styleObj[key]) {
631
+ attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`
632
+ }
633
+ }
634
+ attrs.style = attrs.style.substr(1) || undefined
635
+ // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
636
+ if (!attrs.style) {
637
+ delete attrs.style
638
+ }
639
+ // #endif
640
+ } else {
641
+ if (
642
+ (node.name === 'pre' ||
643
+ ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) &&
644
+ this.pre !== 2
645
+ ) {
646
+ this.pre = node.pre = 1
647
+ }
648
+ node.children = []
649
+ this.stack.push(node)
650
+ }
651
+
652
+ // 加入节点树
653
+ siblings.push(node)
654
+ }
655
+
656
+ /**
657
+ * @description 解析到标签结束
658
+ * @param {String} name 标签名
659
+ * @private
660
+ */
661
+ Parser.prototype.onCloseTag = function (name) {
662
+ // 依次出栈到匹配为止
663
+ name = this.xml ? name : name.toLowerCase()
664
+ let i
665
+ for (i = this.stack.length; i--; ) {
666
+ if (this.stack[i].name === name) break
667
+ }
668
+ if (i !== -1) {
669
+ while (this.stack.length > i) {
670
+ this.popNode()
671
+ }
672
+ } else if (name === 'p' || name === 'br') {
673
+ const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
674
+ siblings.push({
675
+ name,
676
+ attrs: {
677
+ class: tagSelector[name] || '',
678
+ style: this.tagStyle[name] || ''
679
+ }
680
+ })
681
+ }
682
+ }
683
+
684
+ /**
685
+ * @description 处理标签出栈
686
+ * @private
687
+ */
688
+ Parser.prototype.popNode = function () {
689
+ const node = this.stack.pop()
690
+ let attrs = node.attrs
691
+ const children = node.children
692
+ const parent = this.stack[this.stack.length - 1]
693
+ const siblings = parent ? parent.children : this.nodes
694
+
695
+ if (!this.hook(node) || config.ignoreTags[node.name]) {
696
+ // 获取标题
697
+ if (
698
+ node.name === 'title' &&
699
+ children.length &&
700
+ children[0].type === 'text' &&
701
+ this.options.setTitle
702
+ ) {
703
+ uni.setNavigationBarTitle({
704
+ title: children[0].text
705
+ })
706
+ }
707
+ siblings.pop()
708
+ return
709
+ }
710
+
711
+ if (node.pre && this.pre !== 2) {
712
+ // 是否合并空白符标识
713
+ this.pre = node.pre = undefined
714
+ for (let i = this.stack.length; i--; ) {
715
+ if (this.stack[i].pre) {
716
+ this.pre = 1
717
+ }
718
+ }
719
+ }
720
+
721
+ const styleObj = {}
722
+
723
+ // 转换 svg
724
+ if (node.name === 'svg') {
725
+ if (this.xml > 1) {
726
+ // 多层 svg 嵌套
727
+ this.xml--
728
+ return
729
+ }
730
+ // #ifdef APP-PLUS-NVUE
731
+ ;(function traversal(node) {
732
+ if (node.name) {
733
+ // 调整 svg 的大小写
734
+ node.name = config.svgDict[node.name] || node.name
735
+ for (const item in node.attrs) {
736
+ if (config.svgDict[item]) {
737
+ node.attrs[config.svgDict[item]] = node.attrs[item]
738
+ node.attrs[item] = undefined
739
+ }
740
+ }
741
+ for (let i = 0; i < (node.children || []).length; i++) {
742
+ traversal(node.children[i])
743
+ }
744
+ }
745
+ })(node)
746
+ // #endif
747
+ // #ifndef APP-PLUS-NVUE
748
+ let src = ''
749
+ const style = attrs.style
750
+ attrs.style = ''
751
+ attrs.xmlns = 'http://www.w3.org/2000/svg'
752
+ ;(function traversal(node) {
753
+ if (node.type === 'text') {
754
+ src += node.text
755
+ return
756
+ }
757
+ const name = config.svgDict[node.name] || node.name
758
+ src += '<' + name
759
+ for (const item in node.attrs) {
760
+ const val = node.attrs[item]
761
+ if (val) {
762
+ src += ` ${config.svgDict[item] || item}="${val}"`
763
+ }
764
+ }
765
+ if (!node.children) {
766
+ src += '/>'
767
+ } else {
768
+ src += '>'
769
+ for (let i = 0; i < node.children.length; i++) {
770
+ traversal(node.children[i])
771
+ }
772
+ src += '</' + name + '>'
773
+ }
774
+ })(node)
775
+ node.name = 'img'
776
+ node.attrs = {
777
+ src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
778
+ style,
779
+ ignore: 'T'
780
+ }
781
+ node.children = undefined
782
+ // #endif
783
+ this.xml = false
784
+ return
785
+ }
786
+
787
+ // #ifndef APP-PLUS-NVUE
788
+ // 转换 align 属性
789
+ if (attrs.align) {
790
+ if (node.name === 'table') {
791
+ if (attrs.align === 'center') {
792
+ styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'
793
+ } else {
794
+ styleObj.float = attrs.align
795
+ }
796
+ } else {
797
+ styleObj['text-align'] = attrs.align
798
+ }
799
+ attrs.align = undefined
800
+ }
801
+
802
+ // 转换 dir 属性
803
+ if (attrs.dir) {
804
+ styleObj.direction = attrs.dir
805
+ attrs.dir = undefined
806
+ }
807
+
808
+ // 转换 font 标签的属性
809
+ if (node.name === 'font') {
810
+ if (attrs.color) {
811
+ styleObj.color = attrs.color
812
+ attrs.color = undefined
813
+ }
814
+ if (attrs.face) {
815
+ styleObj['font-family'] = attrs.face
816
+ attrs.face = undefined
817
+ }
818
+ if (attrs.size) {
819
+ let size = parseInt(attrs.size)
820
+ if (!isNaN(size)) {
821
+ if (size < 1) {
822
+ size = 1
823
+ } else if (size > 7) {
824
+ size = 7
825
+ }
826
+ styleObj['font-size'] = [
827
+ 'x-small',
828
+ 'small',
829
+ 'medium',
830
+ 'large',
831
+ 'x-large',
832
+ 'xx-large',
833
+ 'xxx-large'
834
+ ][size - 1]
835
+ }
836
+ attrs.size = undefined
837
+ }
838
+ }
839
+ // #endif
840
+
841
+ // 一些编辑器的自带 class
842
+ if ((attrs.class || '').includes('align-center')) {
843
+ styleObj['text-align'] = 'center'
844
+ }
845
+
846
+ Object.assign(styleObj, this.parseStyle(node))
847
+
848
+ if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {
849
+ styleObj['max-width'] = '100%'
850
+ styleObj['box-sizing'] = 'border-box'
851
+ }
852
+
853
+ // #ifndef APP-PLUS-NVUE
854
+ if (config.blockTags[node.name]) {
855
+ node.name = 'div'
856
+ } else if (!config.trustTags[node.name] && !this.xml) {
857
+ // 未知标签转为 span,避免无法显示
858
+ node.name = 'span'
859
+ }
860
+
861
+ if (
862
+ node.name === 'a' ||
863
+ node.name === 'ad' ||
864
+ // #ifdef H5 || APP-PLUS
865
+ node.name === 'iframe' // eslint-disable-line
866
+ // #endif
867
+ ) {
868
+ this.expose()
869
+ } else if (node.name === 'video') {
870
+ if ((styleObj.height || '').includes('auto')) {
871
+ styleObj.height = undefined
872
+ }
873
+ /* #ifdef APP-PLUS */
874
+ let str = '<video style="width:100%;height:100%"'
875
+ for (const item in attrs) {
876
+ if (attrs[item]) {
877
+ str += ' ' + item + '="' + attrs[item] + '"'
878
+ }
879
+ }
880
+ if (this.options.pauseVideo) {
881
+ str +=
882
+ " onplay=\"this.dispatchEvent(new CustomEvent('vplay',{bubbles:!0}));for(var e=document.getElementsByTagName('video'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()\""
883
+ }
884
+ str += '>'
885
+ for (let i = 0; i < node.src.length; i++) {
886
+ str += '<source src="' + node.src[i] + '">'
887
+ }
888
+ str += '</video>'
889
+ node.html = str
890
+ /* #endif */
891
+ } else if ((node.name === 'ul' || node.name === 'ol') && node.c) {
892
+ // 列表处理
893
+ const types = {
894
+ a: 'lower-alpha',
895
+ A: 'upper-alpha',
896
+ i: 'lower-roman',
897
+ I: 'upper-roman'
898
+ }
899
+ if (types[attrs.type]) {
900
+ attrs.style += ';list-style-type:' + types[attrs.type]
901
+ attrs.type = undefined
902
+ }
903
+ for (let i = children.length; i--; ) {
904
+ if (children[i].name === 'li') {
905
+ children[i].c = 1
906
+ }
907
+ }
908
+ } else if (node.name === 'table') {
909
+ // 表格处理
910
+ // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
911
+ let padding = parseFloat(attrs.cellpadding)
912
+ let spacing = parseFloat(attrs.cellspacing)
913
+ const border = parseFloat(attrs.border)
914
+ const bordercolor = styleObj['border-color']
915
+ const borderstyle = styleObj['border-style']
916
+ if (node.c) {
917
+ // padding 和 spacing 默认 2
918
+ if (isNaN(padding)) {
919
+ padding = 2
920
+ }
921
+ if (isNaN(spacing)) {
922
+ spacing = 2
923
+ }
924
+ }
925
+ if (border) {
926
+ attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`
927
+ }
928
+ if (node.flag && node.c) {
929
+ // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
930
+ styleObj.display = 'grid'
931
+ if (spacing) {
932
+ styleObj['grid-gap'] = spacing + 'px'
933
+ styleObj.padding = spacing + 'px'
934
+ } else if (border) {
935
+ // 无间隔的情况下避免边框重叠
936
+ attrs.style += ';border-left:0;border-top:0'
937
+ }
938
+
939
+ const width = [] // 表格的列宽
940
+ const trList = [] // tr 列表
941
+ const cells = [] // 保存新的单元格
942
+ const map = {} // 被合并单元格占用的格子
943
+
944
+ ;(function traversal(nodes) {
945
+ for (let i = 0; i < nodes.length; i++) {
946
+ if (nodes[i].name === 'tr') {
947
+ trList.push(nodes[i])
948
+ } else {
949
+ traversal(nodes[i].children || [])
950
+ }
951
+ }
952
+ })(children)
953
+
954
+ for (let row = 1; row <= trList.length; row++) {
955
+ let col = 1
956
+ for (let j = 0; j < trList[row - 1].children.length; j++) {
957
+ const td = trList[row - 1].children[j]
958
+ if (td.name === 'td' || td.name === 'th') {
959
+ // 这个格子被上面的单元格占用,则列号++
960
+ while (map[row + '.' + col]) {
961
+ col++
962
+ }
963
+ let style = td.attrs.style || ''
964
+ let start = style.indexOf('width') ? style.indexOf(';width') : 0
965
+ // 提取出 td 的宽度
966
+ if (start !== -1) {
967
+ let end = style.indexOf(';', start + 6)
968
+ if (end === -1) {
969
+ end = style.length
970
+ }
971
+ if (!td.attrs.colspan) {
972
+ width[col] = style.substring(start ? start + 7 : 6, end)
973
+ }
974
+ style = style.substr(0, start) + style.substr(end)
975
+ }
976
+ // 设置竖直对齐
977
+ style += ';display:flex'
978
+ start = style.indexOf('vertical-align')
979
+ if (start !== -1) {
980
+ const val = style.substr(start + 15, 10)
981
+ if (val.includes('middle')) {
982
+ style += ';align-items:center'
983
+ } else if (val.includes('bottom')) {
984
+ style += ';align-items:flex-end'
985
+ }
986
+ } else {
987
+ style += ';align-items:center'
988
+ }
989
+ // 设置水平对齐
990
+ start = style.indexOf('text-align')
991
+ if (start !== -1) {
992
+ const val = style.substr(start + 11, 10)
993
+ if (val.includes('center')) {
994
+ style += ';justify-content: center'
995
+ } else if (val.includes('right')) {
996
+ style += ';justify-content: right'
997
+ }
998
+ }
999
+ style =
1000
+ (border
1001
+ ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` +
1002
+ (spacing ? '' : ';border-right:0;border-bottom:0')
1003
+ : '') +
1004
+ (padding ? `;padding:${padding}px` : '') +
1005
+ ';' +
1006
+ style
1007
+ // 处理列合并
1008
+ if (td.attrs.colspan) {
1009
+ style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`
1010
+ if (!td.attrs.rowspan) {
1011
+ style += `;grid-row-start:${row};grid-row-end:${row + 1}`
1012
+ }
1013
+ col += parseInt(td.attrs.colspan) - 1
1014
+ }
1015
+ // 处理行合并
1016
+ if (td.attrs.rowspan) {
1017
+ style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`
1018
+ if (!td.attrs.colspan) {
1019
+ style += `;grid-column-start:${col};grid-column-end:${col + 1}`
1020
+ }
1021
+ // 记录下方单元格被占用
1022
+ for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {
1023
+ for (
1024
+ let colspan = 0;
1025
+ colspan < (td.attrs.colspan || 1);
1026
+ colspan++
1027
+ ) {
1028
+ map[row + rowspan + '.' + (col - colspan)] = 1
1029
+ }
1030
+ }
1031
+ }
1032
+ if (style) {
1033
+ td.attrs.style = style
1034
+ }
1035
+ cells.push(td)
1036
+ col++
1037
+ }
1038
+ }
1039
+ if (row === 1) {
1040
+ let temp = ''
1041
+ for (let i = 1; i < col; i++) {
1042
+ temp += (width[i] ? width[i] : 'auto') + ' '
1043
+ }
1044
+ styleObj['grid-template-columns'] = temp
1045
+ }
1046
+ }
1047
+ node.children = cells
1048
+ } else {
1049
+ // 没有使用合并单元格的表格通过 table 布局实现
1050
+ if (node.c) {
1051
+ styleObj.display = 'table'
1052
+ }
1053
+ if (!isNaN(spacing)) {
1054
+ styleObj['border-spacing'] = spacing + 'px'
1055
+ }
1056
+ if (border || padding) {
1057
+ // 遍历
1058
+ ;(function traversal(nodes) {
1059
+ for (let i = 0; i < nodes.length; i++) {
1060
+ const td = nodes[i]
1061
+ if (td.name === 'th' || td.name === 'td') {
1062
+ if (border) {
1063
+ td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`
1064
+ }
1065
+ if (padding) {
1066
+ td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`
1067
+ }
1068
+ } else if (td.children) {
1069
+ traversal(td.children)
1070
+ }
1071
+ }
1072
+ })(children)
1073
+ }
1074
+ }
1075
+ // 给表格添加一个单独的横向滚动层
1076
+ if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
1077
+ const table = Object.assign({}, node)
1078
+ node.name = 'div'
1079
+ node.attrs = {
1080
+ style: 'overflow:auto'
1081
+ }
1082
+ node.children = [table]
1083
+ attrs = table.attrs
1084
+ }
1085
+ } else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {
1086
+ for (let i = this.stack.length; i--; ) {
1087
+ if (this.stack[i].name === 'table') {
1088
+ this.stack[i].flag = 1 // 指示含有合并单元格
1089
+ break
1090
+ }
1091
+ }
1092
+ } else if (node.name === 'ruby') {
1093
+ // 转换 ruby
1094
+ node.name = 'span'
1095
+ for (let i = 0; i < children.length - 1; i++) {
1096
+ if (children[i].type === 'text' && children[i + 1].name === 'rt') {
1097
+ children[i] = {
1098
+ name: 'div',
1099
+ attrs: {
1100
+ style: 'display:inline-block;text-align:center'
1101
+ },
1102
+ children: [
1103
+ {
1104
+ name: 'div',
1105
+ attrs: {
1106
+ style: 'font-size:50%;' + (children[i + 1].attrs.style || '')
1107
+ },
1108
+ children: children[i + 1].children
1109
+ },
1110
+ children[i]
1111
+ ]
1112
+ }
1113
+ children.splice(i + 1, 1)
1114
+ }
1115
+ }
1116
+ } else if (node.c) {
1117
+ ;(function traversal(node) {
1118
+ node.c = 2
1119
+ for (let i = node.children.length; i--; ) {
1120
+ const child = node.children[i]
1121
+ // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3
1122
+ if (
1123
+ child.name &&
1124
+ (config.inlineTags[child.name] ||
1125
+ ((child.attrs.style || '').includes('inline') && child.children)) &&
1126
+ !child.c
1127
+ ) {
1128
+ traversal(child)
1129
+ }
1130
+ // #endif
1131
+ if (!child.c || child.name === 'table') {
1132
+ node.c = 1
1133
+ }
1134
+ }
1135
+ })(node)
1136
+ }
1137
+
1138
+ if ((styleObj.display || '').includes('flex') && !node.c) {
1139
+ for (let i = children.length; i--; ) {
1140
+ const item = children[i]
1141
+ if (item.f) {
1142
+ item.attrs.style = (item.attrs.style || '') + item.f
1143
+ item.f = undefined
1144
+ }
1145
+ }
1146
+ }
1147
+ // flex 布局时部分样式需要提取到 rich-text 外层
1148
+ const flex =
1149
+ parent &&
1150
+ ((parent.attrs.style || '').includes('flex') ||
1151
+ (parent.attrs.style || '').includes('grid')) &&
1152
+ // #ifdef MP-WEIXIN
1153
+ // 检查基础库版本 virtualHost 是否可用
1154
+ !(node.c && wx.getNFCAdapter) && // eslint-disable-line
1155
+ // #endif
1156
+ // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
1157
+ !node.c // eslint-disable-line
1158
+ // #endif
1159
+ if (flex) {
1160
+ node.f = ';max-width:100%'
1161
+ }
1162
+
1163
+ if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {
1164
+ mergeNodes(children)
1165
+ }
1166
+ // #endif
1167
+
1168
+ for (const key in styleObj) {
1169
+ if (styleObj[key]) {
1170
+ const val = `;${key}:${styleObj[key].replace(' !important', '')}`
1171
+ /* #ifndef APP-PLUS-NVUE */
1172
+ if (
1173
+ flex &&
1174
+ ((key.includes('flex') && key !== 'flex-direction') ||
1175
+ key === 'align-self' ||
1176
+ key.includes('grid') ||
1177
+ styleObj[key][0] === '-' ||
1178
+ (key.includes('width') && val.includes('%')))
1179
+ ) {
1180
+ node.f += val
1181
+ if (key === 'width') {
1182
+ attrs.style += ';width:100%'
1183
+ }
1184
+ } else /* #endif */ {
1185
+ attrs.style += val
1186
+ }
1187
+ }
1188
+ }
1189
+ attrs.style = attrs.style.substr(1) || undefined
1190
+ // #ifdef (MP-WEIXIN || MP-QQ) && VUE3
1191
+ for (const key in attrs) {
1192
+ if (!attrs[key]) {
1193
+ delete attrs[key]
1194
+ }
1195
+ }
1196
+ // #endif
1197
+ }
1198
+
1199
+ /**
1200
+ * @description 解析到文本
1201
+ * @param {String} text 文本内容
1202
+ */
1203
+ Parser.prototype.onText = function (text) {
1204
+ if (!this.pre) {
1205
+ // 合并空白符
1206
+ let trim = ''
1207
+ let flag
1208
+ for (let i = 0, len = text.length; i < len; i++) {
1209
+ if (!blankChar[text[i]]) {
1210
+ trim += text[i]
1211
+ } else {
1212
+ if (trim[trim.length - 1] !== ' ') {
1213
+ trim += ' '
1214
+ }
1215
+ if (text[i] === '\n' && !flag) {
1216
+ flag = true
1217
+ }
1218
+ }
1219
+ }
1220
+ // 去除含有换行符的空串
1221
+ if (trim === ' ') {
1222
+ if (flag) return
1223
+ // #ifdef VUE3
1224
+ else {
1225
+ const parent = this.stack[this.stack.length - 1]
1226
+ if (parent && parent.name[0] === 't') return
1227
+ }
1228
+ // #endif
1229
+ }
1230
+ text = trim
1231
+ }
1232
+ const node = Object.create(null)
1233
+ node.type = 'text'
1234
+ // #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3
1235
+ node.attrs = {}
1236
+ // #endif
1237
+ node.text = decodeEntity(text)
1238
+ if (this.hook(node)) {
1239
+ // #ifdef MP-WEIXIN
1240
+ if (
1241
+ this.options.selectable === 'force' &&
1242
+ system.includes('iOS') &&
1243
+ !uni.canIUse('rich-text.user-select')
1244
+ ) {
1245
+ this.expose()
1246
+ }
1247
+ // #endif
1248
+ const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes
1249
+ siblings.push(node)
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * @description html 词法分析器
1255
+ * @param {Object} handler 高层处理器
1256
+ */
1257
+ function Lexer(handler) {
1258
+ this.handler = handler
1259
+ }
1260
+
1261
+ /**
1262
+ * @description 执行解析
1263
+ * @param {String} content 要解析的文本
1264
+ */
1265
+ Lexer.prototype.parse = function (content) {
1266
+ this.content = content || ''
1267
+ this.i = 0 // 标记解析位置
1268
+ this.start = 0 // 标记一个单词的开始位置
1269
+ this.state = this.text // 当前状态
1270
+ for (let len = this.content.length; this.i !== -1 && this.i < len; ) {
1271
+ this.state()
1272
+ }
1273
+ }
1274
+
1275
+ /**
1276
+ * @description 检查标签是否闭合
1277
+ * @param {String} method 如果闭合要进行的操作
1278
+ * @returns {Boolean} 是否闭合
1279
+ * @private
1280
+ */
1281
+ Lexer.prototype.checkClose = function (method) {
1282
+ const selfClose = this.content[this.i] === '/'
1283
+ if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {
1284
+ if (method) {
1285
+ this.handler[method](this.content.substring(this.start, this.i))
1286
+ }
1287
+ this.i += selfClose ? 2 : 1
1288
+ this.start = this.i
1289
+ this.handler.onOpenTag(selfClose)
1290
+ if (this.handler.tagName === 'script') {
1291
+ this.i = this.content.indexOf('</', this.i)
1292
+ if (this.i !== -1) {
1293
+ this.i += 2
1294
+ this.start = this.i
1295
+ }
1296
+ this.state = this.endTag
1297
+ } else {
1298
+ this.state = this.text
1299
+ }
1300
+ return true
1301
+ }
1302
+ return false
1303
+ }
1304
+
1305
+ /**
1306
+ * @description 文本状态
1307
+ * @private
1308
+ */
1309
+ Lexer.prototype.text = function () {
1310
+ this.i = this.content.indexOf('<', this.i) // 查找最近的标签
1311
+ if (this.i === -1) {
1312
+ // 没有标签了
1313
+ if (this.start < this.content.length) {
1314
+ this.handler.onText(this.content.substring(this.start, this.content.length))
1315
+ }
1316
+ return
1317
+ }
1318
+ const c = this.content[this.i + 1]
1319
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
1320
+ // 标签开头
1321
+ if (this.start !== this.i) {
1322
+ this.handler.onText(this.content.substring(this.start, this.i))
1323
+ }
1324
+ this.start = ++this.i
1325
+ this.state = this.tagName
1326
+ } else if (c === '/' || c === '!' || c === '?') {
1327
+ if (this.start !== this.i) {
1328
+ this.handler.onText(this.content.substring(this.start, this.i))
1329
+ }
1330
+ const next = this.content[this.i + 2]
1331
+ if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {
1332
+ // 标签结尾
1333
+ this.i += 2
1334
+ this.start = this.i
1335
+ this.state = this.endTag
1336
+ return
1337
+ }
1338
+ // 处理注释
1339
+ let end = '-->'
1340
+ if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {
1341
+ end = '>'
1342
+ }
1343
+ this.i = this.content.indexOf(end, this.i)
1344
+ if (this.i !== -1) {
1345
+ this.i += end.length
1346
+ this.start = this.i
1347
+ }
1348
+ } else {
1349
+ this.i++
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * @description 标签名状态
1355
+ * @private
1356
+ */
1357
+ Lexer.prototype.tagName = function () {
1358
+ if (blankChar[this.content[this.i]]) {
1359
+ // 解析到标签名
1360
+ this.handler.onTagName(this.content.substring(this.start, this.i))
1361
+ while (blankChar[this.content[++this.i]]);
1362
+ if (this.i < this.content.length && !this.checkClose()) {
1363
+ this.start = this.i
1364
+ this.state = this.attrName
1365
+ }
1366
+ } else if (!this.checkClose('onTagName')) {
1367
+ this.i++
1368
+ }
1369
+ }
1370
+
1371
+ /**
1372
+ * @description 属性名状态
1373
+ * @private
1374
+ */
1375
+ Lexer.prototype.attrName = function () {
1376
+ let c = this.content[this.i]
1377
+ if (blankChar[c] || c === '=') {
1378
+ // 解析到属性名
1379
+ this.handler.onAttrName(this.content.substring(this.start, this.i))
1380
+ let needVal = c === '='
1381
+ const len = this.content.length
1382
+ while (++this.i < len) {
1383
+ c = this.content[this.i]
1384
+ if (!blankChar[c]) {
1385
+ if (this.checkClose()) return
1386
+ if (needVal) {
1387
+ // 等号后遇到第一个非空字符
1388
+ this.start = this.i
1389
+ this.state = this.attrVal
1390
+ return
1391
+ }
1392
+ if (this.content[this.i] === '=') {
1393
+ needVal = true
1394
+ } else {
1395
+ this.start = this.i
1396
+ this.state = this.attrName
1397
+ return
1398
+ }
1399
+ }
1400
+ }
1401
+ } else if (!this.checkClose('onAttrName')) {
1402
+ this.i++
1403
+ }
1404
+ }
1405
+
1406
+ /**
1407
+ * @description 属性值状态
1408
+ * @private
1409
+ */
1410
+ Lexer.prototype.attrVal = function () {
1411
+ const c = this.content[this.i]
1412
+ const len = this.content.length
1413
+ if (c === '"' || c === "'") {
1414
+ // 有冒号的属性
1415
+ this.start = ++this.i
1416
+ this.i = this.content.indexOf(c, this.i)
1417
+ if (this.i === -1) return
1418
+ this.handler.onAttrVal(this.content.substring(this.start, this.i))
1419
+ } else {
1420
+ // 没有冒号的属性
1421
+ for (; this.i < len; this.i++) {
1422
+ if (blankChar[this.content[this.i]]) {
1423
+ this.handler.onAttrVal(this.content.substring(this.start, this.i))
1424
+ break
1425
+ } else if (this.checkClose('onAttrVal')) return
1426
+ }
1427
+ }
1428
+ while (blankChar[this.content[++this.i]]);
1429
+ if (this.i < len && !this.checkClose()) {
1430
+ this.start = this.i
1431
+ this.state = this.attrName
1432
+ }
1433
+ }
1434
+
1435
+ /**
1436
+ * @description 结束标签状态
1437
+ * @returns {String} 结束的标签名
1438
+ * @private
1439
+ */
1440
+ Lexer.prototype.endTag = function () {
1441
+ const c = this.content[this.i]
1442
+ if (blankChar[c] || c === '>' || c === '/') {
1443
+ this.handler.onCloseTag(this.content.substring(this.start, this.i))
1444
+ if (c !== '>') {
1445
+ this.i = this.content.indexOf('>', this.i)
1446
+ if (this.i === -1) return
1447
+ }
1448
+ this.start = ++this.i
1449
+ this.state = this.text
1450
+ } else {
1451
+ this.i++
1452
+ }
1453
+ }
1454
+
1455
+ export default Parser