uniapp 富文本插件 感觉还不错留着

uniapp 富文本插件
引用插件
结构
在这里插入图片描述

文件原码
config.js

/* 配置文件 */
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;

CssHandler.js

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;
}

handler.wxs

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
	}
}

MpHtmlParser.js

/**
 * 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;

trees.vue

<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" style="max-width: 100%;"/>
			<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" style="max-width: 100%;"/>
			<!--音频-->
			<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%;
		margin: auto;
		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;
		width: 100%;
		margin: auto;
	}
	
	/* #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>

textParser.vue

<template>
	<view :style="'width: '+width+'rpx;margin: auto;overflow: hidden;'">
		<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,
			width: 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;
		overflow: hidden; 
	}

	/* #endif */

</style>

需要富文本的地方直接引用

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值