parser.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  1. "use strict";
  2. /**
  3. * @fileoverview html 解析器
  4. */
  5. // 配置
  6. var config = {
  7. // 信任的标签(保持标签名不变)
  8. trustTags: makeMap('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'),
  9. // 块级标签(转为 div,其他的非信任标签转为 span)
  10. blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
  11. // 要移除的标签
  12. ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
  13. // 自闭合的标签
  14. voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
  15. // html 实体
  16. entities: {
  17. lt: '<',
  18. gt: '>',
  19. quot: '"',
  20. apos: "'",
  21. ensp: "\u2002",
  22. emsp: "\u2003",
  23. nbsp: '\xA0',
  24. semi: ';',
  25. ndash: '–',
  26. mdash: '—',
  27. middot: '·',
  28. lsquo: '‘',
  29. rsquo: '’',
  30. ldquo: '“',
  31. rdquo: '”',
  32. bull: '•',
  33. hellip: '…'
  34. },
  35. // 默认的标签样式
  36. tagStyle: {
  37. // #ifndef APP-PLUS-NVUE
  38. address: 'font-style:italic',
  39. big: 'display:inline;font-size:1.2em',
  40. caption: 'display:table-caption;text-align:center',
  41. center: 'text-align:center',
  42. cite: 'font-style:italic',
  43. dd: 'margin-left:40px',
  44. mark: 'background-color:yellow',
  45. pre: 'font-family:monospace;white-space:pre',
  46. s: 'text-decoration:line-through',
  47. small: 'display:inline;font-size:0.8em',
  48. u: 'text-decoration:underline' // #endif
  49. }
  50. };
  51. var windowWidth = uni.getSystemInfoSync().windowWidth;
  52. var blankChar = makeMap(' ,\r,\n,\t,\f');
  53. var idIndex = 0; // #ifdef H5 || APP-PLUS
  54. config.ignoreTags.iframe = void 0;
  55. config.trustTags.iframe = true;
  56. config.ignoreTags.embed = void 0;
  57. config.trustTags.embed = true; // #endif
  58. // #ifdef APP-PLUS-NVUE
  59. config.ignoreTags.source = void 0;
  60. config.ignoreTags.style = void 0; // #endif
  61. /**
  62. * @description 创建 map
  63. * @param {String} str 逗号分隔
  64. */
  65. function makeMap(str) {
  66. var map = Object.create(null),
  67. list = str.split(',');
  68. for (var i = list.length; i--;) {
  69. map[list[i]] = true;
  70. }
  71. return map;
  72. }
  73. /**
  74. * @description 解码 html 实体
  75. * @param {String} str 要解码的字符串
  76. * @param {Boolean} amp 要不要解码 &amp;
  77. * @returns {String} 解码后的字符串
  78. */
  79. function decodeEntity(str, amp) {
  80. var i = str.indexOf('&');
  81. while (i != -1) {
  82. var j = str.indexOf(';', i + 3),
  83. code = void 0;
  84. if (j == -1) break;
  85. if (str[i + 1] == '#') {
  86. // &#123; 形式的实体
  87. code = parseInt((str[i + 2] == 'x' ? '0' : '') + str.substring(i + 2, j));
  88. if (!isNaN(code)) str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1);
  89. } else {
  90. // &nbsp; 形式的实体
  91. code = str.substring(i + 1, j);
  92. if (config.entities[code] || code == 'amp' && amp) str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1);
  93. }
  94. i = str.indexOf('&', i + 1);
  95. }
  96. return str;
  97. }
  98. /**
  99. * @description html 解析器
  100. * @param {Object} vm 组件实例
  101. */
  102. function parser(vm) {
  103. this.options = vm || {};
  104. this.tagStyle = Object.assign(config.tagStyle, this.options.tagStyle);
  105. this.imgList = vm.imgList || [];
  106. this.plugins = vm.plugins || [];
  107. this.attrs = Object.create(null);
  108. this.stack = [];
  109. this.nodes = [];
  110. }
  111. /**
  112. * @description 执行解析
  113. * @param {String} content 要解析的文本
  114. */
  115. parser.prototype.parse = function (content) {
  116. // 插件处理
  117. for (var i = this.plugins.length; i--;) {
  118. if (this.plugins[i].onUpdate) content = this.plugins[i].onUpdate(content, config) || content;
  119. }
  120. new lexer(this).parse(content); // 出栈未闭合的标签
  121. while (this.stack.length) {
  122. this.popNode();
  123. }
  124. return this.nodes;
  125. };
  126. /**
  127. * @description 将标签暴露出来(不被 rich-text 包含)
  128. */
  129. parser.prototype.expose = function () {
  130. // #ifndef APP-PLUS-NVUE
  131. for (var i = this.stack.length; i--;) {
  132. var item = this.stack[i];
  133. if (item.name == 'a' || item.c) return;
  134. item.c = 1;
  135. } // #endif
  136. };
  137. /**
  138. * @description 处理插件
  139. * @param {Object} node 要处理的标签
  140. * @returns {Boolean} 是否要移除此标签
  141. */
  142. parser.prototype.hook = function (node) {
  143. for (var i = this.plugins.length; i--;) {
  144. if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) == false) return false;
  145. }
  146. return true;
  147. };
  148. /**
  149. * @description 将链接拼接上主域名
  150. * @param {String} url 需要拼接的链接
  151. * @returns {String} 拼接后的链接
  152. */
  153. parser.prototype.getUrl = function (url) {
  154. var domain = this.options.domain;
  155. if (url[0] == '/') {
  156. // // 开头的补充协议名
  157. if (url[1] == '/') url = (domain ? domain.split('://')[0] : 'http') + ':' + url; // 否则补充整个域名
  158. else if (domain) url = domain + url;
  159. } else if (domain && !url.includes('data:') && !url.includes('://')) url = domain + '/' + url;
  160. return url;
  161. };
  162. /**
  163. * @description 解析样式表
  164. * @param {Object} node 标签
  165. * @returns {Object}
  166. */
  167. parser.prototype.parseStyle = function (node) {
  168. var attrs = node.attrs,
  169. list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';')),
  170. styleObj = {},
  171. tmp = '';
  172. if (attrs.id) {
  173. // 暴露锚点
  174. if (this.options.useAnchor) this.expose();else if (node.name != 'img' && node.name != 'a' && node.name != 'video' && node.name != 'audio') attrs.id = void 0;
  175. } // #ifndef APP-PLUS-NVUE
  176. // 转换 width 和 height 属性
  177. if (attrs.width) {
  178. styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
  179. attrs.width = void 0;
  180. }
  181. if (attrs.height) {
  182. styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
  183. attrs.height = void 0;
  184. } // #endif
  185. for (var i = 0, len = list.length; i < len; i++) {
  186. var info = list[i].split(':');
  187. if (info.length < 2) continue;
  188. var key = info.shift().trim().toLowerCase(),
  189. value = info.join(':').trim(); // 兼容性的 css 不压缩
  190. if (value[0] == '-' && value.lastIndexOf('-') > 0 || value.includes('safe')) tmp += ";".concat(key, ":").concat(value); // 重复的样式进行覆盖
  191. else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
  192. // 填充链接
  193. if (value.includes('url')) {
  194. var j = value.indexOf('(') + 1;
  195. if (j) {
  196. while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) {
  197. j++;
  198. }
  199. value = value.substr(0, j) + this.getUrl(value.substr(j));
  200. }
  201. } // 转换 rpx(rich-text 内部不支持 rpx)
  202. else if (value.includes('rpx')) value = value.replace(/[0-9.]+\s*rpx/g, function ($) {
  203. return parseFloat($) * windowWidth / 750 + 'px';
  204. });
  205. styleObj[key] = value;
  206. }
  207. }
  208. node.attrs.style = tmp;
  209. return styleObj;
  210. };
  211. /**
  212. * @description 解析到标签名
  213. * @param {String} name 标签名
  214. * @private
  215. */
  216. parser.prototype.onTagName = function (name) {
  217. this.tagName = this.xml ? name : name.toLowerCase();
  218. if (this.tagName == 'svg') this.xml = true; // svg 标签内大小写敏感
  219. };
  220. /**
  221. * @description 解析到属性名
  222. * @param {String} name 属性名
  223. * @private
  224. */
  225. parser.prototype.onAttrName = function (name) {
  226. name = this.xml ? name : name.toLowerCase();
  227. if (name.substr(0, 5) == 'data-') {
  228. // data-src 自动转为 src
  229. if (name == 'data-src') this.attrName = 'src'; // a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
  230. else if (this.tagName == 'img' || this.tagName == 'a') this.attrName = name; // 剩余的移除以减小大小
  231. else this.attrName = void 0;
  232. } else {
  233. this.attrName = name;
  234. this.attrs[name] = 'T'; // boolean 型属性缺省设置
  235. }
  236. };
  237. /**
  238. * @description 解析到属性值
  239. * @param {String} val 属性值
  240. * @private
  241. */
  242. parser.prototype.onAttrVal = function (val) {
  243. var name = this.attrName || ''; // 部分属性进行实体解码
  244. if (name == 'style' || name == 'href') this.attrs[name] = decodeEntity(val, true); // 拼接主域名
  245. else if (name.includes('src')) this.attrs[name] = this.getUrl(decodeEntity(val, true));else if (name) this.attrs[name] = val;
  246. };
  247. /**
  248. * @description 解析到标签开始
  249. * @param {Boolean} selfClose 是否有自闭合标识 />
  250. * @private
  251. */
  252. parser.prototype.onOpenTag = function (selfClose) {
  253. // 拼装 node
  254. var node = Object.create(null);
  255. node.name = this.tagName;
  256. node.attrs = this.attrs;
  257. this.attrs = Object.create(null);
  258. var attrs = node.attrs,
  259. parent = this.stack[this.stack.length - 1],
  260. siblings = parent ? parent.children : this.nodes,
  261. close = this.xml ? selfClose : config.voidTags[node.name]; // 转换 embed 标签
  262. if (node.name == 'embed') {
  263. // #ifndef H5 || APP-PLUS
  264. var src = attrs.src || ''; // 按照后缀名和 type 将 embed 转为 video 或 audio
  265. if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) node.name = 'video';else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) node.name = 'audio';
  266. if (attrs.autostart) attrs.autoplay = 'T';
  267. attrs.controls = 'T'; // #endif
  268. // #ifdef H5 || APP-PLUS
  269. this.expose(); // #endif
  270. } // #ifndef APP-PLUS-NVUE
  271. // 处理音视频
  272. if (node.name == 'video' || node.name == 'audio') {
  273. // 设置 id 以便获取 context
  274. if (node.name == 'video' && !attrs.id) attrs.id = 'v' + idIndex++; // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
  275. if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T'; // 用数组存储所有可用的 source
  276. node.src = [];
  277. if (attrs.src) {
  278. node.src.push(attrs.src);
  279. attrs.src = void 0;
  280. }
  281. this.expose();
  282. } // #endif
  283. // 处理自闭合标签
  284. if (close) {
  285. if (!this.hook(node) || config.ignoreTags[node.name]) {
  286. // 通过 base 标签设置主域名
  287. if (node.name == 'base' && !this.options.domain) this.options.domain = attrs.href; // #ifndef APP-PLUS-NVUE
  288. // 设置 source 标签(仅父节点为 video 或 audio 时有效)
  289. else if (node.name == 'source' && parent && (parent.name == 'video' || parent.name == 'audio') && attrs.src) parent.src.push(attrs.src); // #endif
  290. return;
  291. } // 解析 style
  292. var styleObj = this.parseStyle(node); // 处理图片
  293. if (node.name == 'img') {
  294. if (attrs.src) {
  295. // 标记 webp
  296. if (attrs.src.includes('webp')) node.webp = 'T'; // data url 图片如果没有设置 original-src 默认为不可预览的小图片
  297. if (attrs.src.includes('data:') && !attrs['original-src']) attrs.ignore = 'T';
  298. if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
  299. var i;
  300. for (i = this.stack.length; i--;) {
  301. var item = this.stack[i];
  302. if (item.name == 'a') break; // #ifndef H5 || APP-PLUS
  303. var style = item.attrs.style || '';
  304. if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || !styleObj.width.includes('%'))) {
  305. styleObj.width = '100% !important';
  306. styleObj.height = '';
  307. for (var j = i + 1; j < this.stack.length; j++) {
  308. this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '');
  309. }
  310. } else if (style.includes('flex') && styleObj.width == '100%') {
  311. for (var _j = i + 1; _j < this.stack.length; _j++) {
  312. var _style = this.stack[_j].attrs.style || '';
  313. if (!_style.includes(';width') && !_style.includes(' width') && _style.indexOf('width') != 0) {
  314. styleObj.width = '';
  315. break;
  316. }
  317. }
  318. } else if (style.includes('inline-block')) {
  319. if (styleObj.width && styleObj.width[styleObj.width.length - 1] == '%') {
  320. item.attrs.style += ';max-width:' + styleObj.width;
  321. styleObj.width = '';
  322. } else item.attrs.style += ';max-width:100%';
  323. } // #endif
  324. item.c = 1;
  325. }
  326. if (i == -1) {
  327. attrs.i = this.imgList.length.toString();
  328. var _src = attrs['original-src'] || attrs.src; // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
  329. if (this.imgList.includes(_src)) {
  330. // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
  331. var _i = _src.indexOf('://');
  332. if (_i != -1) {
  333. _i += 3;
  334. var newSrc = _src.substr(0, _i);
  335. for (; _i < _src.length; _i++) {
  336. if (_src[_i] == '/') break;
  337. newSrc += Math.random() > 0.5 ? _src[_i].toUpperCase() : _src[_i];
  338. }
  339. newSrc += _src.substr(_i);
  340. _src = newSrc;
  341. }
  342. } // #endif
  343. this.imgList.push(_src); // #ifdef H5 || APP-PLUS
  344. if (this.options.lazyLoad) {
  345. attrs['data-src'] = attrs.src;
  346. attrs.src = void 0;
  347. } // #endif
  348. } else attrs.ignore = 'T';
  349. }
  350. }
  351. if (styleObj.display == 'inline') styleObj.display = ''; // #ifndef APP-PLUS-NVUE
  352. if (attrs.ignore) {
  353. styleObj['max-width'] = '100%';
  354. attrs.style += ';-webkit-touch-callout:none';
  355. } // #endif
  356. // 设置的宽度超出屏幕,为避免变形,高度转为自动
  357. if (parseInt(styleObj.width) > windowWidth) styleObj.height = void 0; // 记录是否设置了宽高
  358. if (styleObj.width) {
  359. if (styleObj.width.includes('auto')) styleObj.width = '';else {
  360. node.w = 'T';
  361. if (styleObj.height && !styleObj.height.includes('auto')) node.h = 'T';
  362. }
  363. }
  364. } else if (node.name == 'svg') {
  365. siblings.push(node);
  366. this.stack.push(node);
  367. this.popNode();
  368. return;
  369. }
  370. for (var key in styleObj) {
  371. if (styleObj[key]) attrs.style += ";".concat(key, ":").concat(styleObj[key].replace(' !important', ''));
  372. }
  373. attrs.style = attrs.style.substr(1) || void 0;
  374. } else {
  375. if (node.name == 'pre' || (attrs.style || '').includes('white-space') && attrs.style.includes('pre')) this.pre = node.pre = true;
  376. node.children = [];
  377. this.stack.push(node);
  378. } // 加入节点树
  379. siblings.push(node);
  380. };
  381. /**
  382. * @description 解析到标签结束
  383. * @param {String} name 标签名
  384. * @private
  385. */
  386. parser.prototype.onCloseTag = function (name) {
  387. // 依次出栈到匹配为止
  388. name = this.xml ? name : name.toLowerCase();
  389. var i;
  390. for (i = this.stack.length; i--;) {
  391. if (this.stack[i].name == name) break;
  392. }
  393. if (i != -1) {
  394. while (this.stack.length > i) {
  395. this.popNode();
  396. }
  397. } else if (name == 'p' || name == 'br') {
  398. var siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes;
  399. siblings.push({
  400. name: name,
  401. attrs: {}
  402. });
  403. }
  404. };
  405. /**
  406. * @description 处理标签出栈
  407. * @private
  408. */
  409. parser.prototype.popNode = function () {
  410. var node = this.stack.pop(),
  411. attrs = node.attrs,
  412. children = node.children,
  413. parent = this.stack[this.stack.length - 1],
  414. siblings = parent ? parent.children : this.nodes;
  415. if (!this.hook(node) || config.ignoreTags[node.name]) {
  416. // 获取标题
  417. if (node.name == 'title' && children.length && children[0].type == 'text' && this.options.setTitle) uni.setNavigationBarTitle({
  418. title: children[0].text
  419. });
  420. siblings.pop();
  421. return;
  422. }
  423. if (node.pre) {
  424. // 是否合并空白符标识
  425. node.pre = this.pre = void 0;
  426. for (var i = this.stack.length; i--;) {
  427. if (this.stack[i].pre) this.pre = true;
  428. }
  429. }
  430. var styleObj = {}; // 转换 svg
  431. if (node.name == 'svg') {
  432. // #ifndef APP-PLUS-NVUE
  433. var src = '',
  434. style = attrs.style;
  435. attrs.style = '';
  436. attrs.xmlns = 'http://www.w3.org/2000/svg';
  437. (function traversal(node) {
  438. src += '<' + node.name;
  439. for (var item in node.attrs) {
  440. var val = node.attrs[item];
  441. if (val) {
  442. if (item == 'viewbox') item = 'viewBox';
  443. src += " ".concat(item, "=\"").concat(val, "\"");
  444. }
  445. }
  446. if (!node.children) src += '/>';else {
  447. src += '>';
  448. for (var _i2 = 0; _i2 < node.children.length; _i2++) {
  449. traversal(node.children[_i2]);
  450. }
  451. src += '</' + node.name + '>';
  452. }
  453. })(node);
  454. node.name = 'img';
  455. node.attrs = {
  456. src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
  457. style: style,
  458. ignore: 'T'
  459. };
  460. node.children = void 0; // #endif
  461. this.xml = false;
  462. return;
  463. } // #ifndef APP-PLUS-NVUE
  464. // 转换 align 属性
  465. if (attrs.align) {
  466. if (node.name == 'table') {
  467. if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';else styleObj["float"] = attrs.align;
  468. } else styleObj['text-align'] = attrs.align;
  469. attrs.align = void 0;
  470. } // 转换 font 标签的属性
  471. if (node.name == 'font') {
  472. if (attrs.color) {
  473. styleObj.color = attrs.color;
  474. attrs.color = void 0;
  475. }
  476. if (attrs.face) {
  477. styleObj['font-family'] = attrs.face;
  478. attrs.face = void 0;
  479. }
  480. if (attrs.size) {
  481. var size = parseInt(attrs.size);
  482. if (!isNaN(size)) {
  483. if (size < 1) size = 1;else if (size > 7) size = 7;
  484. styleObj['font-size'] = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'][size - 1];
  485. }
  486. attrs.size = void 0;
  487. }
  488. } // #endif
  489. // 一些编辑器的自带 class
  490. if ((attrs["class"] || '').includes('align-center')) styleObj['text-align'] = 'center';
  491. Object.assign(styleObj, this.parseStyle(node));
  492. if (parseInt(styleObj.width) > windowWidth) styleObj['max-width'] = '100%'; // #ifndef APP-PLUS-NVUE
  493. if (config.blockTags[node.name]) node.name = 'div'; // 未知标签转为 span,避免无法显示
  494. else if (!config.trustTags[node.name] && !this.xml) node.name = 'span';
  495. if (node.name == 'a' || node.name == 'ad' // #ifdef H5 || APP-PLUS
  496. || node.name == 'iframe' // #endif
  497. ) this.expose(); // #ifdef APP-PLUS
  498. else if (node.name == 'video') {
  499. var str = '<video style="max-width:100%"';
  500. for (var item in attrs) {
  501. if (attrs[item]) str += ' ' + item + '="' + attrs[item] + '"';
  502. }
  503. if (this.options.pauseVideo) str += ' onplay="for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"';
  504. str += '>';
  505. for (var _i3 = 0; _i3 < node.src.length; _i3++) {
  506. str += '<source src="' + node.src[_i3] + '">';
  507. }
  508. str += '</video>';
  509. node.html = str;
  510. } // #endif
  511. // 列表处理
  512. else if ((node.name == 'ul' || node.name == 'ol') && node.c) {
  513. var types = {
  514. a: 'lower-alpha',
  515. A: 'upper-alpha',
  516. i: 'lower-roman',
  517. I: 'upper-roman'
  518. };
  519. if (types[attrs.type]) {
  520. attrs.style += ';list-style-type:' + types[attrs.type];
  521. attrs.type = void 0;
  522. }
  523. for (var _i4 = children.length; _i4--;) {
  524. if (children[_i4].name == 'li') children[_i4].c = 1;
  525. }
  526. } // 表格处理
  527. else if (node.name == 'table') {
  528. // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
  529. var padding = parseFloat(attrs.cellpadding),
  530. spacing = parseFloat(attrs.cellspacing),
  531. border = parseFloat(attrs.border);
  532. if (node.c) {
  533. // padding 和 spacing 默认 2
  534. if (isNaN(padding)) padding = 2;
  535. if (isNaN(spacing)) spacing = 2;
  536. }
  537. if (border) attrs.style += ';border:' + border + 'px solid gray';
  538. if (node.flag && node.c) {
  539. // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
  540. styleObj.display = 'grid';
  541. if (spacing) {
  542. styleObj['grid-gap'] = spacing + 'px';
  543. styleObj.padding = spacing + 'px';
  544. } // 无间隔的情况下避免边框重叠
  545. else if (border) attrs.style += ';border-left:0;border-top:0';
  546. var width = [],
  547. // 表格的列宽
  548. trList = [],
  549. // tr 列表
  550. cells = [],
  551. // 保存新的单元格
  552. map = {}; // 被合并单元格占用的格子
  553. (function traversal(nodes) {
  554. for (var _i5 = 0; _i5 < nodes.length; _i5++) {
  555. if (nodes[_i5].name == 'tr') trList.push(nodes[_i5]);else traversal(nodes[_i5].children || []);
  556. }
  557. })(children);
  558. for (var row = 1; row <= trList.length; row++) {
  559. var col = 1;
  560. for (var j = 0; j < trList[row - 1].children.length; j++, col++) {
  561. var td = trList[row - 1].children[j];
  562. if (td.name == 'td' || td.name == 'th') {
  563. // 这个格子被上面的单元格占用,则列号++
  564. while (map[row + '.' + col]) {
  565. col++;
  566. }
  567. var _style2 = td.attrs.style || '',
  568. start = _style2.indexOf('width') ? _style2.indexOf(';width') : 0; // 提取出 td 的宽度
  569. if (start != -1) {
  570. var end = _style2.indexOf(';', start + 6);
  571. if (end == -1) end = _style2.length;
  572. if (!td.attrs.colspan) width[col] = _style2.substring(start ? start + 7 : 6, end);
  573. _style2 = _style2.substr(0, start) + _style2.substr(end);
  574. }
  575. _style2 += (border ? ";border:".concat(border, "px solid gray") + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? ";padding:".concat(padding, "px") : ''); // 处理列合并
  576. if (td.attrs.colspan) {
  577. _style2 += ";grid-column-start:".concat(col, ";grid-column-end:").concat(col + parseInt(td.attrs.colspan));
  578. if (!td.attrs.rowspan) _style2 += ";grid-row-start:".concat(row, ";grid-row-end:").concat(row + 1);
  579. col += parseInt(td.attrs.colspan) - 1;
  580. } // 处理行合并
  581. if (td.attrs.rowspan) {
  582. _style2 += ";grid-row-start:".concat(row, ";grid-row-end:").concat(row + parseInt(td.attrs.rowspan));
  583. if (!td.attrs.colspan) _style2 += ";grid-column-start:".concat(col, ";grid-column-end:").concat(col + 1); // 记录下方单元格被占用
  584. for (var k = 1; k < td.attrs.rowspan; k++) {
  585. map[row + k + '.' + col] = 1;
  586. }
  587. }
  588. if (_style2) td.attrs.style = _style2;
  589. cells.push(td);
  590. }
  591. }
  592. if (row == 1) {
  593. var temp = '';
  594. for (var _i6 = 1; _i6 < col; _i6++) {
  595. temp += (width[_i6] ? width[_i6] : 'auto') + ' ';
  596. }
  597. styleObj['grid-template-columns'] = temp;
  598. }
  599. }
  600. node.children = cells;
  601. } else {
  602. // 没有使用合并单元格的表格通过 table 布局实现
  603. if (node.c) styleObj.display = 'table';
  604. if (!isNaN(spacing)) styleObj['border-spacing'] = spacing + 'px';
  605. if (border || padding) {
  606. // 遍历
  607. (function traversal(nodes) {
  608. for (var _i7 = 0; _i7 < nodes.length; _i7++) {
  609. var _td = nodes[_i7];
  610. if (_td.name == 'th' || _td.name == 'td') {
  611. if (border) _td.attrs.style = "border:".concat(border, "px solid gray;").concat(_td.attrs.style || '');
  612. if (padding) _td.attrs.style = "padding:".concat(padding, "px;").concat(_td.attrs.style || '');
  613. } else if (_td.children) traversal(_td.children);
  614. }
  615. })(children);
  616. }
  617. } // 给表格添加一个单独的横向滚动层
  618. if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
  619. var table = Object.assign({}, node);
  620. node.name = 'div';
  621. node.attrs = {
  622. style: 'overflow:auto'
  623. };
  624. node.children = [table];
  625. attrs = table.attrs;
  626. }
  627. } else if ((node.name == 'td' || node.name == 'th') && (attrs.colspan || attrs.rowspan)) {
  628. for (var _i8 = this.stack.length; _i8--;) {
  629. if (this.stack[_i8].name == 'table') {
  630. this.stack[_i8].flag = 1; // 指示含有合并单元格
  631. break;
  632. }
  633. }
  634. } // 转换 ruby
  635. else if (node.name == 'ruby') {
  636. node.name = 'span';
  637. for (var _i9 = 0; _i9 < children.length - 1; _i9++) {
  638. if (children[_i9].type == 'text' && children[_i9 + 1].name == 'rt') {
  639. children[_i9] = {
  640. name: 'div',
  641. attrs: {
  642. style: 'display:inline-block'
  643. },
  644. children: [{
  645. name: 'div',
  646. attrs: {
  647. style: 'font-size:50%;text-align:start'
  648. },
  649. children: children[_i9 + 1].children
  650. }, children[_i9]]
  651. };
  652. children.splice(_i9 + 1, 1);
  653. }
  654. }
  655. } else if (node.c) {
  656. node.c = 2;
  657. for (var _i10 = node.children.length; _i10--;) {
  658. if (!node.children[_i10].c || node.children[_i10].name == 'table') node.c = 1;
  659. }
  660. }
  661. if ((styleObj.display || '').includes('flex') && !node.c) for (var _i11 = children.length; _i11--;) {
  662. var _item = children[_i11];
  663. if (_item.f) {
  664. _item.attrs.style = (_item.attrs.style || '') + _item.f;
  665. _item.f = void 0;
  666. }
  667. } // flex 布局时部分样式需要提取到 rich-text 外层
  668. var flex = parent && (parent.attrs.style || '').includes('flex') // #ifdef MP-WEIXIN
  669. // 检查基础库版本 virtualHost 是否可用
  670. && !(node.c && wx.getNFCAdapter) // #endif
  671. // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
  672. && !node.c; // #endif
  673. if (flex) node.f = ';max-width:100%'; // #endif
  674. for (var key in styleObj) {
  675. if (styleObj[key]) {
  676. var val = ";".concat(key, ":").concat(styleObj[key].replace(' !important', '')); // #ifndef APP-PLUS-NVUE
  677. if (flex && (key.includes('flex') && key != 'flex-direction' || key == 'align-self' || styleObj[key][0] == '-' || key == 'width' && val.includes('%'))) {
  678. node.f += val;
  679. if (key == 'width') attrs.style += ';width:100%';
  680. } else // #endif
  681. attrs.style += val;
  682. }
  683. }
  684. attrs.style = attrs.style.substr(1) || void 0;
  685. };
  686. /**
  687. * @description 解析到文本
  688. * @param {String} text 文本内容
  689. */
  690. parser.prototype.onText = function (text) {
  691. if (!this.pre) {
  692. // 合并空白符
  693. var trim = '',
  694. flag;
  695. for (var i = 0, len = text.length; i < len; i++) {
  696. if (!blankChar[text[i]]) trim += text[i];else {
  697. if (trim[trim.length - 1] != ' ') trim += ' ';
  698. if (text[i] == '\n' && !flag) flag = true;
  699. }
  700. } // 去除含有换行符的空串
  701. if (trim == ' ' && flag) return;
  702. text = trim;
  703. }
  704. var node = Object.create(null);
  705. node.type = 'text';
  706. node.text = decodeEntity(text);
  707. if (this.hook(node)) {
  708. var siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes;
  709. siblings.push(node);
  710. }
  711. };
  712. /**
  713. * @description html 词法分析器
  714. * @param {Object} handler 高层处理器
  715. */
  716. function lexer(handler) {
  717. this.handler = handler;
  718. }
  719. /**
  720. * @description 执行解析
  721. * @param {String} content 要解析的文本
  722. */
  723. lexer.prototype.parse = function (content) {
  724. this.content = content || '';
  725. this.i = 0; // 标记解析位置
  726. this.start = 0; // 标记一个单词的开始位置
  727. this.state = this.text; // 当前状态
  728. for (var len = this.content.length; this.i != -1 && this.i < len;) {
  729. this.state();
  730. }
  731. };
  732. /**
  733. * @description 检查标签是否闭合
  734. * @param {String} method 如果闭合要进行的操作
  735. * @returns {Boolean} 是否闭合
  736. * @private
  737. */
  738. lexer.prototype.checkClose = function (method) {
  739. var selfClose = this.content[this.i] == '/';
  740. if (this.content[this.i] == '>' || selfClose && this.content[this.i + 1] == '>') {
  741. if (method) this.handler[method](this.content.substring(this.start, this.i));
  742. this.i += selfClose ? 2 : 1;
  743. this.start = this.i;
  744. this.handler.onOpenTag(selfClose);
  745. this.state = this.text;
  746. return true;
  747. }
  748. return false;
  749. };
  750. /**
  751. * @description 文本状态
  752. * @private
  753. */
  754. lexer.prototype.text = function () {
  755. this.i = this.content.indexOf('<', this.i); // 查找最近的标签
  756. if (this.i == -1) {
  757. // 没有标签了
  758. if (this.start < this.content.length) this.handler.onText(this.content.substring(this.start, this.content.length));
  759. return;
  760. }
  761. var c = this.content[this.i + 1];
  762. if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
  763. // 标签开头
  764. if (this.start != this.i) this.handler.onText(this.content.substring(this.start, this.i));
  765. this.start = ++this.i;
  766. this.state = this.tagName;
  767. } else if (c == '/' || c == '!' || c == '?') {
  768. if (this.start != this.i) this.handler.onText(this.content.substring(this.start, this.i));
  769. var next = this.content[this.i + 2];
  770. if (c == '/' && (next >= 'a' && next <= 'z' || next >= 'A' && next <= 'Z')) {
  771. // 标签结尾
  772. this.i += 2;
  773. this.start = this.i;
  774. return this.state = this.endTag;
  775. } // 处理注释
  776. var end = '-->';
  777. if (c != '!' || this.content[this.i + 2] != '-' || this.content[this.i + 3] != '-') end = '>';
  778. this.i = this.content.indexOf(end, this.i);
  779. if (this.i != -1) {
  780. this.i += end.length;
  781. this.start = this.i;
  782. }
  783. } else this.i++;
  784. };
  785. /**
  786. * @description 标签名状态
  787. * @private
  788. */
  789. lexer.prototype.tagName = function () {
  790. if (blankChar[this.content[this.i]]) {
  791. // 解析到标签名
  792. this.handler.onTagName(this.content.substring(this.start, this.i));
  793. while (blankChar[this.content[++this.i]]) {
  794. ;
  795. }
  796. if (this.i < this.content.length && !this.checkClose()) {
  797. this.start = this.i;
  798. this.state = this.attrName;
  799. }
  800. } else if (!this.checkClose('onTagName')) this.i++;
  801. };
  802. /**
  803. * @description 属性名状态
  804. * @private
  805. */
  806. lexer.prototype.attrName = function () {
  807. var c = this.content[this.i];
  808. if (blankChar[c] || c == '=') {
  809. // 解析到属性名
  810. this.handler.onAttrName(this.content.substring(this.start, this.i));
  811. var needVal = c == '=',
  812. len = this.content.length;
  813. while (++this.i < len) {
  814. c = this.content[this.i];
  815. if (!blankChar[c]) {
  816. if (this.checkClose()) return;
  817. if (needVal) {
  818. // 等号后遇到第一个非空字符
  819. this.start = this.i;
  820. return this.state = this.attrVal;
  821. }
  822. if (this.content[this.i] == '=') needVal = true;else {
  823. this.start = this.i;
  824. return this.state = this.attrName;
  825. }
  826. }
  827. }
  828. } else if (!this.checkClose('onAttrName')) this.i++;
  829. };
  830. /**
  831. * @description 属性值状态
  832. * @private
  833. */
  834. lexer.prototype.attrVal = function () {
  835. var c = this.content[this.i],
  836. len = this.content.length; // 有冒号的属性
  837. if (c == '"' || c == "'") {
  838. this.start = ++this.i;
  839. this.i = this.content.indexOf(c, this.i);
  840. if (this.i == -1) return;
  841. this.handler.onAttrVal(this.content.substring(this.start, this.i));
  842. } // 没有冒号的属性
  843. else for (; this.i < len; this.i++) {
  844. if (blankChar[this.content[this.i]]) {
  845. this.handler.onAttrVal(this.content.substring(this.start, this.i));
  846. break;
  847. } else if (this.checkClose('onAttrVal')) return;
  848. }
  849. while (blankChar[this.content[++this.i]]) {
  850. ;
  851. }
  852. if (this.i < len && !this.checkClose()) {
  853. this.start = this.i;
  854. this.state = this.attrName;
  855. }
  856. };
  857. /**
  858. * @description 结束标签状态
  859. * @returns {String} 结束的标签名
  860. * @private
  861. */
  862. lexer.prototype.endTag = function () {
  863. var c = this.content[this.i];
  864. if (blankChar[c] || c == '>' || c == '/') {
  865. this.handler.onCloseTag(this.content.substring(this.start, this.i));
  866. if (c != '>') {
  867. this.i = this.content.indexOf('>', this.i);
  868. if (this.i == -1) return;
  869. }
  870. this.start = ++this.i;
  871. this.state = this.text;
  872. } else this.i++;
  873. };
  874. module.exports = parser;