dever 4 years ago
parent
commit
6bd2b55482

+ 3 - 3
lib/dever/components/content.vue

@@ -7,7 +7,7 @@
 			</view>
 			
 			<view :class="[skeleton ? 'grace-skeletons' : 'grace-art-content']" v-else-if="v.type == 'html'">
-				<jyf-parser :html="v.content" ref="article"></jyf-parser>
+				<mp-html :content="v.content" ref="article"></mp-html>
 			</view>
 			
 		</block>
@@ -15,7 +15,7 @@
 </template>
 
 <script>
-import jyfParser from "@/lib/jyf-parser/jyf-parser";
+import mpHtml from "@/lib/mp-html/mp-html.vue";
 export default {
 	props: {
 		skeleton : {
@@ -35,7 +35,7 @@ export default {
 		
 	},
 	components: {
-		jyfParser
+		mpHtml
 	},
 }
 </script>

+ 19 - 8
lib/dever/components/position.vue

@@ -84,17 +84,22 @@ export default {
 
 .position-tag.tl {
   top: 88rpx;
-  left: 30rpx;
+  left: 30rpx;
+  top: 5%;
+  left: 5%;
 }
 
 .position-tag.tr {
   top: 88rpx;
-  right: 30rpx;
+  right: 30rpx;
+  top: 5%;
+  left: 5%;
   text-align: right;
 }
 
 .position-tag.tc {
-  top: 88rpx;
+  top: 88rpx;
+  top: 5%;
   left: 50%;
   transform: translate(-50%, 0);
   text-align: center;
@@ -103,7 +108,8 @@ export default {
 .position-tag.cl {
   top: 50%;
   transform: translate(0, -50%);
-  left: 30rpx;
+  left: 30rpx;
+  left: 5%;
 }
 
 .position-tag.cc {
@@ -115,23 +121,28 @@ export default {
 .position-tag.cr {
   top: 50%;
   transform: translate(0, -50%);
-  right: 30rpx;
+  right: 30rpx;
+  right: 5%;
   text-align: right;
 }
 
 .position-tag.bl {
   bottom: 125rpx;
-  left: 30rpx;
+  left: 30rpx;
+  bottom: 10%;
+  left: 5%;
 }
 
 .position-tag.br {
-  bottom: 125rpx;
+  bottom: 125rpx;
+  bottom: 10%;
   right: 30rpx;
   text-align: right;
 }
 
 .position-tag.bc {
-  bottom: 125rpx;
+  bottom: 125rpx;
+  bottom: 10%;
   left: 50%;
   transform: translate(-50%, 0);
   text-align: center;

+ 1 - 1
lib/dever/config.js

@@ -11,7 +11,7 @@ const im = {
 const request = {
 	//	服务器接口地址
 	host: "https://he.shemic.com/",
-	//host: "http://dev.shemic.com/dreamland/",
+	host: "http://dev.shemic.com/dreamland/",
 	
 	//	请求的参数	
 	data: {},

+ 1 - 1
lib/dever/index.js

@@ -30,7 +30,7 @@ var http = {
 		var self = this;
 		if (vue == 'post') {
 			config.request.method = 'POST';
-			//config.request.method = 'GET';
+			config.request.method = 'GET';
 			vue = false;
 		} else {
 			config.request.method = 'GET';

+ 0 - 630
lib/jyf-parser/jyf-parser.vue

@@ -1,630 +0,0 @@
-<template>
-	<view>
-		<slot v-if="!nodes.length" />
-		<!--#ifdef APP-PLUS-NVUE-->
-		<web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
-		<!--#endif-->
-		<!--#ifndef APP-PLUS-NVUE-->
-		<view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
-			<!--#ifdef H5 || MP-360-->
-			<div :id="'rtf'+uid"></div>
-			<!--#endif-->
-			<!--#ifndef H5 || MP-360-->
-			<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
-			<!--#endif-->
-		</view>
-		<!--#endif-->
-	</view>
-</template>
-
-<script>
-	// #ifndef H5 || APP-PLUS-NVUE || MP-360
-	import trees from './libs/trees';
-	var cache = {},
-		// #ifdef MP-WEIXIN || MP-TOUTIAO
-		fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
-		// #endif
-		Parser = require('./libs/MpHtmlParser.js');
-	var dom;
-	// 计算 cache 的 key
-	function hash(str) {
-		for (var i = str.length, val = 5381; i--;)
-			val += (val << 5) + str.charCodeAt(i);
-		return val;
-	}
-	// #endif
-	// #ifdef H5 || APP-PLUS-NVUE || MP-360
-	var windowWidth = uni.getSystemInfoSync().windowWidth,
-		cfg = require('./libs/config.js');
-	// #endif
-	// #ifdef APP-PLUS-NVUE
-	var weexDom = weex.requireModule('dom');
-	// #endif
-	/**
-	 * Parser 富文本组件
-	 * @tutorial https://github.com/jin-yufeng/Parser
-	 * @property {String} html 富文本数据
-	 * @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
-	 * @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
-	 * @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
-	 * @property {Number} compress 压缩等级
-	 * @property {String} domain 图片、视频等链接的主域名
-	 * @property {Boolean} lazyLoad 是否开启图片懒加载
-	 * @property {String} loadingImg 图片加载完成前的占位图
-	 * @property {Boolean} selectable 是否开启长按复制
-	 * @property {Object} tagStyle 标签的默认样式
-	 * @property {Boolean} showWithAnimation 是否使用渐显动画
-	 * @property {Boolean} useAnchor 是否使用锚点
-	 * @property {Boolean} useCache 是否缓存解析结果
-	 * @event {Function} parse 解析完成事件
-	 * @event {Function} load dom 加载完成事件
-	 * @event {Function} ready 所有图片加载完毕事件
-	 * @event {Function} error 错误事件
-	 * @event {Function} imgtap 图片点击事件
-	 * @event {Function} linkpress 链接点击事件
-	 * @author JinYufeng
-	 * @version 20200728
-	 * @listens MIT
-	 */
-	export default {
-		name: 'parser',
-		data() {
-			return {
-				// #ifdef H5 || MP-360
-				uid: this._uid,
-				// #endif
-				// #ifdef APP-PLUS-NVUE
-				height: 1,
-				// #endif
-				// #ifndef APP-PLUS-NVUE
-				showAm: '',
-				// #endif
-				nodes: []
-			}
-		},
-		// #ifndef H5 || APP-PLUS-NVUE || MP-360
-		components: {
-			trees
-		},
-		// #endif
-		props: {
-			html: String,
-			autopause: {
-				type: Boolean,
-				default: true
-			},
-			autoscroll: Boolean,
-			autosetTitle: {
-				type: Boolean,
-				default: true
-			},
-			// #ifndef H5 || APP-PLUS-NVUE || MP-360
-			compress: Number,
-			loadingImg: String,
-			useCache: Boolean,
-			// #endif
-			domain: String,
-			lazyLoad: Boolean,
-			selectable: Boolean,
-			tagStyle: Object,
-			showWithAnimation: Boolean,
-			useAnchor: Boolean
-		},
-		watch: {
-			html(html) {
-				this.setContent(html);
-			}
-		},
-		created() {
-			// 图片数组
-			this.imgList = [];
-			this.imgList.each = function(f) {
-				for (var i = 0, len = this.length; i < len; i++)
-					this.setItem(i, f(this[i], i, this));
-			}
-			this.imgList.setItem = function(i, src) {
-				if (i == void 0 || !src) return;
-				// #ifndef MP-ALIPAY || APP-PLUS
-				// 去重
-				if (src.indexOf('http') == 0 && this.includes(src)) {
-					var newSrc = src.split('://')[0];
-					for (var j = newSrc.length, c; c = src[j]; j++) {
-						if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
-						newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
-					}
-					newSrc += src.substr(j);
-					return this[i] = newSrc;
-				}
-				// #endif
-				this[i] = src;
-				// 暂存 data src
-				if (src.includes('data:image')) {
-					var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
-					if (!info) return;
-					// #ifdef MP-WEIXIN || MP-TOUTIAO
-					filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
-					fs && fs.writeFile({
-						filePath,
-						data: info[3],
-						encoding: info[2],
-						success: () => this[i] = filePath
-					})
-					// #endif
-					// #ifdef APP-PLUS
-					filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
-					var bitmap = new plus.nativeObj.Bitmap();
-					bitmap.loadBase64Data(src, () => {
-						bitmap.save(filePath, {}, () => {
-							bitmap.clear()
-							this[i] = filePath;
-						})
-					})
-					// #endif
-				}
-			}
-		},
-		mounted() {
-			// #ifdef H5 || MP-360
-			this.document = document.getElementById('rtf' + this._uid);
-			// #endif
-			// #ifndef H5 || APP-PLUS-NVUE || MP-360
-			if (dom) this.document = new dom(this);
-			// #endif
-			// #ifdef APP-PLUS-NVUE
-			this.document = this.$refs.web;
-			setTimeout(() => {
-				// #endif
-				if (this.html) this.setContent(this.html);
-				// #ifdef APP-PLUS-NVUE
-			}, 30)
-			// #endif
-		},
-		beforeDestroy() {
-			// #ifdef H5 || MP-360
-			if (this._observer) this._observer.disconnect();
-			// #endif
-			this.imgList.each(src => {
-				// #ifdef APP-PLUS
-				if (src && src.includes('_doc')) {
-					plus.io.resolveLocalFileSystemURL(src, entry => {
-						entry.remove();
-					});
-				}
-				// #endif
-				// #ifdef MP-WEIXIN || MP-TOUTIAO
-				if (src && src.includes(uni.env.USER_DATA_PATH))
-					fs && fs.unlink({
-						filePath: src
-					})
-				// #endif
-			})
-			clearInterval(this._timer);
-		},
-		methods: {
-			// 设置富文本内容
-			setContent(html, append) {
-				// #ifdef APP-PLUS-NVUE
-				if (!html)
-					return this.height = 1;
-				if (append)
-					this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
-						"';document.getElementById('parser').appendChild(b)");
-				else {
-					html =
-						'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
-						this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
-						'</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
-						(this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
-						'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
-					this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
-				}
-				this.$refs.web.evalJs(
-					'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
-					windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
-					'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.src),a.onclick=function(){e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(){var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
-					(this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
-					';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
-					(this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
-					'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
-				)
-				this.nodes = [1];
-				// #endif
-				// #ifdef H5 || MP-360
-				if (!html) {
-					if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
-					return;
-				}
-				var div = document.createElement('div');
-				if (!append) {
-					if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
-					this.rtf = div;
-				} else {
-					if (!this.rtf) this.rtf = div;
-					else this.rtf.appendChild(div);
-				}
-				div.innerHTML = this._handleHtml(html, append);
-				for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
-					style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
-					style.setAttribute('scoped', 'true');
-				}
-				// 懒加载
-				if (!this._observer && this.lazyLoad && IntersectionObserver) {
-					this._observer = new IntersectionObserver(changes => {
-						for (let item, i = 0; item = changes[i++];) {
-							if (item.isIntersecting) {
-								item.target.src = item.target.getAttribute('data-src');
-								item.target.removeAttribute('data-src');
-								this._observer.unobserve(item.target);
-							}
-						}
-					}, {
-						rootMargin: '500px 0px 500px 0px'
-					})
-				}
-				var _ts = this;
-				// 获取标题
-				var title = this.rtf.getElementsByTagName('title');
-				if (title.length && this.autosetTitle)
-					uni.setNavigationBarTitle({
-						title: title[0].innerText
-					})
-				// 图片处理
-				this.imgList.length = 0;
-				var imgs = this.rtf.getElementsByTagName('img');
-				for (let i = 0, j = 0, img; img = imgs[i]; i++) {
-					if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
-						img.style.height = 'auto';
-					var src = img.getAttribute('src');
-					if (this.domain && src) {
-						if (src[0] == '/') {
-							if (src[1] == '/')
-								img.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
-							else img.src = this.domain + src;
-						} else if (!src.includes('://')) img.src = this.domain + '/' + src;
-					}
-					if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
-						img.i = j++;
-						_ts.imgList.push(img.src || img.getAttribute('data-src'));
-						img.onclick = function() {
-							var preview = true;
-							this.ignore = () => preview = false;
-							_ts.$emit('imgtap', this);
-							if (preview) {
-								uni.previewImage({
-									current: this.i,
-									urls: _ts.imgList
-								});
-							}
-						}
-					}
-					img.onerror = function() {
-						if (cfg.errorImg)
-							_ts.imgList[this.i] = this.src = cfg.errorImg;
-						_ts.$emit('error', {
-							source: 'img',
-							target: this
-						});
-					}
-					if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
-						img.setAttribute('data-src', img.src);
-						img.removeAttribute('src');
-						this._observer.observe(img);
-					}
-				}
-				// 链接处理
-				var links = this.rtf.getElementsByTagName('a');
-				for (var link of links) {
-					link.onclick = function() {
-						var jump = true,
-							href = this.getAttribute('href');
-						_ts.$emit('linkpress', {
-							href,
-							ignore: () => jump = false
-						});
-						if (jump && href) {
-							if (href[0] == '#') {
-								if (_ts.useAnchor) {
-									_ts.navigateTo({
-										id: href.substr(1)
-									})
-								}
-							} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
-								return true;
-							else
-								uni.navigateTo({
-									url: href
-								})
-						}
-						return false;
-					}
-				}
-				// 视频处理
-				var videos = this.rtf.getElementsByTagName('video');
-				_ts.videoContexts = videos;
-				for (let video, i = 0; video = videos[i++];) {
-					video.style.maxWidth = '100%';
-					video.onerror = function() {
-						_ts.$emit('error', {
-							source: 'video',
-							target: this
-						});
-					}
-					video.onplay = function() {
-						if (_ts.autopause)
-							for (let item, i = 0; item = _ts.videoContexts[i++];)
-								if (item != this) item.pause();
-					}
-				}
-				// 音频处理
-				var audios = this.rtf.getElementsByTagName('audio');
-				for (var audio of audios)
-					audio.onerror = function() {
-						_ts.$emit('error', {
-							source: 'audio',
-							target: this
-						});
-					}
-				// 表格处理
-				if (this.autoscroll) {
-					var tables = this.rtf.getElementsByTagName('table');
-					for (var table of tables) {
-						let div = document.createElement('div');
-						div.style.overflow = 'scroll';
-						table.parentNode.replaceChild(div, table);
-						div.appendChild(table);
-					}
-				}
-				if (!append) this.document.appendChild(this.rtf);
-				this.$nextTick(() => {
-					this.nodes = [1];
-					this.$emit('load');
-				});
-				setTimeout(() => this.showAm = '', 500);
-				// #endif
-				// #ifndef APP-PLUS-NVUE
-				// #ifndef H5 || MP-360
-				var nodes;
-				if (!html) return this.nodes = [];
-				var parser = new Parser(html, this);
-				// 缓存读取
-				if (this.useCache) {
-					var hashVal = hash(html);
-					if (cache[hashVal])
-						nodes = cache[hashVal];
-					else {
-						nodes = parser.parse();
-						cache[hashVal] = nodes;
-					}
-				} else nodes = parser.parse();
-				this.$emit('parse', nodes);
-				if (append) this.nodes = this.nodes.concat(nodes);
-				else this.nodes = nodes;
-				if (nodes.length && nodes.title && this.autosetTitle)
-					uni.setNavigationBarTitle({
-						title: nodes.title
-					})
-				if (this.imgList) this.imgList.length = 0;
-				this.videoContexts = [];
-				this.$nextTick(() => {
-					(function f(cs) {
-						for (var i = cs.length; i--;) {
-							if (cs[i].top) {
-								cs[i].controls = [];
-								cs[i].init();
-								f(cs[i].$children);
-							}
-						}
-					})(this.$children)
-					this.$emit('load');
-				})
-				// #endif
-				var height;
-				clearInterval(this._timer);
-				this._timer = setInterval(() => {
-					// #ifdef H5 || MP-360
-					this.rect = this.rtf.getBoundingClientRect();
-					// #endif
-					// #ifndef H5 || MP-360
-					uni.createSelectorQuery().in(this)
-						.select('#_top').boundingClientRect().exec(res => {
-							if (!res) return;
-							this.rect = res[0];
-							// #endif
-							if (this.rect.height == height) {
-								this.$emit('ready', this.rect)
-								clearInterval(this._timer);
-							}
-							height = this.rect.height;
-							// #ifndef H5 || MP-360
-						});
-					// #endif
-				}, 350);
-				if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
-				// #endif
-			},
-			// 获取文本内容
-			getText(ns = this.nodes) {
-				var txt = '';
-				// #ifdef APP-PLUS-NVUE
-				txt = this._text;
-				// #endif
-				// #ifdef H5 || MP-360
-				txt = this.rtf.innerText;
-				// #endif
-				// #ifndef H5 || APP-PLUS-NVUE || MP-360
-				for (var i = 0, n; n = ns[i++];) {
-					if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
-						.replace(/&amp;/g, '&');
-					else if (n.type == 'br') txt += '\n';
-					else {
-						// 块级标签前后加换行
-						var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
-							'0' && n.name[1] < '7');
-						if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
-						if (n.children) txt += this.getText(n.children);
-						if (block && txt[txt.length - 1] != '\n') txt += '\n';
-						else if (n.name == 'td' || n.name == 'th') txt += '\t';
-					}
-				}
-				// #endif
-				return txt;
-			},
-			// 锚点跳转
-			in (obj) {
-				if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
-			},
-			navigateTo(obj) {
-				if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
-				// #ifdef APP-PLUS-NVUE
-				if (!obj.id)
-					weexDom.scrollToElement(this.$refs.web);
-				else
-					this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
-						'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
-				obj.success && obj.success();
-				// #endif
-				// #ifndef APP-PLUS-NVUE
-				var d = ' ';
-				// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
-				d = '>>>';
-				// #endif
-				var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
-					'#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
-				if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
-				else selector.selectViewport().scrollOffset();
-				selector.exec(res => {
-					if (!res[0]) return obj.fail && obj.fail('Label not found')
-					var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
-					if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
-					else uni.pageScrollTo({
-						scrollTop,
-						duration: 300
-					})
-					obj.success && obj.success();
-				})
-				// #endif
-			},
-			// 获取视频对象
-			getVideoContext(id) {
-				// #ifndef APP-PLUS-NVUE
-				if (!id) return this.videoContexts;
-				else
-					for (var i = this.videoContexts.length; i--;)
-						if (this.videoContexts[i].id == id) return this.videoContexts[i];
-				// #endif
-			},
-			// #ifdef H5 || APP-PLUS-NVUE || MP-360
-			_handleHtml(html, append) {
-				if (!append) {
-					// 处理 tag-style 和 userAgentStyles
-					var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%}';
-					for (var item in cfg.userAgentStyles)
-						style += `${item}{${cfg.userAgentStyles[item]}}`;
-					for (item in this.tagStyle)
-						style += `${item}{${this.tagStyle[item]}}`;
-					style += '</style>';
-					html = style + html;
-				}
-				// 处理 rpx
-				if (html.includes('rpx'))
-					html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
-				return html;
-			},
-			// #endif
-			// #ifdef APP-PLUS-NVUE
-			_message(e) {
-				// 接收 web-view 消息
-				var d = e.detail.data[0];
-				switch (d.action) {
-					case 'load':
-						this.$emit('load');
-						this.height = d.height;
-						this._text = d.text;
-						break;
-					case 'getTitle':
-						if (this.autosetTitle)
-							uni.setNavigationBarTitle({
-								title: d.title
-							})
-						break;
-					case 'getImgList':
-						this.imgList.length = 0;
-						for (var i = d.imgList.length; i--;)
-							this.imgList.setItem(i, d.imgList[i]);
-						break;
-					case 'preview':
-						var preview = true;
-						d.img.ignore = () => preview = false;
-						this.$emit('imgtap', d.img);
-						if (preview)
-							uni.previewImage({
-								current: d.img.i,
-								urls: this.imgList
-							})
-						break;
-					case 'linkpress':
-						var jump = true,
-							href = d.href;
-						this.$emit('linkpress', {
-							href,
-							ignore: () => jump = false
-						})
-						if (jump && href) {
-							if (href[0] == '#') {
-								if (this.useAnchor)
-									weexDom.scrollToElement(this.$refs.web, {
-										offset: d.offset
-									})
-							} else if (href.includes('://'))
-								plus.runtime.openWeb(href);
-							else
-								uni.navigateTo({
-									url: href
-								})
-						}
-						break;
-					case 'error':
-						if (d.source == 'img' && cfg.errorImg)
-							this.imgList.setItem(d.target.i, cfg.errorImg);
-						this.$emit('error', {
-							source: d.source,
-							target: d.target
-						})
-						break;
-					case 'ready':
-						this.height = d.height;
-						if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
-							this.rect = res[0];
-							this.$emit('ready', res[0]);
-						})
-						break;
-					case 'click':
-						this.$emit('click');
-						this.$emit('tap');
-				}
-			},
-			// #endif
-		}
-	}
-</script>
-
-<style>
-	@keyframes _show {
-		0% {
-			opacity: 0;
-		}
-
-		100% {
-			opacity: 1;
-		}
-	}
-
-	/* #ifdef MP-WEIXIN */
-	:host {
-		display: block;
-		overflow: scroll;
-		-webkit-overflow-scrolling: touch;
-	}
-
-	/* #endif */
-</style>

+ 0 - 97
lib/jyf-parser/libs/CssHandler.js

@@ -1,97 +0,0 @@
-const cfg = require('./config.js'),
-	isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
-
-function CssHandler(tagStyle) {
-	var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
-	for (var item in tagStyle)
-		styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
-	this.styles = styles;
-}
-CssHandler.prototype.getStyle = function(data) {
-	this.styles = new parser(data, this.styles).parse();
-}
-CssHandler.prototype.match = function(name, attrs) {
-	var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
-	if (attrs.class) {
-		var items = attrs.class.split(' ');
-		for (var i = 0, item; item = items[i]; i++)
-			if (tmp = this.styles['.' + item])
-				matched += tmp + ';';
-	}
-	if (tmp = this.styles['#' + attrs.id])
-		matched += tmp + ';';
-	return matched;
-}
-module.exports = CssHandler;
-
-function parser(data, init) {
-	this.data = data;
-	this.floor = 0;
-	this.i = 0;
-	this.list = [];
-	this.res = init;
-	this.state = this.Space;
-}
-parser.prototype.parse = function() {
-	for (var c; c = this.data[this.i]; this.i++)
-		this.state(c);
-	return this.res;
-}
-parser.prototype.section = function() {
-	return this.data.substring(this.start, this.i);
-}
-// 状态机
-parser.prototype.Space = function(c) {
-	if (c == '.' || c == '#' || isLetter(c)) {
-		this.start = this.i;
-		this.state = this.Name;
-	} else if (c == '/' && this.data[this.i + 1] == '*')
-		this.Comment();
-	else if (!cfg.blankChar[c] && c != ';')
-		this.state = this.Ignore;
-}
-parser.prototype.Comment = function() {
-	this.i = this.data.indexOf('*/', this.i) + 1;
-	if (!this.i) this.i = this.data.length;
-	this.state = this.Space;
-}
-parser.prototype.Ignore = function(c) {
-	if (c == '{') this.floor++;
-	else if (c == '}' && !--this.floor) this.state = this.Space;
-}
-parser.prototype.Name = function(c) {
-	if (cfg.blankChar[c]) {
-		this.list.push(this.section());
-		this.state = this.NameSpace;
-	} else if (c == '{') {
-		this.list.push(this.section());
-		this.Content();
-	} else if (c == ',') {
-		this.list.push(this.section());
-		this.Comma();
-	} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
-		this.state = this.Ignore;
-}
-parser.prototype.NameSpace = function(c) {
-	if (c == '{') this.Content();
-	else if (c == ',') this.Comma();
-	else if (!cfg.blankChar[c]) this.state = this.Ignore;
-}
-parser.prototype.Comma = function() {
-	while (cfg.blankChar[this.data[++this.i]]);
-	if (this.data[this.i] == '{') this.Content();
-	else {
-		this.start = this.i--;
-		this.state = this.Name;
-	}
-}
-parser.prototype.Content = function() {
-	this.start = ++this.i;
-	if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
-	var content = this.section();
-	for (var i = 0, item; item = this.list[i++];)
-		if (this.res[item]) this.res[item] += ';' + content;
-		else this.res[item] = content;
-	this.list = [];
-	this.state = this.Space;
-}

+ 0 - 535
lib/jyf-parser/libs/MpHtmlParser.js

@@ -1,535 +0,0 @@
-/**
- * html 解析器
- * @tutorial https://github.com/jin-yufeng/Parser
- * @version 20200728
- * @author JinYufeng
- * @listens MIT
- */
-const cfg = require('./config.js'),
-	blankChar = cfg.blankChar,
-	CssHandler = require('./CssHandler.js'),
-	windowWidth = uni.getSystemInfoSync().windowWidth;
-var emoji;
-
-function MpHtmlParser(data, options = {}) {
-	this.attrs = {};
-	this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
-	this.data = data;
-	this.domain = options.domain;
-	this.DOM = [];
-	this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
-	options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
-	this.options = options;
-	this.state = this.Text;
-	this.STACK = [];
-	// 工具函数
-	this.bubble = () => {
-		for (var i = this.STACK.length, item; item = this.STACK[--i];) {
-			if (cfg.richOnlyTags[item.name]) {
-				if (item.name == 'table' && !Object.hasOwnProperty.call(item, 'c')) item.c = 1;
-				return false;
-			}
-			item.c = 1;
-		}
-		return true;
-	}
-	this.decode = (val, amp) => {
-		var i = -1,
-			j, en;
-		while (1) {
-			if ((i = val.indexOf('&', i + 1)) == -1) break;
-			if ((j = val.indexOf(';', i + 2)) == -1) break;
-			if (val[i + 1] == '#') {
-				en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
-				if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
-			} else {
-				en = val.substring(i + 1, j);
-				if (cfg.entities[en] || en == amp)
-					val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
-			}
-		}
-		return val;
-	}
-	this.getUrl = url => {
-		if (url[0] == '/') {
-			if (url[1] == '/') url = this.options.prot + ':' + url;
-			else if (this.domain) url = this.domain + url;
-		} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
-			url = this.domain + '/' + url;
-		return url;
-	}
-	this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
-	this.section = () => this.data.substring(this.start, this.i);
-	this.parent = () => this.STACK[this.STACK.length - 1];
-	this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
-}
-MpHtmlParser.prototype.parse = function() {
-	if (emoji) this.data = emoji.parseEmoji(this.data);
-	for (var c; c = this.data[this.i]; this.i++)
-		this.state(c);
-	if (this.state == this.Text) this.setText();
-	while (this.STACK.length) this.popNode(this.STACK.pop());
-	return this.DOM;
-}
-// 设置属性
-MpHtmlParser.prototype.setAttr = function() {
-	var name = this.attrName.toLowerCase(),
-		val = this.attrVal;
-	if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
-	else if (val) {
-		if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
-		else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
-		else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
-	}
-	this.attrVal = '';
-	while (blankChar[this.data[this.i]]) this.i++;
-	if (this.isClose()) this.setNode();
-	else {
-		this.start = this.i;
-		this.state = this.AttrName;
-	}
-}
-// 设置文本节点
-MpHtmlParser.prototype.setText = function() {
-	var back, text = this.section();
-	if (!text) return;
-	text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
-	if (back) {
-		this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
-		let j = this.start + text.length;
-		for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
-		return;
-	}
-	if (!this.pre) {
-		// 合并空白符
-		var flag, tmp = [];
-		for (let i = text.length, c; c = text[--i];)
-			if (!blankChar[c]) {
-				tmp.unshift(c);
-				if (!flag) flag = 1;
-			} else {
-				if (tmp[0] != ' ') tmp.unshift(' ');
-				if (c == '\n' && flag == void 0) flag = 0;
-			}
-		if (flag == 0) return;
-		text = tmp.join('');
-	}
-	this.siblings().push({
-		type: 'text',
-		text: this.decode(text)
-	});
-}
-// 设置元素节点
-MpHtmlParser.prototype.setNode = function() {
-	var node = {
-			name: this.tagName.toLowerCase(),
-			attrs: this.attrs
-		},
-		close = cfg.selfClosingTags[node.name];
-	if (this.options.nodes.length) node.type = 'node';
-	this.attrs = {};
-	if (!cfg.ignoreTags[node.name]) {
-		// 处理属性
-		var attrs = node.attrs,
-			style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
-			styleObj = {};
-		if (attrs.id) {
-			if (this.options.compress & 1) attrs.id = void 0;
-			else if (this.options.useAnchor) this.bubble();
-		}
-		if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
-		switch (node.name) {
-			case 'a':
-			case 'ad': // #ifdef APP-PLUS
-			case 'iframe':
-				// #endif
-				this.bubble();
-				break;
-			case 'font':
-				if (attrs.color) {
-					styleObj['color'] = attrs.color;
-					attrs.color = void 0;
-				}
-				if (attrs.face) {
-					styleObj['font-family'] = attrs.face;
-					attrs.face = void 0;
-				}
-				if (attrs.size) {
-					var size = parseInt(attrs.size);
-					if (size < 1) size = 1;
-					else if (size > 7) size = 7;
-					var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
-					styleObj['font-size'] = map[size - 1];
-					attrs.size = void 0;
-				}
-				break;
-			case 'embed':
-				// #ifndef APP-PLUS
-				var src = node.attrs.src || '',
-					type = node.attrs.type || '';
-				if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
-					node.name = 'video';
-				else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
-						'.aac'))
-					node.name = 'audio';
-				else break;
-				if (node.attrs.autostart)
-					node.attrs.autoplay = 'T';
-				node.attrs.controls = 'T';
-				// #endif
-				// #ifdef APP-PLUS
-				this.bubble();
-				break;
-				// #endif
-			case 'video':
-			case 'audio':
-				if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
-				else this[`${node.name}Num`]++;
-				if (node.name == 'video') {
-					if (this.videoNum > 3)
-						node.lazyLoad = 1;
-					if (attrs.width) {
-						styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
-						attrs.width = void 0;
-					}
-					if (attrs.height) {
-						styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
-						attrs.height = void 0;
-					}
-				}
-				if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T';
-				attrs.source = [];
-				if (attrs.src) {
-					attrs.source.push(attrs.src);
-					attrs.src = void 0;
-				}
-				this.bubble();
-				break;
-			case 'td':
-			case 'th':
-				if (attrs.colspan || attrs.rowspan)
-					for (var k = this.STACK.length, item; item = this.STACK[--k];)
-						if (item.name == 'table') {
-							item.c = void 0;
-							break;
-						}
-		}
-		if (attrs.align) {
-			styleObj['text-align'] = attrs.align;
-			attrs.align = void 0;
-		}
-		// 压缩 style
-		var styles = style.split(';');
-		style = '';
-		for (var i = 0, len = styles.length; i < len; i++) {
-			var info = styles[i].split(':');
-			if (info.length < 2) continue;
-			let key = info[0].trim().toLowerCase(),
-				value = info.slice(1).join(':').trim();
-			if (value[0] == '-' || value.includes('safe'))
-				style += `;${key}:${value}`;
-			else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
-				styleObj[key] = value;
-		}
-		if (node.name == 'img') {
-			if (attrs.src && !attrs.ignore) {
-				if (this.bubble())
-					attrs.i = (this.imgNum++).toString();
-				else attrs.ignore = 'T';
-			}
-			if (attrs.ignore) {
-				style += ';-webkit-touch-callout:none';
-				styleObj['max-width'] = '100%';
-			}
-			var width;
-			if (styleObj.width) width = styleObj.width;
-			else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : attrs.width + 'px';
-			if (width) {
-				styleObj.width = width;
-				attrs.width = '100%';
-				if (parseInt(width) > windowWidth) {
-					styleObj.height = '';
-					if (attrs.height) attrs.height = void 0;
-				}
-			}
-			if (styleObj.height) {
-				attrs.height = styleObj.height;
-				styleObj.height = '';
-			} else if (attrs.height && !attrs.height.includes('%'))
-				attrs.height += 'px';
-		}
-		for (var key in styleObj) {
-			var value = styleObj[key];
-			if (!value) continue;
-			if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
-			// 填充链接
-			if (value.includes('url')) {
-				var j = value.indexOf('(');
-				if (j++ != -1) {
-					while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
-					value = value.substr(0, j) + this.getUrl(value.substr(j));
-				}
-			}
-			// 转换 rpx
-			else if (value.includes('rpx'))
-				value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
-			else if (key == 'white-space' && value.includes('pre') && !close)
-				this.pre = node.pre = true;
-			style += `;${key}:${value}`;
-		}
-		style = style.substr(1);
-		if (style) attrs.style = style;
-		if (!close) {
-			node.children = [];
-			if (node.name == 'pre' && cfg.highlight) {
-				this.remove(node);
-				this.pre = node.pre = true;
-			}
-			this.siblings().push(node);
-			this.STACK.push(node);
-		} else if (!cfg.filter || cfg.filter(node, this) != false)
-			this.siblings().push(node);
-	} else {
-		if (!close) this.remove(node);
-		else if (node.name == 'source') {
-			var parent = this.parent();
-			if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
-				parent.attrs.source.push(node.attrs.src);
-		} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
-	}
-	if (this.data[this.i] == '/') this.i++;
-	this.start = this.i + 1;
-	this.state = this.Text;
-}
-// 移除标签
-MpHtmlParser.prototype.remove = function(node) {
-	var name = node.name,
-		j = this.i;
-	// 处理 svg
-	var handleSvg = () => {
-		var src = this.data.substring(j, this.i + 1);
-		if (!node.attrs.xmlns) src = ' xmlns="http://www.w3.org/2000/svg"' + src;
-		var i = j;
-		while (this.data[j] != '<') j--;
-		src = this.data.substring(j, i).replace("viewbox", "viewBox") + src;
-		var parent = this.parent();
-		if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
-			parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
-		this.siblings().push({
-			name: 'img',
-			attrs: {
-				src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
-				style: (/vertical[^;]+/.exec(node.attrs.style) || []).shift(),
-				ignore: 'T'
-			}
-		})
-	}
-	if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
-	while (1) {
-		if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
-			if (name == 'pre' || name == 'svg') this.i = j;
-			else this.i = this.data.length;
-			return;
-		}
-		this.start = (this.i += 2);
-		while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
-		if (this.section().toLowerCase() == name) {
-			// 代码块高亮
-			if (name == 'pre') {
-				this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
-					.substr(this.i - 5);
-				return this.i = j;
-			} else if (name == 'style')
-				this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
-			else if (name == 'title')
-				this.DOM.title = this.data.substring(j + 1, this.i - 7);
-			if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
-			if (name == 'svg') handleSvg();
-			return;
-		}
-	}
-}
-// 节点出栈处理
-MpHtmlParser.prototype.popNode = function(node) {
-	// 空白符处理
-	if (node.pre) {
-		node.pre = this.pre = void 0;
-		for (let i = this.STACK.length; i--;)
-			if (this.STACK[i].pre)
-				this.pre = true;
-	}
-	var siblings = this.siblings(),
-		len = siblings.length,
-		childs = node.children;
-	if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
-		return siblings.pop();
-	var attrs = node.attrs;
-	// 替换一些标签名
-	if (cfg.blockTags[node.name]) node.name = 'div';
-	else if (!cfg.trustTags[node.name]) node.name = 'span';
-	// 处理列表
-	if (node.c && (node.name == 'ul' || node.name == 'ol')) {
-		if ((node.attrs.style || '').includes('list-style:none')) {
-			for (let i = 0, child; child = childs[i++];)
-				if (child.name == 'li')
-					child.name = 'div';
-		} else if (node.name == 'ul') {
-			var floor = 1;
-			for (let i = this.STACK.length; i--;)
-				if (this.STACK[i].name == 'ul') floor++;
-			if (floor != 1)
-				for (let i = childs.length; i--;)
-					childs[i].floor = floor;
-		} else {
-			for (let i = 0, num = 1, child; child = childs[i++];)
-				if (child.name == 'li') {
-					child.type = 'ol';
-					child.num = ((num, type) => {
-						if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
-						if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
-						if (type == 'i' || type == 'I') {
-							num = (num - 1) % 99 + 1;
-							var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
-								ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
-								res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
-							if (type == 'i') return res.toLowerCase();
-							return res;
-						}
-						return num;
-					})(num++, attrs.type) + '.';
-				}
-		}
-	}
-	// 处理表格的边框
-	if (node.name == 'table') {
-		var padding = attrs.cellpadding,
-			spacing = attrs.cellspacing,
-			border = attrs.border;
-		if (node.c) {
-			this.bubble();
-			attrs.style = (attrs.style || '') + ';display:table';
-			if (!padding) padding = 2;
-			if (!spacing) spacing = 2;
-		}
-		if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
-		if (spacing) attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
-		if (border || padding || node.c)
-			(function f(ns) {
-				for (var i = 0, n; n = ns[i]; i++) {
-					if (n.type == 'text') continue;
-					var style = n.attrs.style || '';
-					if (node.c && n.name[0] == 't') {
-						n.c = 1;
-						style += ';display:table-' + (n.name == 'th' || n.name == 'td' ? 'cell' : (n.name == 'tr' ? 'row' : 'row-group'));
-					}
-					if (n.name == 'th' || n.name == 'td') {
-						if (border) style = `border:${border}px solid gray;${style}`;
-						if (padding) style = `padding:${padding}px;${style}`;
-					} else f(n.children || []);
-					if (style) n.attrs.style = style;
-				}
-			})(childs)
-		if (this.options.autoscroll) {
-			var table = Object.assign({}, node);
-			node.name = 'div';
-			node.attrs = {
-				style: 'overflow:scroll'
-			}
-			node.children = [table];
-		}
-	}
-	this.CssHandler.pop && this.CssHandler.pop(node);
-	// 自动压缩
-	if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
-		siblings[len - 1] = childs[0];
-}
-// 状态机
-MpHtmlParser.prototype.Text = function(c) {
-	if (c == '<') {
-		var next = this.data[this.i + 1],
-			isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
-		if (isLetter(next)) {
-			this.setText();
-			this.start = this.i + 1;
-			this.state = this.TagName;
-		} else if (next == '/') {
-			this.setText();
-			if (isLetter(this.data[++this.i + 1])) {
-				this.start = this.i + 1;
-				this.state = this.EndTag;
-			} else this.Comment();
-		} else if (next == '!' || next == '?') {
-			this.setText();
-			this.Comment();
-		}
-	}
-}
-MpHtmlParser.prototype.Comment = function() {
-	var key;
-	if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
-	else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
-	else key = '>';
-	if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
-	else this.i += key.length - 1;
-	this.start = this.i + 1;
-	this.state = this.Text;
-}
-MpHtmlParser.prototype.TagName = function(c) {
-	if (blankChar[c]) {
-		this.tagName = this.section();
-		while (blankChar[this.data[this.i]]) this.i++;
-		if (this.isClose()) this.setNode();
-		else {
-			this.start = this.i;
-			this.state = this.AttrName;
-		}
-	} else if (this.isClose()) {
-		this.tagName = this.section();
-		this.setNode();
-	}
-}
-MpHtmlParser.prototype.AttrName = function(c) {
-	if (c == '=' || blankChar[c] || this.isClose()) {
-		this.attrName = this.section();
-		if (blankChar[c])
-			while (blankChar[this.data[++this.i]]);
-		if (this.data[this.i] == '=') {
-			while (blankChar[this.data[++this.i]]);
-			this.start = this.i--;
-			this.state = this.AttrValue;
-		} else this.setAttr();
-	}
-}
-MpHtmlParser.prototype.AttrValue = function(c) {
-	if (c == '"' || c == "'") {
-		this.start++;
-		if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
-		this.attrVal = this.section();
-		this.i++;
-	} else {
-		for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
-		this.attrVal = this.section();
-	}
-	this.setAttr();
-}
-MpHtmlParser.prototype.EndTag = function(c) {
-	if (blankChar[c] || c == '>' || c == '/') {
-		var name = this.section().toLowerCase();
-		for (var i = this.STACK.length; i--;)
-			if (this.STACK[i].name == name) break;
-		if (i != -1) {
-			var node;
-			while ((node = this.STACK.pop()).name != name) this.popNode(node);
-			this.popNode(node);
-		} else if (name == 'p' || name == 'br')
-			this.siblings().push({
-				name,
-				attrs: {}
-			});
-		this.i = this.data.indexOf('>', this.i);
-		this.start = this.i + 1;
-		if (this.i == -1) this.i = this.data.length;
-		else this.state = this.Text;
-	}
-}
-module.exports = MpHtmlParser;

+ 0 - 80
lib/jyf-parser/libs/config.js

@@ -1,80 +0,0 @@
-/* 配置文件 */
-var cfg = {
-	// 出错占位图
-	errorImg: null,
-	// 过滤器函数
-	filter: null,
-	// 代码高亮函数
-	highlight: null,
-	// 文本处理函数
-	onText: null,
-	// 实体编码列表
-	entities: {
-		quot: '"',
-		apos: "'",
-		semi: ';',
-		nbsp: '\xA0',
-		ensp: '\u2002',
-		emsp: '\u2003',
-		ndash: '–',
-		mdash: '—',
-		middot: '·',
-		lsquo: '‘',
-		rsquo: '’',
-		ldquo: '“',
-		rdquo: '”',
-		bull: '•',
-		hellip: '…'
-	},
-	blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
-	boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
-	// 块级标签,将被转为 div
-	blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
-	// 将被移除的标签
-	ignoreTags: makeMap('area,base,canvas,frame,iframe,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'),
-	// 只能被 rich-text 显示的标签
-	richOnlyTags: makeMap('a,colgroup,fieldset,legend,table'),
-	// 自闭合的标签
-	selfClosingTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
-	// 信任的标签
-	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,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
-	// 默认的标签样式
-	userAgentStyles: {
-		address: 'font-style:italic',
-		big: 'display:inline;font-size:1.2em',
-		blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
-		caption: 'display:table-caption;text-align:center',
-		center: 'text-align:center',
-		cite: 'font-style:italic',
-		dd: 'margin-left:40px',
-		mark: 'background-color:yellow',
-		pre: 'font-family:monospace;white-space:pre;overflow:scroll',
-		s: 'text-decoration:line-through',
-		small: 'display:inline;font-size:0.8em',
-		u: 'text-decoration:underline'
-	}
-}
-
-function makeMap(str) {
-	var map = Object.create(null),
-		list = str.split(',');
-	for (var i = list.length; i--;)
-		map[list[i]] = true;
-	return map;
-}
-
-// #ifdef MP-WEIXIN
-if (wx.canIUse('editor')) {
-	cfg.blockTags.pre = void 0;
-	cfg.ignoreTags.rp = true;
-	Object.assign(cfg.richOnlyTags, makeMap('bdi,bdo,caption,rt,ruby'));
-	Object.assign(cfg.trustTags, makeMap('bdi,bdo,caption,pre,rt,ruby'));
-}
-// #endif
-
-// #ifdef APP-PLUS
-cfg.ignoreTags.iframe = void 0;
-Object.assign(cfg.trustTags, makeMap('embed,iframe'));
-// #endif
-
-module.exports = cfg;

+ 0 - 22
lib/jyf-parser/libs/handler.wxs

@@ -1,22 +0,0 @@
-var inline = {
-	abbr: 1,
-	b: 1,
-	big: 1,
-	code: 1,
-	del: 1,
-	em: 1,
-	i: 1,
-	ins: 1,
-	label: 1,
-	q: 1,
-	small: 1,
-	span: 1,
-	strong: 1,
-	sub: 1,
-	sup: 1
-}
-module.exports = {
-	use: function(item) {
-		return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
-	}
-}

+ 0 - 501
lib/jyf-parser/libs/trees.vue

@@ -1,501 +0,0 @@
-<template>
-	<view :class="'interlayer '+(c||'')" :style="s">
-		<block v-for="(n, i) in nodes" v-bind:key="i">
-			<!--图片-->
-			<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap="imgtap">
-				<rich-text v-if="ctrl[i]!=0" :nodes="[{attrs:{src:loading&&(ctrl[i]||0)<2?loading:(lazyLoad&&!ctrl[i]?placeholder:(ctrl[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
-				<image class="_image" :src="lazyLoad&&!ctrl[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
-				 :show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
-				 @error="error" />
-			</view>
-			<!--文本-->
-			<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
-			<!--#ifndef MP-BAIDU-->
-			<text v-else-if="n.name=='br'">\n</text>
-			<!--#endif-->
-			<!--视频-->
-			<view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&ctrl[i]==undefined" :id="n.attrs.id" :class="'_video '+(n.attrs.class||'')"
-			 :style="n.attrs.style" :data-i="i" @tap="_loadVideo" />
-			<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||ctrl[i]==0"
-			 :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[ctrl[i]||0]"
-			 :unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
-			<!--音频-->
-			<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
-			 :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
-			 :src="n.attrs.source[ctrl[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio"
-			 @error.native="error" @play.native="play" />
-			<!--链接-->
-			<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
-			 :data-attrs="n.attrs" @tap="linkpress">
-				<trees class="_span" c="_span" :nodes="n.children" />
-			</view>
-			<!--广告-->
-			<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
-			<!--列表-->
-			<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex;flex-direction:row'">
-				<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
-				<view v-else class="_ul-bef">
-					<view v-if="n.floor%3==0" class="_ul-p1">█</view>
-					<view v-else-if="n.floor%3==2" class="_ul-p2" />
-					<view v-else class="_ul-p1" style="border-radius:50%">█</view>
-				</view>
-				<trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
-			</view>
-			<!--表格-->
-			<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
-				<view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
-					<view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
-						<trees v-if="tr.name=='td'" :nodes="tr.children" />
-						<trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
-						 :s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
-					</view>
-				</view>
-			</view>
-			<!--#ifdef APP-PLUS-->
-			<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
-			 :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
-			<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
-			<!--#endif-->
-			<!--富文本-->
-			<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
-			<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
-			<!--#endif-->
-			<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
-			<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
-			<!--#endif-->
-			<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
-			 :style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
-		</block>
-	</view>
-</template>
-<script module="handler" lang="wxs" src="./handler.wxs"></script>
-<script>
-	global.Parser = {};
-	import trees from './trees'
-	const errorImg = require('../libs/config.js').errorImg;
-	export default {
-		components: {
-			trees
-		},
-		name: 'trees',
-		data() {
-			return {
-				ctrl: [],
-				placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
-				errorImg,
-				loadVideo: typeof plus == 'undefined',
-				// #ifndef MP-ALIPAY
-				c: '',
-				s: ''
-				// #endif
-			}
-		},
-		props: {
-			nodes: Array,
-			lazyLoad: Boolean,
-			loading: String,
-			// #ifdef MP-ALIPAY
-			c: String,
-			s: String
-			// #endif
-		},
-		mounted() {
-			for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
-			this.init();
-		},
-		// #ifdef APP-PLUS
-		beforeDestroy() {
-			this.observer && this.observer.disconnect();
-		},
-		// #endif
-		methods: {
-			init() {
-				for (var i = this.nodes.length, n; n = this.nodes[--i];) {
-					if (n.name == 'img') {
-						this.top.imgList.setItem(n.attrs.i, n.attrs.src);
-						// #ifdef APP-PLUS
-						if (this.lazyLoad && !this.observer) {
-							this.observer = uni.createIntersectionObserver(this).relativeToViewport({
-								top: 500,
-								bottom: 500
-							});
-							setTimeout(() => {
-								this.observer.observe('._img', res => {
-									if (res.intersectionRatio) {
-										for (var j = this.nodes.length; j--;)
-											if (this.nodes[j].name == 'img')
-												this.$set(this.ctrl, j, 1);
-										this.observer.disconnect();
-									}
-								})
-							}, 0)
-						}
-						// #endif
-					} else if (n.name == 'video' || n.name == 'audio') {
-						var ctx;
-						if (n.name == 'video') {
-							ctx = uni.createVideoContext(n.attrs.id
-								// #ifndef MP-BAIDU
-								, this
-								// #endif
-							);
-						} else if (this.$refs[n.attrs.id])
-							ctx = this.$refs[n.attrs.id][0];
-						if (ctx) {
-							ctx.id = n.attrs.id;
-							this.top.videoContexts.push(ctx);
-						}
-					}
-				}
-				// #ifdef APP-PLUS
-				// APP 上避免 video 错位需要延时渲染
-				setTimeout(() => {
-					this.loadVideo = true;
-				}, 1000)
-				// #endif
-			},
-			play(e) {
-				var contexts = this.top.videoContexts;
-				if (contexts.length > 1 && this.top.autopause)
-					for (var i = contexts.length; i--;)
-						if (contexts[i].id != e.currentTarget.dataset.id)
-							contexts[i].pause();
-			},
-			imgtap(e) {
-				var attrs = e.currentTarget.dataset.attrs;
-				if (!attrs.ignore) {
-					var preview = true,
-						data = {
-							id: e.target.id,
-							src: attrs.src,
-							ignore: () => preview = false
-						};
-					global.Parser.onImgtap && global.Parser.onImgtap(data);
-					this.top.$emit('imgtap', data);
-					if (preview) {
-						var urls = this.top.imgList,
-							current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
-						uni.previewImage({
-							current,
-							urls
-						})
-					}
-				}
-			},
-			loadImg(e) {
-				var i = e.currentTarget.dataset.i;
-				if (this.lazyLoad && !this.ctrl[i]) {
-					// #ifdef QUICKAPP-WEBVIEW
-					this.$set(this.ctrl, i, 0);
-					this.$nextTick(function() {
-						// #endif
-						// #ifndef APP-PLUS
-						this.$set(this.ctrl, i, 1);
-						// #endif
-						// #ifdef QUICKAPP-WEBVIEW
-					})
-					// #endif
-				} else if (this.loading && this.ctrl[i] != 2) {
-					// #ifdef QUICKAPP-WEBVIEW
-					this.$set(this.ctrl, i, 0);
-					this.$nextTick(function() {
-						// #endif
-						this.$set(this.ctrl, i, 2);
-						// #ifdef QUICKAPP-WEBVIEW
-					})
-					// #endif
-				}
-			},
-			linkpress(e) {
-				var jump = true,
-					attrs = e.currentTarget.dataset.attrs;
-				attrs.ignore = () => jump = false;
-				global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
-				this.top.$emit('linkpress', attrs);
-				if (jump) {
-					// #ifdef MP
-					if (attrs['app-id']) {
-						return uni.navigateToMiniProgram({
-							appId: attrs['app-id'],
-							path: attrs.path
-						})
-					}
-					// #endif
-					if (attrs.href) {
-						if (attrs.href[0] == '#') {
-							if (this.top.useAnchor)
-								this.top.navigateTo({
-									id: attrs.href.substring(1)
-								})
-						} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
-							// #ifdef APP-PLUS
-							plus.runtime.openWeb(attrs.href);
-							// #endif
-							// #ifndef APP-PLUS
-							uni.setClipboardData({
-								data: attrs.href,
-								success: () =>
-									uni.showToast({
-										title: '链接已复制'
-									})
-							})
-							// #endif
-						} else
-							uni.navigateTo({
-								url: attrs.href,
-								fail() {
-									uni.switchTab({
-										url: attrs.href,
-									})
-								}
-							})
-					}
-				}
-			},
-			error(e) {
-				var target = e.currentTarget,
-					source = target.dataset.source,
-					i = target.dataset.i;
-				if (source == 'video' || source == 'audio') {
-					// 加载其他 source
-					var index = this.ctrl[i] ? this.ctrl[i].i + 1 : 1;
-					if (index < this.nodes[i].attrs.source.length)
-						this.$set(this.ctrl, i, index);
-					if (e.detail.__args__)
-						e.detail = e.detail.__args__[0];
-				} else if (errorImg && source == 'img') {
-					this.top.imgList.setItem(target.dataset.index, errorImg);
-					this.$set(this.ctrl, i, 3);
-				}
-				this.top && this.top.$emit('error', {
-					source,
-					target,
-					errMsg: e.detail.errMsg
-				});
-			},
-			_loadVideo(e) {
-				this.$set(this.ctrl, e.target.dataset.i, 0);
-			}
-		}
-	}
-</script>
-
-<style>
-	/* 在这里引入自定义样式 */
-
-	/* 链接和图片效果 */
-	._a {
-		display: inline;
-		padding: 1.5px 0 1.5px 0;
-		color: #366092;
-		word-break: break-all;
-	}
-
-	._hover {
-		text-decoration: underline;
-		opacity: 0.7;
-	}
-
-	._img {
-		display: inline-block;
-		max-width: 100%;
-		overflow: hidden;
-	}
-
-	/* #ifdef MP-WEIXIN */
-	:host {
-		display: inline;
-	}
-
-	/* #endif */
-
-	/* #ifndef MP-ALIPAY || APP-PLUS */
-	.interlayer {
-		display: inherit;
-		flex-direction: inherit;
-		flex-wrap: inherit;
-		align-content: inherit;
-		align-items: inherit;
-		justify-content: inherit;
-		width: 100%;
-		white-space: inherit;
-	}
-
-	/* #endif */
-
-	._b,
-	._strong {
-		font-weight: bold;
-	}
-
-	/* #ifndef MP-ALIPAY */
-	._blockquote,
-	._div,
-	._p,
-	._ol,
-	._ul,
-	._li {
-		display: block;
-	}
-	
-	/* #endif */
-
-	._code {
-		font-family: monospace;
-	}
-
-	._del {
-		text-decoration: line-through;
-	}
-
-	._em,
-	._i {
-		font-style: italic;
-	}
-
-	._h1 {
-		font-size: 2em;
-	}
-
-	._h2 {
-		font-size: 1.5em;
-	}
-
-	._h3 {
-		font-size: 1.17em;
-	}
-
-	._h5 {
-		font-size: 0.83em;
-	}
-
-	._h6 {
-		font-size: 0.67em;
-	}
-
-	._h1,
-	._h2,
-	._h3,
-	._h4,
-	._h5,
-	._h6 {
-		display: block;
-		font-weight: bold;
-	}
-
-	._image {
-		display: block;
-		width: 100%;
-		height: 360px;
-		margin-top: -360px;
-		opacity: 0;
-	}
-
-	._ins {
-		text-decoration: underline;
-	}
-
-	._li {
-		flex: 1;
-		width: 0;
-	}
-
-	._ol-bef {
-		width: 36px;
-		margin-right: 5px;
-		text-align: right;
-	}
-
-	._ul-bef {
-		display: block;
-		margin: 0 12px 0 23px;
-		line-height: normal;
-	}
-
-	._ol-bef,
-	._ul-bef {
-		flex: none;
-		user-select: none;
-	}
-
-	._ul-p1 {
-		display: inline-block;
-		width: 0.3em;
-		height: 0.3em;
-		overflow: hidden;
-		line-height: 0.3em;
-	}
-
-	._ul-p2 {
-		display: inline-block;
-		width: 0.23em;
-		height: 0.23em;
-		border: 0.05em solid black;
-		border-radius: 50%;
-	}
-
-	._q::before {
-		content: '"';
-	}
-
-	._q::after {
-		content: '"';
-	}
-
-	._sub {
-		font-size: smaller;
-		vertical-align: sub;
-	}
-
-	._sup {
-		font-size: smaller;
-		vertical-align: super;
-	}
-
-	/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
-	._abbr,
-	._b,
-	._code,
-	._del,
-	._em,
-	._i,
-	._ins,
-	._label,
-	._q,
-	._span,
-	._strong,
-	._sub,
-	._sup {
-		display: inline;
-	}
-
-	/* #endif */
-
-	/* #ifdef MP-WEIXIN || MP-QQ */
-	.__bdo,
-	.__bdi,
-	.__ruby,
-	.__rt {
-		display: inline-block;
-	}
-
-	/* #endif */
-	._video {
-		position: relative;
-		display: inline-block;
-		width: 300px;
-		height: 225px;
-		background-color: black;
-	}
-
-	._video::after {
-		position: absolute;
-		top: 50%;
-		left: 50%;
-		margin: -15px 0 0 -15px;
-		content: '';
-		border-color: transparent transparent transparent white;
-		border-style: solid;
-		border-width: 15px 0 15px 30px;
-	}
-</style>

+ 405 - 0
lib/mp-html/mp-html.vue

@@ -0,0 +1,405 @@
+<template>
+  <view id="_root" :class="(selectable?'_select ':'')+'_root'">
+    <slot v-if="!nodes[0]" />
+    <!-- #ifndef APP-PLUS-NVUE -->
+    <node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu]" />
+    <!-- #endif -->
+    <!-- #ifdef APP-PLUS-NVUE -->
+    <web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
+    <!-- #endif -->
+  </view>
+</template>
+
+<script>
+/**
+ * mp-html v2.0.4
+ * @description 富文本组件
+ * @tutorial https://github.com/jin-yufeng/mp-html
+ * @property {String} content 用于渲染的 html 字符串
+ * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
+ * @property {String} domain 主域名,用于拼接链接
+ * @property {String} error-img 图片出错时的占位图链接
+ * @property {Boolean} lazy-load 是否开启图片懒加载
+ * @property {string} loading-img 图片加载过程中的占位图链接
+ * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
+ * @property {Boolean} preview-img 是否允许图片被点击时自动预览
+ * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
+ * @property {Boolean} selectable 是否开启长按复制
+ * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
+ * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
+ * @property {Object} tag-style 标签的默认样式
+ * @property {Boolean | Number} use-anchor 是否使用锚点链接
+ * @event {Function} load dom 结构加载完毕时触发
+ * @event {Function} ready 所有图片加载完毕时触发
+ * @event {Function} imgTap 图片被点击时触发
+ * @event {Function} linkTap 链接被点击时触发
+ * @event {Function} error 媒体加载出错时触发
+ */
+const plugins=[]
+const parser = require('./parser')
+// #ifndef APP-PLUS-NVUE
+import node from './node/node'
+// #endif
+// #ifdef APP-PLUS-NVUE
+const dom = weex.requireModule('dom')
+// #endif
+export default {
+  name: 'mp-html',
+  data() {
+    return {
+      nodes: [],
+      // #ifdef APP-PLUS-NVUE
+      height: 0
+      // #endif
+    }
+  },
+  props: {
+    // #ifdef APP-PLUS-NVUE
+    bgColor: String,
+    // #endif
+    content: String,
+    copyLink: {
+      type: Boolean,
+      default: true
+    },
+    domain: String,
+    errorImg: {
+      type: String,
+      default: ''
+    },
+    lazyLoad: {
+      type: Boolean,
+      default: false
+    },
+    loadingImg: {
+      type: String,
+      default: ''
+    },
+    pauseVideo: {
+      type: Boolean,
+      default: true
+    },
+    previewImg: {
+      type: Boolean,
+      default: true
+    },
+    scrollTable: Boolean,
+    selectable: Boolean,
+    setTitle: {
+      type: Boolean,
+      default: true
+    },
+    showImgMenu: {
+      type: Boolean,
+      default: true
+    },
+    tagStyle: Object,
+    useAnchor: null
+  },
+  // #ifndef APP-PLUS-NVUE
+  components: {
+    node
+  },
+  // #endif
+  watch: {
+    content(content) {
+      this.setContent(content)
+    }
+  },
+  created() {
+    this.plugins = []
+    for (var i = plugins.length; i--;)
+      this.plugins.push(new plugins[i](this))
+  },
+  mounted() {
+    if (this.content && !this.nodes.length)
+      this.setContent(this.content)
+  },
+  beforeDestroy() {
+    this._hook('onDetached')
+    clearInterval(this._timer)
+  },
+  methods: {
+    /**
+     * @description 将锚点跳转的范围限定在一个 scroll-view 内
+     * @param {Object} page scroll-view 所在页面的示例
+     * @param {String} selector scroll-view 的选择器
+     * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
+     */
+    in(page, selector, scrollTop) {
+      // #ifndef APP-PLUS-NVUE
+      if (page && selector && scrollTop)
+        this._in = {
+          page,
+          selector,
+          scrollTop
+        }
+      // #endif
+    },
+
+    /**
+     * @description 锚点跳转
+     * @param {String} id 要跳转的锚点 id
+     * @param {Number} offset 跳转位置的偏移量
+     * @returns {Promise}
+     */
+    navigateTo(id, offset) {
+      return new Promise((resolve, reject) => {
+        if (!this.useAnchor)
+          return reject('Anchor is disabled')
+        offset = offset || parseInt(this.useAnchor) || 0
+        // #ifdef APP-PLUS-NVUE
+        if (!id) {
+          dom.scrollToElement(this.$refs.web, {
+            offset
+          })
+          resolve()
+        } else {
+          this._navigateTo = {
+            resolve,
+            reject,
+            offset
+          }
+          this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
+        }
+        // #endif
+        // #ifndef APP-PLUS-NVUE
+        var deep = ' '
+        // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
+        deep = '>>>'
+        // #endif
+        var selector = uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this._in ? this._in.page : this)
+          // #endif
+          .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
+        if (this._in)
+          selector.select(this._in.selector).scrollOffset()
+            .select(this._in.selector).boundingClientRect() // 获取 scroll-view 的位置和滚动距离
+        else
+          selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
+        selector.exec(res => {
+          if (!res[0])
+            return reject('Label not found')
+          var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
+          if (this._in)
+            // scroll-view 跳转
+            this._in.page[this._in.scrollTop] = scrollTop
+          else
+            // 页面跳转
+            uni.pageScrollTo({
+              scrollTop,
+              duration: 300
+            })
+          resolve()
+        })
+        // #endif
+      })
+    },
+
+    /**
+     * @description 获取文本内容
+     * @return {String}
+     */
+    getText() {
+      var text = '';
+      (function traversal(nodes) {
+        for (var i = 0; i < nodes.length; i++) {
+          var node = nodes[i]
+          if (node.type == 'text')
+            text += node.text.replace(/&amp;/g, '&')
+          else if (node.name == 'br')
+            text += '\n'
+          else {
+            // 块级标签前后加换行
+            var isBlock = node.name == 'p' || node.name == 'div' || node.name == 'tr' || node.name == 'li' || (node.name[0] == 'h' && node.name[1] > '0' && node.name[1] < '7')
+            if (isBlock && text && text[text.length - 1] != '\n')
+              text += '\n'
+            // 递归获取子节点的文本
+            if (node.children)
+              traversal(node.children)
+            if (isBlock && text[text.length - 1] != '\n')
+              text += '\n'
+            else if (node.name == 'td' || node.name == 'th')
+              text += '\t'
+          }
+        }
+      })(this.nodes)
+      return text
+    },
+
+    /**
+     * @description 获取内容大小和位置
+     * @return {Promise}
+     */
+    getRect() {
+      return new Promise((resolve, reject) => {
+        uni.createSelectorQuery()
+          // #ifndef MP-ALIPAY
+          .in(this)
+          // #endif
+          .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject('Root label not found'))
+      })
+    },
+
+    /**
+     * @description 设置内容
+     * @param {String} content html 内容
+     * @param {Boolean} append 是否在尾部追加
+     */
+    setContent(content, append) {
+      if (!append || !this.imgList)
+        this.imgList = []
+      var nodes = new parser(this).parse(content)
+      // #ifdef APP-PLUS-NVUE
+      if (this._ready)
+        this._set(nodes, append)
+      // #endif
+      this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
+
+      // #ifndef APP-PLUS-NVUE
+      this._videos = []
+      this.$nextTick(() => {
+        this._hook('onLoad')
+        this.$emit('load')
+      })
+
+      // 等待图片加载完毕
+      var height
+      clearInterval(this._timer)
+      this._timer = setInterval(() => {
+        this.getRect().then(rect => {
+          // 350ms 总高度无变化就触发 ready 事件
+          if (rect.height == height) {
+            this.$emit('ready', rect)
+            clearInterval(this._timer)
+          }
+          height = rect.height
+        }).catch(() => { })
+      }, 350)
+      // #endif
+    },
+
+    /**
+     * @description 调用插件钩子函数
+     */
+    _hook(name) {
+      for (var i = plugins.length; i--;)
+        if (this.plugins[i][name])
+          this.plugins[i][name]()
+    },
+
+    // #ifdef APP-PLUS-NVUE
+    /**
+     * @description 设置内容
+     */
+    _set(nodes, append) {
+      this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.bgColor, this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
+    },
+
+    /**
+     * @description 接收到 web-view 消息
+     */
+    _onMessage(e) {
+      var message = e.detail.data[0]
+      switch (message.action) {
+        // web-view 初始化完毕
+        case 'onJSBridgeReady':
+          this._ready = true
+          if (this.nodes)
+            this._set(this.nodes)
+          break
+        // 内容 dom 加载完毕
+        case 'onLoad':
+          this.height = message.height
+          this._hook('onLoad')
+          this.$emit('load')
+          break
+        // 所有图片加载完毕
+        case 'onReady':
+          this.getRect().then(res => {
+            this.$emit('ready', res)
+          }).catch(() => { })
+          break
+        // 总高度发生变化
+        case 'onHeightChange':
+          this.height = message.height
+          break
+        // 图片点击
+        case 'onImgTap':
+          this.$emit('imgtap', message.attrs)
+          if (this.previewImg)
+            uni.previewImage({
+              current: parseInt(message.attrs.i),
+              urls: this.imgList
+            })
+          break
+        // 链接点击
+        case 'onLinkTap':
+          var href = message.attrs.href
+          this.$emit('linktap', message.attrs)
+          if (href) {
+            // 锚点跳转
+            if (href[0] == '#') {
+              if (this.useAnchor)
+                dom.scrollToElement(this.$refs.web, {
+                  offset: message.offset
+                })
+            }
+            // 打开外链
+            else if (href.includes('://')) {
+              if (this.copyLink)
+                plus.runtime.openWeb(href)
+            }
+            else
+              uni.navigateTo({
+                url: href,
+                fail() {
+                  wx.switchTab({
+                    url: href
+                  })
+                }
+              })
+          }
+          break
+        // 获取到锚点的偏移量
+        case 'getOffset':
+          if (typeof message.offset == 'number') {
+            dom.scrollToElement(this.$refs.web, {
+              offset: message.offset + this._navigateTo.offset
+            })
+            this._navigateTo.resolve()
+          } else
+            this._navigateTo.reject('Label not found')
+          break
+        // 点击
+        case 'onClick':
+          this.$emit('tap')
+          break
+        // 出错
+        case 'onError':
+          this.$emit('error', {
+            source: message.source,
+            attrs: message.attrs
+          })
+      }
+    }
+    // #endif
+  }
+}
+</script>
+
+<style>
+/* #ifndef APP-PLUS-NVUE */
+/* 根节点样式 */
+._root {
+  overflow: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 长按复制 */
+._select {
+  user-select: text;
+}
+/* #endif */
+</style>

+ 499 - 0
lib/mp-html/node/node.vue

@@ -0,0 +1,499 @@
+<template>
+  <view :id="attrs.id" :class="'_'+name+' '+attrs.class" :style="attrs.style">
+    <block v-for="(n, i) in childs" v-bind:key="i">
+      <!-- 图片 -->
+      <!-- 占位图 -->
+      <image v-if="n.name=='img'&&((opts[1]&&!ctrl[i])||ctrl[i]<0)" class="_img" :style="n.attrs.style" :src="ctrl[i]<0?opts[2]:opts[1]" mode="widthFix" />
+      <!-- 显示图片 -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <img v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap"/>
+      <!-- #endif -->
+      <!-- #ifndef H5 || APP-PLUS -->
+      <image v-if="n.name=='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]==-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style" :src="n.attrs.src" :mode="n.h?'':'widthFix'" :lazy-load="opts[0]" :webp="n.webp" :show-menu-by-longpress="opts[3]&&!n.attrs.ignore" :image-menu-prevent="!opts[3]||n.attrs.ignore" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" />
+      <!-- #endif -->
+      <!-- 文本 -->
+      <!-- #ifndef MP-BAIDU -->
+      <text v-else-if="n.type=='text'" decode>{{n.text}}</text>
+      <!-- #endif -->
+      <text v-else-if="n.name=='br'">\n</text>
+      <!-- 链接 -->
+      <view v-else-if="n.name=='a'" :id="n.attrs.id" :class="(n.attrs.href?'_a ':'')+n.attrs.class" hover-class="_hover" :style="'display:inline;'+n.attrs.style" :data-i="i" @tap.stop="linkTap">
+        <node name="span" :childs="n.children" :opts="opts" style="display:inherit" />
+      </view>
+      <!-- 视频 -->
+      <!-- #ifdef APP-PLUS -->
+      <view v-else-if="n.html" :id="n.attrs.id" :class="'_video '+n.attrs.class" :style="n.attrs.style" v-html="n.html" />
+      <!-- #endif -->
+      <!-- #ifndef APP-PLUS -->
+      <video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <!-- #ifdef H5 || APP-PLUS -->
+      <iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder" :src="n.attrs.src" />
+      <embed v-else-if="n.name=='embed'" :style="n.attrs.style" :src="n.attrs.src" />
+      <!-- #endif -->
+      <!-- #ifndef MP-TOUTIAO -->
+      <!-- 音频 -->
+      <audio v-else-if="n.name=='audio'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" @play="play" @error="mediaError" />
+      <!-- #endif -->
+      <view v-else-if="(n.name=='table'&&n.c)||n.name=='li'" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.attrs.style">
+        <node v-if="n.name=='li'" :childs="n.children" :opts="opts" />
+        <view v-else v-for="(tbody, x) in n.children" v-bind:key="x" :class="'_'+tbody.name+' '+tbody.attrs.class" :style="tbody.attrs.style">
+          <node v-if="tbody.name=='td'||tbody.name=='th'" :childs="tbody.children" :opts="opts" />
+          <block v-else v-for="(tr, y) in tbody.children" v-bind:key="y">
+            <view v-if="tr.name=='td'||tr.name=='th'" :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <node :childs="tr.children" :opts="opts" />
+            </view>
+            <view v-else :class="'_'+tr.name+' '+tr.attrs.class" :style="tr.attrs.style">
+              <view v-for="(td, z) in tr.children" v-bind:key="z" :class="'_'+td.name+' '+td.attrs.class" :style="td.attrs.style">
+                <node :childs="td.children" :opts="opts" />
+              </view>
+            </view>
+          </block>
+        </view>
+      </view>
+      
+      <!-- 富文本 -->
+      <!-- #ifdef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
+      <rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :style="n.f" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- #ifndef H5 || MP-WEIXIN || MP-QQ || APP-PLUS || MP-360 -->
+      <rich-text v-else-if="!n.c" :id="n.attrs.id" :style="n.f+';display:inline'" :preview="false" :nodes="[n]" />
+      <!-- #endif -->
+      <!-- 继续递归 -->
+      <view v-else-if="n.c==2" :id="n.attrs.id" :class="'_'+n.name+' '+n.attrs.class" :style="n.f+';'+n.attrs.style">
+        <node v-for="(n2, j) in n.children" v-bind:key="j" :style="n2.f" :name="n2.name" :attrs="n2.attrs" :childs="n2.children" :opts="opts" />
+      </view>
+      <node v-else :style="n.f" :name="n.name" :attrs="n.attrs" :childs="n.children" :opts="opts" />
+    </block>
+  </view>
+</template>
+<script module="handler" lang="wxs">
+// 行内标签列表
+var inlineTags = {
+  abbr: true,
+  b: true,
+  big: true,
+  code: true,
+  del: true,
+  em: true,
+  i: true,
+  ins: true,
+  label: true,
+  q: true,
+  small: true,
+  span: true,
+  strong: true,
+  sub: true,
+  sup: true
+}
+/**
+ * @description 是否使用 rich-text 显示剩余内容
+ */
+module.exports = {
+  use: function (item) {
+  // 微信和 QQ 的 rich-text inline 布局无效
+  if (inlineTags[item.name] || (item.attrs.style || '').indexOf('display:inline') != -1)
+    return false
+  return !item.c
+  }
+}
+</script>
+<script>
+
+import node from './node'
+export default {
+  name: 'node',
+  // #ifdef MP-WEIXIN
+  options: {
+    virtualHost: true
+  },
+  // #endif
+  data() {
+    return {
+      ctrl: {}
+    }
+  },
+  props: {
+    name: String,
+    attrs: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    childs: Array,
+    opts: Array
+  },
+  components: {
+
+    node
+  },
+  mounted() {
+    for (this.root = this.$parent; this.root.$options.name != 'mp-html'; this.root = this.root.$parent);
+    // #ifdef H5 || APP-PLUS
+    if (this.opts[0]) {
+      for (var i = this.childs.length; i--;)
+        if (this.childs[i].name == 'img')
+          break
+      if (i != -1) {
+        this.observer = uni.createIntersectionObserver(this).relativeToViewport({
+          top: 500,
+          bottom: 500
+        })
+        this.observer.observe('._img', res => {
+          if (res.intersectionRatio) {
+            this.$set(this.ctrl, 'load', 1)
+            this.observer.disconnect()
+          }
+        })
+      }
+    }
+    // #endif
+  },
+  beforeDestroy() {
+    // #ifdef H5 || APP-PLUS
+    if (this.observer)
+      this.observer.disconnect()
+    // #endif
+  },
+  methods:{
+    // #ifdef MP-WEIXIN
+    toJSON() { },
+    // #endif
+    /**
+     * @description 播放视频事件
+     * @param {Event} e 
+     */
+    play(e) {
+      // #ifndef APP-PLUS
+      if (this.root.pauseVideo) {
+        var flag = false, id = e.target.id
+        for (var i = this.root._videos.length; i--;) {
+          if (this.root._videos[i].id == id)
+            flag = true
+          else
+            this.root._videos[i].pause() // 自动暂停其他视频
+        }
+        // 将自己加入列表
+        if (!flag) {
+          var ctx = uni.createVideoContext(id
+            // #ifndef MP-BAIDU
+            , this
+            // #endif
+          )
+          ctx.id = id
+          this.root._videos.push(ctx)
+        }
+      }
+      // #endif
+    },
+
+    /**
+     * @description 图片点击事件
+     * @param {Event} e 
+     */
+    imgTap(e) {
+      var node = this.childs[e.currentTarget.dataset.i]
+      if (node.a)
+        return this.linkTap(node.a)
+      if (node.attrs.ignore)
+        return
+      // #ifdef H5 || APP-PLUS
+      node.attrs.src = node.attrs.src || node.attrs['data-src']
+      // #endif
+      this.root.$emit('imgtap', node.attrs)
+      // 自动预览图片
+      if (this.root.previewImg)
+        uni.previewImage({
+          current: parseInt(node.attrs.i),
+          urls: this.root.imgList
+        })
+    },
+
+    /**
+     * @description 图片长按
+     */
+    imgLongTap(e) {
+      // #ifdef APP-PLUS
+      var attrs = this.childs[e.currentTarget.dataset.i].attrs
+      if (!attrs.ignore)
+        uni.showActionSheet({
+          itemList: ['保存图片'],
+          success: () => {
+            uni.downloadFile({
+              url: this.root.imgList[attrs.i],
+              success: res => {
+                uni.saveImageToPhotosAlbum({
+                  filePath: res.tempFilePath,
+                  success() {
+                    uni.showToast({
+                      title: '保存成功'
+                    })
+                  }
+                })
+              }
+            })
+          }
+        })
+      // #endif
+    },
+
+    /**
+     * @description 图片加载完成事件
+     * @param {Event} e 
+     */
+    imgLoad(e) {
+      var i = e.currentTarget.dataset.i
+      // #ifndef H5 || APP-PLUS
+      // 设置原宽度
+      if (!this.childs[i].w)
+        this.$set(this.ctrl, i, e.detail.width)
+      else
+        // #endif
+        // 加载完毕,取消加载中占位图
+        if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] == -1)
+          this.$set(this.ctrl, i, 1)
+    },
+
+    /**
+     * @description 链接点击事件
+     * @param {Event} e 
+     */
+    linkTap(e) {
+      var attrs = e.currentTarget ? this.childs[e.currentTarget.dataset.i].attrs : e,
+        href = attrs.href
+      this.root.$emit('linktap', attrs)
+      if (href) {
+        // 跳转锚点
+        if (href[0] == '#')
+          this.root.navigateTo(href.substring(1)).catch(() => { })
+        // 复制外部链接
+        else if (href.includes('://')) {
+          if (this.root.copyLink) {
+            // #ifdef H5
+            window.open(href)
+            // #endif
+            // #ifdef MP
+            uni.setClipboardData({
+              data: href,
+              success: () =>
+                uni.showToast({
+                  title: '链接已复制'
+                })
+            })
+            // #endif
+            // #ifdef APP-PLUS
+            plus.runtime.openWeb(href)
+            // #endif
+          }
+        }
+        // 跳转页面
+        else
+          uni.navigateTo({
+            url: href,
+            fail() {
+              uni.switchTab({
+                url: href,
+                fail() { }
+              })
+            }
+          })
+      }
+    },
+
+    /**
+     * @description 错误事件
+     * @param {Event} e 
+     */
+    mediaError(e) {
+      var i = e.currentTarget.dataset.i,
+        node = this.childs[i]
+      // 加载其他源
+      if (node.name == 'video' || node.name == 'audio') {
+        var index = (this.ctrl[i] || 0) + 1
+        if (index > node.src.length)
+          index = 0
+        if (index < node.src.length)
+          return this.$set(this.ctrl, i, index)
+      }
+      // 显示错误占位图
+      else if (node.name == 'img' && this.opts[2])
+        this.$set(this.ctrl, i, -1)
+      if (this.root)
+        this.root.$emit('error', {
+          source: node.name,
+          attrs: node.attrs,
+          errMsg: e.detail.errMsg
+        })
+    }
+  }
+}
+</script>
+<style>
+/* a 标签默认效果 */
+._a {
+  padding: 1.5px 0 1.5px 0;
+  color: #366092;
+  word-break: break-all;
+}
+
+/* a 标签点击态效果 */
+._hover {
+  text-decoration: underline;
+  opacity: 0.7;
+}
+
+/* 图片默认效果 */
+._img {
+  max-width: 100%;
+  -webkit-touch-callout: none;
+}
+
+/* 内部样式 */
+
+._b,
+._strong {
+  font-weight: bold;
+}
+
+._code {
+  font-family: monospace;
+}
+
+._del {
+  text-decoration: line-through;
+}
+
+._em,
+._i {
+  font-style: italic;
+}
+
+._h1 {
+  font-size: 2em;
+}
+
+._h2 {
+  font-size: 1.5em;
+}
+
+._h3 {
+  font-size: 1.17em;
+}
+
+._h5 {
+  font-size: 0.83em;
+}
+
+._h6 {
+  font-size: 0.67em;
+}
+
+._h1,
+._h2,
+._h3,
+._h4,
+._h5,
+._h6 {
+  display: block;
+  font-weight: bold;
+}
+
+._image {
+  height: 1px;
+}
+
+._ins {
+  text-decoration: underline;
+}
+
+._li {
+  display: list-item;
+}
+
+._ol {
+  list-style-type: decimal;
+}
+
+._ol,
+._ul {
+  display: block;
+  padding-left: 40px;
+  margin: 1em 0;
+}
+
+._q::before {
+  content: '"';
+}
+
+._q::after {
+  content: '"';
+}
+
+._sub {
+  font-size: smaller;
+  vertical-align: sub;
+}
+
+._sup {
+  font-size: smaller;
+  vertical-align: super;
+}
+
+._thead,
+._tbody,
+._tfoot {
+  display: table-row-group;
+}
+
+._tr {
+  display: table-row;
+}
+
+._td,
+._th {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+._th {
+  font-weight: bold;
+  text-align: center;
+}
+
+._ul {
+  list-style-type: disc;
+}
+
+._ul ._ul {
+  margin: 0;
+  list-style-type: circle;
+}
+
+._ul ._ul ._ul {
+  list-style-type: square;
+}
+
+._abbr,
+._b,
+._code,
+._del,
+._em,
+._i,
+._ins,
+._label,
+._q,
+._span,
+._strong,
+._sub,
+._sup {
+  display: inline;
+}
+
+/* #ifdef APP-PLUS */
+._video {
+  width: 300px;
+  height: 225px;
+}
+/* #endif */
+</style>

+ 1112 - 0
lib/mp-html/parser.js

@@ -0,0 +1,1112 @@
+"use strict";
+
+/**
+ * @fileoverview html 解析器
+ */
+// 配置
+var config = {
+  // 信任的标签(保持标签名不变)
+  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'),
+  // 块级标签(转为 div,其他的非信任标签转为 span)
+  blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
+  // 要移除的标签
+  ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),
+  // 自闭合的标签
+  voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
+  // html 实体
+  entities: {
+    lt: '<',
+    gt: '>',
+    quot: '"',
+    apos: "'",
+    ensp: "\u2002",
+    emsp: "\u2003",
+    nbsp: '\xA0',
+    semi: ';',
+    ndash: '–',
+    mdash: '—',
+    middot: '·',
+    lsquo: '‘',
+    rsquo: '’',
+    ldquo: '“',
+    rdquo: '”',
+    bull: '•',
+    hellip: '…'
+  },
+  // 默认的标签样式
+  tagStyle: {
+    // #ifndef APP-PLUS-NVUE
+    address: 'font-style:italic',
+    big: 'display:inline;font-size:1.2em',
+    caption: 'display:table-caption;text-align:center',
+    center: 'text-align:center',
+    cite: 'font-style:italic',
+    dd: 'margin-left:40px',
+    mark: 'background-color:yellow',
+    pre: 'font-family:monospace;white-space:pre',
+    s: 'text-decoration:line-through',
+    small: 'display:inline;font-size:0.8em',
+    u: 'text-decoration:underline' // #endif
+
+  }
+};
+var windowWidth = uni.getSystemInfoSync().windowWidth;
+var blankChar = makeMap(' ,\r,\n,\t,\f');
+var idIndex = 0; // #ifdef H5 || APP-PLUS
+
+config.ignoreTags.iframe = void 0;
+config.trustTags.iframe = true;
+config.ignoreTags.embed = void 0;
+config.trustTags.embed = true; // #endif
+// #ifdef APP-PLUS-NVUE
+
+config.ignoreTags.source = void 0;
+config.ignoreTags.style = void 0; // #endif
+
+/**
+ * @description 创建 map
+ * @param {String} str 逗号分隔
+ */
+
+function makeMap(str) {
+  var map = Object.create(null),
+      list = str.split(',');
+
+  for (var i = list.length; i--;) {
+    map[list[i]] = true;
+  }
+
+  return map;
+}
+/**
+ * @description 解码 html 实体
+ * @param {String} str 要解码的字符串
+ * @param {Boolean} amp 要不要解码 &amp;
+ * @returns {String} 解码后的字符串
+ */
+
+
+function decodeEntity(str, amp) {
+  var i = str.indexOf('&');
+
+  while (i != -1) {
+    var j = str.indexOf(';', i + 3),
+        code = void 0;
+    if (j == -1) break;
+
+    if (str[i + 1] == '#') {
+      // &#123; 形式的实体
+      code = parseInt((str[i + 2] == 'x' ? '0' : '') + str.substring(i + 2, j));
+      if (!isNaN(code)) str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1);
+    } else {
+      // &nbsp; 形式的实体
+      code = str.substring(i + 1, j);
+      if (config.entities[code] || code == 'amp' && amp) str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1);
+    }
+
+    i = str.indexOf('&', i + 1);
+  }
+
+  return str;
+}
+/**
+ * @description html 解析器
+ * @param {Object} vm 组件实例
+ */
+
+
+function parser(vm) {
+  this.options = vm || {};
+  this.tagStyle = Object.assign(config.tagStyle, this.options.tagStyle);
+  this.imgList = vm.imgList || [];
+  this.plugins = vm.plugins || [];
+  this.attrs = Object.create(null);
+  this.stack = [];
+  this.nodes = [];
+}
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+
+
+parser.prototype.parse = function (content) {
+  // 插件处理
+  for (var i = this.plugins.length; i--;) {
+    if (this.plugins[i].onUpdate) content = this.plugins[i].onUpdate(content, config) || content;
+  }
+
+  new lexer(this).parse(content); // 出栈未闭合的标签
+
+  while (this.stack.length) {
+    this.popNode();
+  }
+
+  return this.nodes;
+};
+/**
+ * @description 将标签暴露出来(不被 rich-text 包含)
+ */
+
+
+parser.prototype.expose = function () {
+  // #ifndef APP-PLUS-NVUE
+  for (var i = this.stack.length; i--;) {
+    var item = this.stack[i];
+    if (item.name == 'a' || item.c) return;
+    item.c = 1;
+  } // #endif
+
+};
+/**
+ * @description 处理插件
+ * @param {Object} node 要处理的标签
+ * @returns {Boolean} 是否要移除此标签
+ */
+
+
+parser.prototype.hook = function (node) {
+  for (var i = this.plugins.length; i--;) {
+    if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) == false) return false;
+  }
+
+  return true;
+};
+/**
+ * @description 将链接拼接上主域名
+ * @param {String} url 需要拼接的链接
+ * @returns {String} 拼接后的链接
+ */
+
+
+parser.prototype.getUrl = function (url) {
+  var domain = this.options.domain;
+
+  if (url[0] == '/') {
+    // // 开头的补充协议名
+    if (url[1] == '/') url = (domain ? domain.split('://')[0] : 'http') + ':' + url; // 否则补充整个域名
+    else if (domain) url = domain + url;
+  } else if (domain && !url.includes('data:') && !url.includes('://')) url = domain + '/' + url;
+
+  return url;
+};
+/**
+ * @description 解析样式表
+ * @param {Object} node 标签
+ * @returns {Object} 
+ */
+
+
+parser.prototype.parseStyle = function (node) {
+  var attrs = node.attrs,
+      list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';')),
+      styleObj = {},
+      tmp = '';
+
+  if (attrs.id) {
+    // 暴露锚点
+    if (this.options.useAnchor) this.expose();else if (node.name != 'img' && node.name != 'a' && node.name != 'video' && node.name != 'audio') attrs.id = void 0;
+  } // 转换 width 和 height 属性
+
+
+  if (attrs.width) {
+    styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
+    attrs.width = void 0;
+  }
+
+  if (attrs.height) {
+    styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
+    attrs.height = void 0;
+  }
+
+  for (var i = 0, len = list.length; i < len; i++) {
+    var info = list[i].split(':');
+    if (info.length < 2) continue;
+    var key = info.shift().trim().toLowerCase(),
+        value = info.join(':').trim(); // 兼容性的 css 不压缩
+
+    if (value[0] == '-' && value.lastIndexOf('-') > 0 || value.includes('safe')) tmp += ";".concat(key, ":").concat(value); // 重复的样式进行覆盖
+    else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {
+        // 填充链接
+        if (value.includes('url')) {
+          var j = value.indexOf('(') + 1;
+
+          if (j) {
+            while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) {
+              j++;
+            }
+
+            value = value.substr(0, j) + this.getUrl(value.substr(j));
+          }
+        } // 转换 rpx(rich-text 内部不支持 rpx)
+        else if (value.includes('rpx')) value = value.replace(/[0-9.]+\s*rpx/g, function ($) {
+            return parseFloat($) * windowWidth / 750 + 'px';
+          });
+
+        styleObj[key] = value;
+      }
+  }
+
+  node.attrs.style = tmp;
+  return styleObj;
+};
+/**
+ * @description 解析到标签名
+ * @param {String} name 标签名
+ * @private
+ */
+
+
+parser.prototype.onTagName = function (name) {
+  this.tagName = this.xml ? name : name.toLowerCase();
+  if (this.tagName == 'svg') this.xml = true; // svg 标签内大小写敏感
+};
+/**
+ * @description 解析到属性名
+ * @param {String} name 属性名
+ * @private
+ */
+
+
+parser.prototype.onAttrName = function (name) {
+  name = this.xml ? name : name.toLowerCase();
+
+  if (name.substr(0, 5) == 'data-') {
+    // data-src 自动转为 src
+    if (name == 'data-src' && !this.attrs.src) this.attrName = 'src'; // a 和 img 标签保留 data- 的属性,可以在 imgtap 和 linktap 事件中使用
+    else if (this.tagName == 'img' || this.tagName == 'a') this.attrName = name; // 剩余的移除以减小大小
+      else this.attrName = void 0;
+  } else {
+    this.attrName = name;
+    this.attrs[name] = 'T'; // boolean 型属性缺省设置
+  }
+};
+/**
+ * @description 解析到属性值
+ * @param {String} val 属性值
+ * @private
+ */
+
+
+parser.prototype.onAttrVal = function (val) {
+  var name = this.attrName || ''; // 部分属性进行实体解码
+
+  if (name == 'style' || name == 'href') this.attrs[name] = decodeEntity(val, true); // 拼接主域名
+  else if (name.includes('src')) this.attrs[name] = this.getUrl(decodeEntity(val, true));else if (name) this.attrs[name] = val;
+};
+/**
+ * @description 解析到标签开始
+ * @param {Boolean} selfClose 是否有自闭合标识 />
+ * @private
+ */
+
+
+parser.prototype.onOpenTag = function (selfClose) {
+  // 拼装 node
+  var node = Object.create(null);
+  node.name = this.tagName;
+  node.attrs = this.attrs;
+  this.attrs = Object.create(null);
+  var attrs = node.attrs,
+      parent = this.stack[this.stack.length - 1],
+      siblings = parent ? parent.children : this.nodes,
+      close = this.xml ? selfClose : config.voidTags[node.name]; // 转换 embed 标签
+
+  if (node.name == 'embed') {
+    // #ifndef H5 || APP-PLUS
+    var src = attrs.src || ''; // 按照后缀名和 type 将 embed 转为 video 或 audio
+
+    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';
+    if (attrs.autostart) attrs.autoplay = 'T';
+    attrs.controls = 'T'; // #endif
+    // #ifdef H5 || APP-PLUS
+
+    this.expose(); // #endif
+  } // #ifndef APP-PLUS-NVUE
+  // 处理音视频
+
+
+  if (node.name == 'video' || node.name == 'audio') {
+    // 设置 id 以便获取 context
+    if (node.name == 'video' && !attrs.id) attrs.id = 'v' + idIndex++; // 没有设置 controls 也没有设置 autoplay 的自动设置 controls
+
+    if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T'; // 用数组存储所有可用的 source
+
+    node.src = [];
+
+    if (attrs.src) {
+      node.src.push(attrs.src);
+      attrs.src = void 0;
+    }
+
+    this.expose();
+  } // #endif
+  // 处理自闭合标签
+
+
+  if (close) {
+    if (!this.hook(node) || config.ignoreTags[node.name]) {
+      // 通过 base 标签设置主域名
+      if (node.name == 'base' && !this.options.domain) this.options.domain = attrs.href; // #ifndef APP-PLUS-NVUE
+      // 设置 source 标签(仅父节点为 video 或 audio 时有效)
+      else if (node.name == 'source' && parent && (parent.name == 'video' || parent.name == 'audio') && attrs.src) parent.src.push(attrs.src); // #endif
+
+      return;
+    } // 解析 style
+
+
+    var styleObj = this.parseStyle(node); // 处理图片
+
+    if (node.name == 'img') {
+      if (attrs.src) {
+        // 标记 webp
+        if (attrs.src.includes('webp')) node.webp = 'T'; // data url 图片如果没有设置 original-src 默认为不可预览的小图片
+
+        if (attrs.src.includes('data:') && !attrs['original-src']) attrs.ignore = 'T';
+
+        if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {
+          for (var i = this.stack.length; i--;) {
+            var item = this.stack[i];
+
+            if (item.name == 'a') {
+              node.a = item.attrs;
+              break;
+            } // #ifndef H5 || APP-PLUS
+
+
+            var style = item.attrs.style || '';
+
+            if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || !styleObj.width.includes('%'))) {
+              styleObj.width = '100% !important';
+              styleObj.height = '';
+
+              for (var j = i + 1; j < this.stack.length; j++) {
+                this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '');
+              }
+            } else if (style.includes('flex') && styleObj.width == '100%') {
+              for (var _j = i + 1; _j < this.stack.length; _j++) {
+                var _style = this.stack[_j].attrs.style || '';
+
+                if (!_style.includes(';width') && !_style.includes(' width') && _style.indexOf('width') != 0) {
+                  styleObj.width = '';
+                  break;
+                }
+              }
+            } else if (style.includes('inline-block')) {
+              if (styleObj.width && styleObj.width[styleObj.width.length - 1] == '%') {
+                item.attrs.style += ';max-width:' + styleObj.width;
+                styleObj.width = '';
+              } else item.attrs.style += ';max-width:100%';
+            } // #endif
+
+
+            item.c = 1;
+          }
+
+          attrs.i = this.imgList.length.toString();
+
+          var _src = attrs['original-src'] || attrs.src; // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360
+
+
+          if (this.imgList.includes(_src)) {
+            // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位
+            var _i = _src.indexOf('://');
+
+            if (_i != -1) {
+              _i += 3;
+
+              var newSrc = _src.substr(0, _i);
+
+              for (; _i < _src.length; _i++) {
+                if (_src[_i] == '/') break;
+                newSrc += Math.random() > 0.5 ? _src[_i].toUpperCase() : _src[_i];
+              }
+
+              newSrc += _src.substr(_i);
+              _src = newSrc;
+            }
+          } // #endif
+
+
+          this.imgList.push(_src); // #ifdef H5 || APP-PLUS
+
+          if (this.options.lazyLoad) {
+            attrs['data-src'] = attrs.src;
+            attrs.src = void 0;
+          } // #endif
+
+        }
+      }
+
+      if (styleObj.display == 'inline') styleObj.display = ''; // #ifndef APP-PLUS-NVUE
+
+      if (attrs.ignore) {
+        styleObj['max-width'] = styleObj['max-width'] || '100%';
+        attrs.style += ';-webkit-touch-callout:none';
+      } // #endif
+      // 设置的宽度超出屏幕,为避免变形,高度转为自动
+
+
+      if (parseInt(styleObj.width) > windowWidth) styleObj.height = void 0; // 记录是否设置了宽高
+
+      if (styleObj.width) {
+        if (styleObj.width.includes('auto')) styleObj.width = '';else {
+          node.w = 'T';
+          if (styleObj.height && !styleObj.height.includes('auto')) node.h = 'T';
+        }
+      }
+    } else if (node.name == 'svg') {
+      siblings.push(node);
+      this.stack.push(node);
+      this.popNode();
+      return;
+    }
+
+    for (var key in styleObj) {
+      if (styleObj[key]) attrs.style += ";".concat(key, ":").concat(styleObj[key].replace(' !important', ''));
+    }
+
+    attrs.style = attrs.style.substr(1) || void 0;
+  } else {
+    if (node.name == 'pre' || (attrs.style || '').includes('white-space') && attrs.style.includes('pre')) this.pre = node.pre = true;
+    node.children = [];
+    this.stack.push(node);
+  } // 加入节点树
+
+
+  siblings.push(node);
+};
+/**
+ * @description 解析到标签结束
+ * @param {String} name 标签名
+ * @private
+ */
+
+
+parser.prototype.onCloseTag = function (name) {
+  // 依次出栈到匹配为止
+  name = this.xml ? name : name.toLowerCase();
+  var i;
+
+  for (i = this.stack.length; i--;) {
+    if (this.stack[i].name == name) break;
+  }
+
+  if (i != -1) {
+    while (this.stack.length > i) {
+      this.popNode();
+    }
+  } else if (name == 'p' || name == 'br') {
+    var siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes;
+    siblings.push({
+      name: name,
+      attrs: {}
+    });
+  }
+};
+/**
+ * @description 处理标签出栈
+ * @private
+ */
+
+
+parser.prototype.popNode = function () {
+  var node = this.stack.pop(),
+      attrs = node.attrs,
+      children = node.children,
+      parent = this.stack[this.stack.length - 1],
+      siblings = parent ? parent.children : this.nodes;
+
+  if (!this.hook(node) || config.ignoreTags[node.name]) {
+    // 获取标题
+    if (node.name == 'title' && children.length && children[0].type == 'text' && this.options.setTitle) uni.setNavigationBarTitle({
+      title: children[0].text
+    });
+    siblings.pop();
+    return;
+  }
+
+  if (node.pre) {
+    // 是否合并空白符标识
+    node.pre = this.pre = void 0;
+
+    for (var i = this.stack.length; i--;) {
+      if (this.stack[i].pre) this.pre = true;
+    }
+  }
+
+  var styleObj = {}; // 转换 svg
+
+  if (node.name == 'svg') {
+    // #ifndef APP-PLUS-NVUE
+    var src = '',
+        style = attrs.style;
+    attrs.style = '';
+    attrs.xmlns = 'http://www.w3.org/2000/svg';
+
+    (function traversal(node) {
+      src += '<' + node.name;
+
+      for (var item in node.attrs) {
+        var val = node.attrs[item];
+
+        if (val) {
+          if (item == 'viewbox') item = 'viewBox';
+          src += " ".concat(item, "=\"").concat(val, "\"");
+        }
+      }
+
+      if (!node.children) src += '/>';else {
+        src += '>';
+
+        for (var _i2 = 0; _i2 < node.children.length; _i2++) {
+          traversal(node.children[_i2]);
+        }
+
+        src += '</' + node.name + '>';
+      }
+    })(node);
+
+    node.name = 'img';
+    node.attrs = {
+      src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
+      style: style,
+      ignore: 'T'
+    };
+    node.children = void 0; // #endif
+
+    this.xml = false;
+    return;
+  } // #ifndef APP-PLUS-NVUE
+  // 转换 align 属性
+
+
+  if (attrs.align) {
+    if (node.name == 'table') {
+      if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';else styleObj["float"] = attrs.align;
+    } else styleObj['text-align'] = attrs.align;
+
+    attrs.align = void 0;
+  } // 转换 font 标签的属性
+
+
+  if (node.name == 'font') {
+    if (attrs.color) {
+      styleObj.color = attrs.color;
+      attrs.color = void 0;
+    }
+
+    if (attrs.face) {
+      styleObj['font-family'] = attrs.face;
+      attrs.face = void 0;
+    }
+
+    if (attrs.size) {
+      var size = parseInt(attrs.size);
+
+      if (!isNaN(size)) {
+        if (size < 1) size = 1;else if (size > 7) size = 7;
+        styleObj['font-size'] = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'][size - 1];
+      }
+
+      attrs.size = void 0;
+    }
+  } // #endif
+  // 一些编辑器的自带 class
+
+
+  if ((attrs["class"] || '').includes('align-center')) styleObj['text-align'] = 'center';
+  Object.assign(styleObj, this.parseStyle(node));
+
+  if (parseInt(styleObj.width) > windowWidth) {
+    styleObj['max-width'] = '100%';
+    styleObj['box-sizing'] = 'border-box';
+  } // #ifndef APP-PLUS-NVUE
+
+
+  if (config.blockTags[node.name]) node.name = 'div'; // 未知标签转为 span,避免无法显示
+  else if (!config.trustTags[node.name] && !this.xml) node.name = 'span';
+  if (node.name == 'a' || node.name == 'ad' // #ifdef H5 || APP-PLUS
+  || node.name == 'iframe' // #endif
+  ) this.expose(); // #ifdef APP-PLUS
+  else if (node.name == 'video') {
+      var str = '<video style="width:100%;height:100%"'; // 空白图占位
+
+      if (!attrs.poster && !attrs.autoplay) attrs.poster = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'/>";
+
+      for (var item in attrs) {
+        if (attrs[item]) str += ' ' + item + '="' + attrs[item] + '"';
+      }
+
+      if (this.options.pauseVideo) str += ' onplay="for(var e=document.getElementsByTagName(\'video\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()"';
+      str += '>';
+
+      for (var _i3 = 0; _i3 < node.src.length; _i3++) {
+        str += '<source src="' + node.src[_i3] + '">';
+      }
+
+      str += '</video>';
+      node.html = str;
+    } // #endif
+    // 列表处理
+    else if ((node.name == 'ul' || node.name == 'ol') && node.c) {
+        var types = {
+          a: 'lower-alpha',
+          A: 'upper-alpha',
+          i: 'lower-roman',
+          I: 'upper-roman'
+        };
+
+        if (types[attrs.type]) {
+          attrs.style += ';list-style-type:' + types[attrs.type];
+          attrs.type = void 0;
+        }
+
+        for (var _i4 = children.length; _i4--;) {
+          if (children[_i4].name == 'li') children[_i4].c = 1;
+        }
+      } // 表格处理
+      else if (node.name == 'table') {
+          // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现
+          var padding = parseFloat(attrs.cellpadding),
+              spacing = parseFloat(attrs.cellspacing),
+              border = parseFloat(attrs.border);
+
+          if (node.c) {
+            // padding 和 spacing 默认 2
+            if (isNaN(padding)) padding = 2;
+            if (isNaN(spacing)) spacing = 2;
+          }
+
+          if (border) attrs.style += ';border:' + border + 'px solid gray';
+
+          if (node.flag && node.c) {
+            // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现
+            styleObj.display = 'grid';
+
+            if (spacing) {
+              styleObj['grid-gap'] = spacing + 'px';
+              styleObj.padding = spacing + 'px';
+            } // 无间隔的情况下避免边框重叠
+            else if (border) attrs.style += ';border-left:0;border-top:0';
+
+            var width = [],
+                // 表格的列宽
+            trList = [],
+                // tr 列表
+            cells = [],
+                // 保存新的单元格
+            map = {}; // 被合并单元格占用的格子
+
+            (function traversal(nodes) {
+              for (var _i5 = 0; _i5 < nodes.length; _i5++) {
+                if (nodes[_i5].name == 'tr') trList.push(nodes[_i5]);else traversal(nodes[_i5].children || []);
+              }
+            })(children);
+
+            for (var row = 1; row <= trList.length; row++) {
+              var col = 1;
+
+              for (var j = 0; j < trList[row - 1].children.length; j++, col++) {
+                var td = trList[row - 1].children[j];
+
+                if (td.name == 'td' || td.name == 'th') {
+                  // 这个格子被上面的单元格占用,则列号++
+                  while (map[row + '.' + col]) {
+                    col++;
+                  }
+
+                  var _style2 = td.attrs.style || '',
+                      start = _style2.indexOf('width') ? _style2.indexOf(';width') : 0; // 提取出 td 的宽度
+
+
+                  if (start != -1) {
+                    var end = _style2.indexOf(';', start + 6);
+
+                    if (end == -1) end = _style2.length;
+                    if (!td.attrs.colspan) width[col] = _style2.substring(start ? start + 7 : 6, end);
+                    _style2 = _style2.substr(0, start) + _style2.substr(end);
+                  }
+
+                  _style2 += (border ? ";border:".concat(border, "px solid gray") + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? ";padding:".concat(padding, "px") : ''); // 处理列合并
+
+                  if (td.attrs.colspan) {
+                    _style2 += ";grid-column-start:".concat(col, ";grid-column-end:").concat(col + parseInt(td.attrs.colspan));
+                    if (!td.attrs.rowspan) _style2 += ";grid-row-start:".concat(row, ";grid-row-end:").concat(row + 1);
+                    col += parseInt(td.attrs.colspan) - 1;
+                  } // 处理行合并
+
+
+                  if (td.attrs.rowspan) {
+                    _style2 += ";grid-row-start:".concat(row, ";grid-row-end:").concat(row + parseInt(td.attrs.rowspan));
+                    if (!td.attrs.colspan) _style2 += ";grid-column-start:".concat(col, ";grid-column-end:").concat(col + 1); // 记录下方单元格被占用
+
+                    for (var k = 1; k < td.attrs.rowspan; k++) {
+                      map[row + k + '.' + col] = 1;
+                    }
+                  }
+
+                  if (_style2) td.attrs.style = _style2;
+                  cells.push(td);
+                }
+              }
+
+              if (row == 1) {
+                var temp = '';
+
+                for (var _i6 = 1; _i6 < col; _i6++) {
+                  temp += (width[_i6] ? width[_i6] : 'auto') + ' ';
+                }
+
+                styleObj['grid-template-columns'] = temp;
+              }
+            }
+
+            node.children = cells;
+          } else {
+            // 没有使用合并单元格的表格通过 table 布局实现
+            if (node.c) styleObj.display = 'table';
+            if (!isNaN(spacing)) styleObj['border-spacing'] = spacing + 'px';
+
+            if (border || padding) {
+              // 遍历
+              (function traversal(nodes) {
+                for (var _i7 = 0; _i7 < nodes.length; _i7++) {
+                  var _td = nodes[_i7];
+
+                  if (_td.name == 'th' || _td.name == 'td') {
+                    if (border) _td.attrs.style = "border:".concat(border, "px solid gray;").concat(_td.attrs.style || '');
+                    if (padding) _td.attrs.style = "padding:".concat(padding, "px;").concat(_td.attrs.style || '');
+                  } else if (_td.children) traversal(_td.children);
+                }
+              })(children);
+            }
+          } // 给表格添加一个单独的横向滚动层
+
+
+          if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {
+            var table = Object.assign({}, node);
+            node.name = 'div';
+            node.attrs = {
+              style: 'overflow:auto'
+            };
+            node.children = [table];
+            attrs = table.attrs;
+          }
+        } else if ((node.name == 'td' || node.name == 'th') && (attrs.colspan || attrs.rowspan)) {
+          for (var _i8 = this.stack.length; _i8--;) {
+            if (this.stack[_i8].name == 'table') {
+              this.stack[_i8].flag = 1; // 指示含有合并单元格
+
+              break;
+            }
+          }
+        } // 转换 ruby
+        else if (node.name == 'ruby') {
+            node.name = 'span';
+
+            for (var _i9 = 0; _i9 < children.length - 1; _i9++) {
+              if (children[_i9].type == 'text' && children[_i9 + 1].name == 'rt') {
+                children[_i9] = {
+                  name: 'div',
+                  attrs: {
+                    style: 'display:inline-block'
+                  },
+                  children: [{
+                    name: 'div',
+                    attrs: {
+                      style: 'font-size:50%;text-align:start'
+                    },
+                    children: children[_i9 + 1].children
+                  }, children[_i9]]
+                };
+                children.splice(_i9 + 1, 1);
+              }
+            }
+          } else if (node.c) {
+            node.c = 2;
+
+            for (var _i10 = node.children.length; _i10--;) {
+              if (!node.children[_i10].c || node.children[_i10].name == 'table') node.c = 1;
+            }
+          }
+  if ((styleObj.display || '').includes('flex') && !node.c) for (var _i11 = children.length; _i11--;) {
+    var _item = children[_i11];
+
+    if (_item.f) {
+      _item.attrs.style = (_item.attrs.style || '') + _item.f;
+      _item.f = void 0;
+    }
+  } // flex 布局时部分样式需要提取到 rich-text 外层
+
+  var flex = parent && (parent.attrs.style || '').includes('flex') // #ifdef MP-WEIXIN
+  // 检查基础库版本 virtualHost 是否可用
+  && !(node.c && wx.getNFCAdapter) // #endif
+  // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
+  && !node.c; // #endif
+
+  if (flex) node.f = ';max-width:100%'; // #endif
+
+  for (var key in styleObj) {
+    if (styleObj[key]) {
+      var val = ";".concat(key, ":").concat(styleObj[key].replace(' !important', '')); // #ifndef APP-PLUS-NVUE
+
+      if (flex && (key.includes('flex') && key != 'flex-direction' || key == 'align-self' || styleObj[key][0] == '-' || key == 'width' && val.includes('%'))) {
+        node.f += val;
+        if (key == 'width') attrs.style += ';width:100%';
+      } else // #endif
+        attrs.style += val;
+    }
+  }
+
+  attrs.style = attrs.style.substr(1) || void 0;
+};
+/**
+ * @description 解析到文本
+ * @param {String} text 文本内容
+ */
+
+
+parser.prototype.onText = function (text) {
+  if (!this.pre) {
+    // 合并空白符
+    var trim = '',
+        flag;
+
+    for (var i = 0, len = text.length; i < len; i++) {
+      if (!blankChar[text[i]]) trim += text[i];else {
+        if (trim[trim.length - 1] != ' ') trim += ' ';
+        if (text[i] == '\n' && !flag) flag = true;
+      }
+    } // 去除含有换行符的空串
+
+
+    if (trim == ' ' && flag) return;
+    text = trim;
+  }
+
+  var node = Object.create(null);
+  node.type = 'text';
+  node.text = decodeEntity(text);
+
+  if (this.hook(node)) {
+    var siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes;
+    siblings.push(node);
+  }
+};
+/**
+ * @description html 词法分析器
+ * @param {Object} handler 高层处理器
+ */
+
+
+function lexer(handler) {
+  this.handler = handler;
+}
+/**
+ * @description 执行解析
+ * @param {String} content 要解析的文本
+ */
+
+
+lexer.prototype.parse = function (content) {
+  this.content = content || '';
+  this.i = 0; // 标记解析位置
+
+  this.start = 0; // 标记一个单词的开始位置
+
+  this.state = this.text; // 当前状态
+
+  for (var len = this.content.length; this.i != -1 && this.i < len;) {
+    this.state();
+  }
+};
+/**
+ * @description 检查标签是否闭合
+ * @param {String} method 如果闭合要进行的操作
+ * @returns {Boolean} 是否闭合
+ * @private
+ */
+
+
+lexer.prototype.checkClose = function (method) {
+  var selfClose = this.content[this.i] == '/';
+
+  if (this.content[this.i] == '>' || selfClose && this.content[this.i + 1] == '>') {
+    if (method) this.handler[method](this.content.substring(this.start, this.i));
+    this.i += selfClose ? 2 : 1;
+    this.start = this.i;
+    this.handler.onOpenTag(selfClose);
+
+    if (this.handler.tagName == 'script') {
+      this.i = this.content.indexOf('</', this.i);
+
+      if (this.i != -1) {
+        this.i += 2;
+        this.start = this.i;
+      }
+
+      this.state = this.endTag;
+    } else this.state = this.text;
+
+    return true;
+  }
+
+  return false;
+};
+/**
+ * @description 文本状态
+ * @private
+ */
+
+
+lexer.prototype.text = function () {
+  this.i = this.content.indexOf('<', this.i); // 查找最近的标签
+
+  if (this.i == -1) {
+    // 没有标签了
+    if (this.start < this.content.length) this.handler.onText(this.content.substring(this.start, this.content.length));
+    return;
+  }
+
+  var c = this.content[this.i + 1];
+
+  if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
+    // 标签开头
+    if (this.start != this.i) this.handler.onText(this.content.substring(this.start, this.i));
+    this.start = ++this.i;
+    this.state = this.tagName;
+  } else if (c == '/' || c == '!' || c == '?') {
+    if (this.start != this.i) this.handler.onText(this.content.substring(this.start, this.i));
+    var next = this.content[this.i + 2];
+
+    if (c == '/' && (next >= 'a' && next <= 'z' || next >= 'A' && next <= 'Z')) {
+      // 标签结尾
+      this.i += 2;
+      this.start = this.i;
+      return this.state = this.endTag;
+    } // 处理注释
+
+
+    var end = '-->';
+    if (c != '!' || this.content[this.i + 2] != '-' || this.content[this.i + 3] != '-') end = '>';
+    this.i = this.content.indexOf(end, this.i);
+
+    if (this.i != -1) {
+      this.i += end.length;
+      this.start = this.i;
+    }
+  } else this.i++;
+};
+/**
+ * @description 标签名状态
+ * @private
+ */
+
+
+lexer.prototype.tagName = function () {
+  if (blankChar[this.content[this.i]]) {
+    // 解析到标签名
+    this.handler.onTagName(this.content.substring(this.start, this.i));
+
+    while (blankChar[this.content[++this.i]]) {
+      ;
+    }
+
+    if (this.i < this.content.length && !this.checkClose()) {
+      this.start = this.i;
+      this.state = this.attrName;
+    }
+  } else if (!this.checkClose('onTagName')) this.i++;
+};
+/**
+ * @description 属性名状态
+ * @private
+ */
+
+
+lexer.prototype.attrName = function () {
+  var c = this.content[this.i];
+
+  if (blankChar[c] || c == '=') {
+    // 解析到属性名
+    this.handler.onAttrName(this.content.substring(this.start, this.i));
+    var needVal = c == '=',
+        len = this.content.length;
+
+    while (++this.i < len) {
+      c = this.content[this.i];
+
+      if (!blankChar[c]) {
+        if (this.checkClose()) return;
+
+        if (needVal) {
+          // 等号后遇到第一个非空字符
+          this.start = this.i;
+          return this.state = this.attrVal;
+        }
+
+        if (this.content[this.i] == '=') needVal = true;else {
+          this.start = this.i;
+          return this.state = this.attrName;
+        }
+      }
+    }
+  } else if (!this.checkClose('onAttrName')) this.i++;
+};
+/**
+ * @description 属性值状态
+ * @private
+ */
+
+
+lexer.prototype.attrVal = function () {
+  var c = this.content[this.i],
+      len = this.content.length; // 有冒号的属性
+
+  if (c == '"' || c == "'") {
+    this.start = ++this.i;
+    this.i = this.content.indexOf(c, this.i);
+    if (this.i == -1) return;
+    this.handler.onAttrVal(this.content.substring(this.start, this.i));
+  } // 没有冒号的属性
+  else for (; this.i < len; this.i++) {
+      if (blankChar[this.content[this.i]]) {
+        this.handler.onAttrVal(this.content.substring(this.start, this.i));
+        break;
+      } else if (this.checkClose('onAttrVal')) return;
+    }
+
+  while (blankChar[this.content[++this.i]]) {
+    ;
+  }
+
+  if (this.i < len && !this.checkClose()) {
+    this.start = this.i;
+    this.state = this.attrName;
+  }
+};
+/**
+ * @description 结束标签状态
+ * @returns {String} 结束的标签名
+ * @private
+ */
+
+
+lexer.prototype.endTag = function () {
+  var c = this.content[this.i];
+
+  if (blankChar[c] || c == '>' || c == '/') {
+    this.handler.onCloseTag(this.content.substring(this.start, this.i));
+
+    if (c != '>') {
+      this.i = this.content.indexOf('>', this.i);
+      if (this.i == -1) return;
+    }
+
+    this.start = ++this.i;
+    this.state = this.text;
+  } else this.i++;
+};
+
+module.exports = parser;

+ 27 - 3
pages/dream/buy.vue

@@ -2,8 +2,8 @@
 	<view>
 		<view class="buy-layer" :class="show ?'slidein':''">
 		  <form bindsubmit="doBuy">
-			<view class="tit ">购买{{fetch.info.ticket_name}}</view>
-			<view class="msg" @click="showTip">查看{{fetch.info.ticket_name}}说明</view>
+			<view class="tit ">《{{fetch.info.name}}》{{fetch.info.ticket_name}}</view>
+			<view class="msg"><text @click="showTip">查看{{fetch.info.ticket_name}}说明</text> <text class="spliter"></text> <text @click="showInfo">查看介绍</text></view>
 			<view class="share_msg" @click="showShare" v-if="fetch.ticket && fetch.ticket.total">共有<text style="color:#007AFF;font-size: 30rpx;padding-left:5rpx;padding-right: 5rpx;">{{fetch.ticket.total}}</text>{{fetch.info.unit}},当前剩余<text style="color:#CC1619;font-size: 30rpx;padding-left:5rpx;padding-right: 5rpx;">{{fetch.ticket.num}}</text>{{fetch.info.unit}}<text v-if="fetch.ticket.num > 0">,点此分享给好友</text></view>
 			<scroll-view scroll-y class="list">
 			  <view v-for="(v, k) in fetch.buy" :key="k" class="item" :class="index === k?'active':''" @click="buy(k)">
@@ -28,6 +28,15 @@
 			</view>
 		  </view>
 		</view>
+		<view class="mask" v-if="info">
+		  <view class="buytip-layer layer " catchtap="handleStop">
+			<view class="buy-tit">介绍说明</view>
+			<rich-text class="p" :nodes="fetch.info.desc"></rich-text>
+			<view class="btn-wrapper">
+			  <view class="button" @click="hideInfo" :style="bgcolor">我知道了</view>
+			</view>
+		  </view>
+		</view>
 		<view class="mask" v-if="share">
 		  <view class="buytip-layer layer " catchtap="handleStop">
 			<view class="buy-tit">分享给你的好友</view>
@@ -100,6 +109,7 @@ export default {
 			},
 			index : 0,
 			pay : false,
+			info : false,
 			tip : false,
 			show : false,
 			share : false,
@@ -143,6 +153,12 @@ export default {
 		},
 		hideTip : function() {
 			this.tip = false;
+		},
+		showInfo : function() {
+			this.info = true;
+		},
+		hideInfo : function() {
+			this.info = false;
 		},
 		showShare : function() {
 			this.share = true;
@@ -171,7 +187,15 @@ export default {
 }
 </script>
 
-<style>
+<style>
+.spliter{
+  flex-shrink: 0;
+  width: 1rpx;
+  background: #fff;
+  height: 40rpx;
+  color: #333;
+  margin-left: 20rpx;
+}
 .share_left {
 	position: relative;
 }

+ 5 - 1
pages/dream/index.vue

@@ -111,7 +111,11 @@ export default{
 	onLoad(option) {
 		this.id = option.id;
 		this.code = option.code;
-		this.getData();
+	},
+	onShow() {
+		if (this.id || this.code) {
+			this.getData();
+		}
 	},
 	// 重新加载
 	onPullDownRefresh: function() {

+ 6 - 5
pages/dream/ranking.vue

@@ -26,7 +26,7 @@
 							<view class="avatar-wrap">
 								<image :src="fetch.ranking_data[1].user.avatar"></image>
 							</view>
-							<view class="name">{{fetch.ranking_data[1].user.username}}
+							<view class="name"><rich-text type="text" :nodes="fetch.ranking_data[1].user.username_text"></rich-text>
 								<text>{{fetch.ranking_data[1].num}}{{fetch.ranking[index].unit}}</text>
 							</view>
 						</view>
@@ -34,7 +34,7 @@
 							<view class="avatar-wrap">
 								<image :src="fetch.ranking_data[0].user.avatar"></image>
 							</view>
-							<view class="name">{{fetch.ranking_data[0].user.username}}
+							<view class="name"><rich-text type="text" :nodes="fetch.ranking_data[0].user.username_text"></rich-text>
 								<text>{{fetch.ranking_data[0].num}}{{fetch.ranking[index].unit}}</text>
 							</view>
 						</view>
@@ -42,7 +42,7 @@
 							<view class="avatar-wrap">
 								<image :src="fetch.ranking_data[2].user.avatar"></image>
 							</view>
-							<view class="name">{{fetch.ranking_data[2].user.username}}
+							<view class="name"><rich-text type="text" :nodes="fetch.ranking_data[2].user.username_text"></rich-text>
 								<text>{{fetch.ranking_data[2].num}}{{fetch.ranking[index].unit}}</text>
 							</view>
 						</view>
@@ -51,9 +51,9 @@
 				<view class="scorelist" v-if="fetch.ranking_data.length > 2">
 					<view class="scoreitem" v-for="(v, k) in fetch.ranking_data" :key="k" v-if="k > 2">
 						<view class="avatar">
-							<text class="order-num">{{k+1}}</text>
+							<view class="order-num">{{k+1}}</view>
 							<image :src="v.user.avatar"></image>
-							<text class="name">{{v.user.username}}</text>
+							<view class="name"><rich-text type="text" :nodes="v.user.username_text"></rich-text></view>
 						</view>
 						<view class="score">{{v.num}}{{fetch.ranking[index].unit}}</view>
 					</view> 
@@ -99,6 +99,7 @@ export default {
 		getRankingData : function(ranking_id, ranking_periods) {
 			var self = this;
 			this.fetch.ranking_data = [];
+			this.ranking_periods = [];
 			this.Dever.get(this, 'app/collection/?l=api.getRankingData', {id:this.id,ranking_id:ranking_id,ranking_periods:ranking_periods}, function(t) {
 				self.show = true;
 				if (t && t.ranking_periods) {

+ 4 - 4
pages/dream/view/product.vue

@@ -132,7 +132,7 @@
 								<graceNumberBox :minNum="1" :maxNum="kuNum" :value="buyNum" v-on:change="buyNumChange"></graceNumberBox>
 							</view>
 						</block>
-						<block else>
+						<block v-if="kuNum <= 0">
 							<view class="grace-margin-top"><text class="grace-title">已售空</text></view>
 						</block>
 					</scroll-view>
@@ -346,7 +346,8 @@ export default {
 			this.store_index = e.detail.value;
 		},
 		//打开属性视图
-		buyNow : function() {
+		buyNow : function() {
+			this.getAttrKey();
 			if (this.item.goods.platform == 1) {
 				this.attrIsShow = true;
 			} else {
@@ -431,8 +432,7 @@ export default {
 			for (var i in this.attrSelect) {
 				key.push(this.attrSelect[i]);
 			}
-			this.attrKey = key.join('_');
-			
+			this.attrKey = key.join('_');
 			if (this.item.goods.price_type == 1) {
 				this.kuNum = parseInt(this.item.goods.num);
 				this.skuId = 0;