parser-es-module.js 39 KB

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