parser.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. /**
  2. * Parser 富文本组件
  3. * @tutorial https://github.com/jin-yufeng/Parser
  4. * @version 20200630
  5. * @author JinYufeng
  6. * @listens MIT
  7. */
  8. var cache = {},
  9. Parser = require('./libs/MpHtmlParser.js'),
  10. fs = wx.getFileSystemManager && wx.getFileSystemManager();
  11. var dom;
  12. // 计算 cache 的 key
  13. function hash(str) {
  14. for (var i = str.length, val = 5381; i--;)
  15. val += (val << 5) + str.charCodeAt(i);
  16. return val;
  17. }
  18. Component({
  19. options: {
  20. pureDataPattern: /^[acdgtu]|W/
  21. },
  22. data: {
  23. nodes: []
  24. },
  25. properties: {
  26. html: {
  27. type: String,
  28. observer(html) {
  29. this.setContent(html);
  30. }
  31. },
  32. autopause: {
  33. type: Boolean,
  34. value: true
  35. },
  36. autoscroll: Boolean,
  37. autosetTitle: {
  38. type: Boolean,
  39. value: true
  40. },
  41. compress: Number,
  42. domain: String,
  43. lazyLoad: Boolean,
  44. loadingImg: String,
  45. selectable: Boolean,
  46. tagStyle: Object,
  47. showWithAnimation: Boolean,
  48. useAnchor: Boolean,
  49. useCache: Boolean
  50. },
  51. relations: {
  52. '../parser-group/parser-group': {
  53. type: 'ancestor'
  54. }
  55. },
  56. created() {
  57. // 图片数组
  58. this.imgList = [];
  59. this.imgList.setItem = function(i, src) {
  60. if (!i || !src) return;
  61. // 去重
  62. if (src.indexOf('http') == 0 && this.includes(src)) {
  63. var newSrc = '';
  64. for (var j = 0, c; c = src[j]; j++) {
  65. if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
  66. newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
  67. }
  68. newSrc += src.substr(j);
  69. return this[i] = newSrc;
  70. }
  71. this[i] = src;
  72. // 暂存 data src
  73. if (src.includes('data:image')) {
  74. var info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
  75. if (!info) return;
  76. var filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
  77. fs && fs.writeFile({
  78. filePath,
  79. data: info[3],
  80. encoding: info[2],
  81. success: () => this[i] = filePath
  82. })
  83. }
  84. }
  85. this.imgList.each = function(f) {
  86. for (var i = 0, len = this.length; i < len; i++)
  87. this.setItem(i, f(this[i], i, this));
  88. }
  89. if (dom) this.document = new dom(this);
  90. },
  91. detached() {
  92. // 删除暂存
  93. this.imgList.each(src => {
  94. if (src && src.includes(wx.env.USER_DATA_PATH) && fs)
  95. fs.unlink({
  96. filePath: src
  97. })
  98. })
  99. clearInterval(this._timer);
  100. },
  101. methods: {
  102. // 锚点跳转
  103. navigateTo(obj) {
  104. if (!this.data.useAnchor)
  105. return obj.fail && obj.fail({
  106. errMsg: 'Anchor is disabled'
  107. })
  108. this.createSelectorQuery()
  109. .select('.top' + (obj.id ? '>>>#' + obj.id : '')).boundingClientRect()
  110. .selectViewport().scrollOffset().exec(res => {
  111. if (!res[0])
  112. return this.group ? this.group.navigateTo(this.i, obj) :
  113. obj.fail && obj.fail({
  114. errMsg: 'Label not found'
  115. });
  116. obj.scrollTop = res[1].scrollTop + res[0].top + (obj.offset || 0);
  117. wx.pageScrollTo(obj);
  118. })
  119. },
  120. // 获取文本
  121. getText(ns = this.data.html) {
  122. var txt = '';
  123. for (var i = 0, n; n = ns[i++];) {
  124. if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
  125. else if (n.type == 'br') txt += '\n';
  126. else {
  127. // 块级标签前后加换行
  128. var br = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] > '0' && n.name[1] < '7');
  129. if (br && txt && txt[txt.length - 1] != '\n') txt += '\n';
  130. if (n.children) txt += this.getText(n.children);
  131. if (br && txt[txt.length - 1] != '\n') txt += '\n';
  132. else if (n.name == 'td' || n.name == 'th') txt += '\t';
  133. }
  134. }
  135. return txt;
  136. },
  137. // 获取视频 context
  138. getVideoContext(id) {
  139. if (!id) return this.videoContexts;
  140. for (var i = this.videoContexts.length; i--;)
  141. if (this.videoContexts[i].id == id) return this.videoContexts[i];
  142. },
  143. // 渲染富文本
  144. setContent(html, append) {
  145. var nodes, parser = new Parser(html, this.data);
  146. // 缓存读取
  147. if (this.data.useCache) {
  148. var hashVal = hash(html);
  149. if (cache[hashVal]) nodes = cache[hashVal];
  150. else cache[hashVal] = nodes = parser.parse();
  151. } else nodes = parser.parse();
  152. this.triggerEvent('parse', nodes);
  153. var data = {};
  154. if (append)
  155. for (let i = this.data.nodes.length, j = nodes.length; j--;)
  156. data[`nodes[${i + j}]`] = nodes[j];
  157. else data.nodes = nodes;
  158. if (this.showWithAnimation) data.showAm = 'animation: show .5s';
  159. this.setData(data, () => {
  160. this.triggerEvent('load')
  161. });
  162. // 设置标题
  163. if (nodes.title && this.data.autosetTitle)
  164. wx.setNavigationBarTitle({
  165. title: nodes.title
  166. })
  167. this.imgList.length = 0;
  168. this.videoContexts = [];
  169. var ns = this.selectAllComponents('.top,.top>>>._node');
  170. for (let i = 0, n; n = ns[i++];) {
  171. n.top = this;
  172. for (let j = 0, item; item = n.data.nodes[j++];) {
  173. if (item.c) continue;
  174. // 获取图片列表
  175. if (item.name == 'img')
  176. this.imgList.setItem(item.attrs.i, item.attrs.src);
  177. // 音视频控制
  178. else if (item.name == 'video' || item.name == 'audio') {
  179. var ctx;
  180. if (item.name == 'video') ctx = wx.createVideoContext(item.attrs.id, n);
  181. else ctx = n.selectComponent('#' + item.attrs.id);
  182. if (ctx) {
  183. ctx.id = item.attrs.id;
  184. this.videoContexts.push(ctx);
  185. }
  186. }
  187. }
  188. }
  189. var height;
  190. clearInterval(this._timer);
  191. this._timer = setInterval(() => {
  192. this.createSelectorQuery().select('.top').boundingClientRect(res => {
  193. if (!res) return;
  194. this.rect = res;
  195. if (res.height == height) {
  196. this.triggerEvent('ready', res)
  197. clearInterval(this._timer);
  198. }
  199. height = res.height;
  200. }).exec();
  201. }, 350)
  202. }
  203. }
  204. })