<!--
 parser 主模块组件
 github地址：https://github.com/jin-yufeng/Parser
 文档地址：https://jin-yufeng.github.io/Parser
 插件市场：https://ext.dcloud.net.cn/plugin?id=805
 author：JinYufeng
-->
<template>
	<view style="display: inherit;">
		<slot v-if="(!html||!html.length||(!html[0].name&&!html[0].type))&&!nodes.length"></slot>
		<view class="_contain" :style="(selectable?'user-select:text;-webkit-user-select:text;':'')+(showWithAnimation?'opacity:0;':'')"
		 :animation="showAnimation" @tap="tap" @touchstart="touchstart" @touchmove="touchmove">
			<!--#ifdef H5-->
			<div :id="'rtf'+uid"></div>
			<!--#endif-->
			<!--#ifndef H5-->
			<trees :nodes="nodes.length?nodes:(html&&html.length&&(html[0].name||html[0].type)?html:[])" :loadVideo="loadVideo" />
			<!--#endif-->
		</view>
	</view>
</template>

<script>
	// #ifndef H5
	import trees from "./libs/trees"
	var document; // document 补丁包，详见 https://jin-yufeng.github.io/Parser/#/instructions?id=document
	const parseHtml = require('./libs/MpHtmlParser.js');
	const cache = getApp().parserCache = {};
	const CssHandler = require("./libs/CssHandler.js");
	// 散列函数（计算 cache 的 key）
	function Hash(str) {
		for (var i = str.length, hash = 5381; i--;)
			hash += (hash << 5) + str.charCodeAt(i);
		return hash;
	};
	// #endif
	// 动画
	const showAnimation = uni.createAnimation({
		timingFunction: "ease"
	}).opacity(1).step().export();
	const config = require('./libs/config.js');
	// #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO
	// 图片链接去重
	function Deduplicate(src) {
		if (src.indexOf("http") != 0) return src;
		var newSrc = '';
		for (var i = 0; i < src.length; i++) {
			newSrc += (Math.random() >= 0.5 ? src[i].toUpperCase() : src[i].toLowerCase());
			if (src[i] == '/' && src[i - 1] != '/' && src[i + 1] != '/') break;
		}
		newSrc += src.substring(i + 1);
		return newSrc;
	}
	// #endif
	export default {
		name: 'parser',
		data() {
			return {
				// #ifdef APP-PLUS
				loadVideo: false,
				// #endif
				// #ifdef H5
				uid: this._uid,
				// #endif
				showAnimation: '',
				// #ifndef H5
				controls: {},
				// #endif
				nodes: []
			}
		},
		// #ifndef H5
		components: {
			trees
		},
		// #endif
		props: {
			"html": {
				type: null,
				default: null
			},
			// #ifndef MP-ALIPAY
			"autopause": {
				type: Boolean,
				default: true
			},
			// #endif
			"autosetTitle": {
				type: Boolean,
				default: true
			},
			"domain": {
				type: String,
				default: null
			},
			// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
			"gestureZoom": {
				type: Boolean,
				default: false
			},
			// #endif
			// #ifdef MP-WEIXIN || MP-QQ || H5 || APP-PLUS
			"lazyLoad": {
				type: Boolean,
				default: false
			},
			// #endif
			"selectable": {
				type: Boolean,
				default: false
			},
			"tagStyle": {
				type: Object,
				default: () => {
					return {};
				}
			},
			"showWithAnimation": {
				type: Boolean,
				default: false
			},
			"useAnchor": {
				type: Boolean,
				default: false
			},
			"useCache": {
				type: Boolean,
				default: false
			}
		},
		watch: {
			html(html) {
				this.setContent(html, undefined, true);
			}
		},
		mounted() {
			this.imgList = [];
			this.imgList.each = function(f) {
				for (var i = 0; i < this.length; i++) {
					// #ifdef MP-ALIPAY || APP-PLUS
					this[i] = f(this[i], i, this) || this[i];
					// #endif
					// #ifndef MP-ALIPAY || APP-PLUS
					var newSrc = f(this[i], i, this);
					if (newSrc) {
						if (this.includes(newSrc)) this[i] = Deduplicate(newSrc);
						else this[i] = newSrc;
					}
					// #endif
				}
			}
			this.setContent(this.html, undefined, true);
		},
		// #ifdef H5
		beforeDestroy() {
			if (this._observer) this._observer.disconnect();
		},
		// #endif
		methods: {
			// #ifdef H5
			setContent(html, options, observed) {
				if (typeof options == "object")
					for (var key in options) {
						key = key.replace(/-(\w)/g, function() {
							return arguments[1].toUpperCase();
						})
						this[key] = options[key];
					}
				html = html || '';
				if (!html) {
					if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
					return;
				}
				if (typeof html != "string") html = this.Dom2Str(html.nodes || html);
				// 处理 rpx
				if (html.includes("rpx"))
					html = html.replace(/[0-9.]*rpx/g, function($) {
						return parseFloat($) * config.rpx + "px";
					})
				// 处理 tag-style 和 userAgentStyles
				var style = "<style>";
				for (var item in config.userAgentStyles)
					style += (item + '{' + config.userAgentStyles[item] + '}');
				for (var item in this.tagStyle)
					style += (item + '{' + this.tagStyle[item] + '}');
				style += "</style>";
				html = style + html;
				if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
				this.rtf = document.createElement('div');
				this.rtf.innerHTML = html;
				for (var style of this.rtf.getElementsByTagName("style")) {
					style.innerHTML = style.innerHTML.replace(/\s*body/g, "#rtf" + this._uid);
					style.setAttribute("scoped", "true");
				}
				// 懒加载
				if (this.lazyLoad && IntersectionObserver) {
					if (this._observer) this._observer.disconnect();
					this._observer = new IntersectionObserver(changes => {
						for (var change of changes) {
							if (change.isIntersecting) {
								change.target.src = change.target.getAttribute("data-src");
								change.target.removeAttribute("data-src");
								this._observer.unobserve(change.target);
							}
						}
					}, {
						rootMargin: "1000px 0px 1000px 0px"
					})
				}
				var component = 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 (var i = 0; i < imgs.length; i++) {
					var img = imgs[i];
					img.style.maxWidth = "100%";
					img.i = i;
					if (this.domain && img.getAttribute("src")[0] == "/") {
						if (img.getAttribute("src")[1] == "/")
							img.src = (this.domain.includes("://") ? this.domain.split("://")[0] : "http") + ':' + img.getAttribute("src");
						else img.src = this.domain + img.getAttribute("src");
					}
					component.imgList.push(img.src);
					if (img.parentElement.nodeName != 'A') {
						img.onclick = function() {
							if (!this.hasAttribute('ignore')) {
								var preview = true;
								this.ignore = () => preview = false;
								component.$emit('imgtap', this);
								if (preview) {
									uni.previewImage({
										current: this.i,
										urls: component.imgList
									});
								}
							}
						}
					}
					img.onerror = function() {
						component.$emit('error', {
							source: "img",
							target: this
						});
					}
					if (component.lazyLoad && this._observer) {
						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(e) {
						var jump = true,
							href = this.getAttribute("href");
						component.$emit('linkpress', {
							href,
							ignore: () => jump = false
						});
						if (jump && href) {
							if (href[0] == '#') {
								if (component.useAnchor) {
									component.navigateTo({
										id: href.substring(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");
				component.videoContexts = videos;
				for (var video of videos) {
					video.style.maxWidth = "100%";
					video.onerror = function() {
						component.$emit('error', {
							source: "video",
							target: this
						});
					}
					video.onplay = function() {
						if (component.autopause) {
							for (var video of component.videoContexts) {
								if (video != this)
									video.pause();
							}
						}
					}
				}
				// 音频处理
				var audios = this.rtf.getElementsByTagName("audios");
				for (var audio of audios) {
					audio.onerror = function(e) {
						component.$emit('error', {
							source: "audio",
							target: this
						});
					}
				}
				document.getElementById("rtf" + this._uid).appendChild(this.rtf);
				if (this.showWithAnimation)
					this.showAnimation = showAnimation;
				if (!observed) this.nodes = [0];
				this.$nextTick(() => {
					var rect = this.rtf.getBoundingClientRect();
					this.width = rect.width;
					this.$emit("ready", rect);
				})
			},
			Dom2Str(nodes) {
				var str = "";
				for (var node of nodes) {
					if (node.type == "text")
						str += node.text;
					else {
						str += ('<' + node.name);
						for (var attr in node.attrs || {})
							str += (' ' + attr + '="' + node.attrs[attr] + '"');
						if (!node.children || !node.children.length) str += "/>";
						else str += ('>' + this.Dom2Str(node.children) + "</" + node.name + '>');
					}
				}
				return str;
			},
			getText(whiteSpace = true) {
				if (!whiteSpace) return this.rtf.innerText.replace(/\s/g, '');
				return this.rtf.innerText;
			},
			navigateTo(obj) {
				obj.fail = obj.fail || (() => {});
				if (!this.useAnchor)
					return obj.fail({
						errMsg: "Use-anchor attribute is disabled"
					})
				if (!obj.id) {
					window.scrollTo(0, this.rtf.offsetTop);
					return obj.success ? obj.success({
						errMsg: "pageScrollTo:ok"
					}) : null;
				}
				var target = document.getElementById(obj.id);
				if (!target) return obj.fail({
					errMsg: "Label not found"
				});
				uni.pageScrollTo({
					scrollTop: this.rtf.offsetTop + target.offsetTop,
					success: obj.success,
					fail: obj.fail
				});
			},
			// #endif
			// #ifndef H5
			setContent(html, options, observed) {
				if (typeof options == "object")
					for (var key in options) {
						key = key.replace(/-(\w)/g, function($, $1) {
							return $1.toUpperCase();
						})
						this[key] = options[key];
					}
				if (this.showWithAnimation)
					this.showAnimation = showAnimation;
				if (!html) {
					if (observed) return;
					else this.nodes = [];
				} else if (typeof html == "string") {
					// 缓存读取
					if (this.useCache) {
						var hash = Hash(html);
						if (cache[hash])
							this.nodes = cache[hash];
						else {
							this.nodes = parseHtml(html, this);
							cache[hash] = this.nodes;
						}
					} else this.nodes = parseHtml(html, this);
					this.$emit('parse', this.nodes);
				} else if (Object.prototype.toString.call(html) == "[object Array]") {
					if (!observed) this.nodes = html;
					else this.nodes = [];
					// 非本插件产生的 array 需要进行一些转换
					if (html.length && html[0].PoweredBy != "Parser") {
						const Parser = {
							_imgNum: 0,
							_videoNum: 0,
							_audioNum: 0,
							_domain: this.domain,
							_protocol: this.domain ? (this.domain.includes("://") ? this.domain.split("://")[0] : "http") : undefined,
							_STACK: [],
							CssHandler: new CssHandler(this.tagStyle)
						};
						Parser.CssHandler.getStyle('');

						function DFS(nodes) {
							for (var i = 0, node; node = nodes[i++];) {
								if (node.type == "text") continue;
								node.attrs = node.attrs || {};
								for (var item in node.attrs) {
									if (!config.trustAttrs[item]) node.attrs[item] = undefined;
									else if (typeof node.attrs[item] != "string") node.attrs[item] = node.attrs[item].toString();
								}
								config.LabelAttrsHandler(node, Parser);
								if (config.blockTags[node.name]) node.name = 'div';
								else if (!config.trustTags[node.name]) node.name = 'span';
								if (node.children && node.children.length) {
									Parser._STACK.push(node);
									DFS(node.children);
									Parser._STACK.pop();
								} else node.children = undefined;
							}
						}
						DFS(html);
						this.nodes = html;
					}
				} else if (typeof html == 'object' && html.nodes) {
					this.nodes = html.nodes;
					console.warn("Parser 类型错误：object 类型已废弃，请直接将 html 设置为 object.nodes （array 类型）");
				} else {
					return this.$emit('error', {
						source: "parse",
						errMsg: "传入的nodes数组格式不正确！应该传入的类型是array，实际传入的类型是：" + typeof html.nodes
					});
				}
				// #ifdef APP-PLUS
				this.loadVideo = false;
				// #endif
				if (document) this.document = new document("html", this.html || html, this);

				this.$nextTick(() => {
					this.imgList.length = 0;
					this.videoContexts = [];
					const getContext = (components) => {
						for (var i = components.length; i--;) {
							let component = components[i];
							if (component.$options.name == "trees") {
								var observered = false;
								for (var j = component.nodes.length, item; item = component.nodes[--j];) {
									if (item.c) continue;
									if (item.name == 'img') {
										if (item.attrs.src && item.attrs.i) {
											// #ifndef MP-ALIPAY || APP-PLUS
											if (this.imgList.indexOf(item.attrs.src) == -1)
												this.imgList[item.attrs.i] = item.attrs.src;
											else this.imgList[item.attrs.i] = Deduplicate(item.attrs.src);
											// #endif
											// #ifdef MP-ALIPAY || APP-PLUS
											this.imgList[item.attrs.i] = item.attrs.src;
											// #endif
										}
										// #ifndef MP-ALIPAY
										if (!observered) {
											observered = true;
											if (this.lazyLoad && uni.createIntersectionObserver) {
												if (component._observer) component._observer.disconnect();
												component._observer = uni.createIntersectionObserver(component);
												component._observer.relativeToViewport({
													top: 1000,
													bottom: 1000
												}).observe('._img', res => {
													component.imgLoad = true;
													component._observer.disconnect();
													component._observer = null;
												})
											} else
												component.imgLoad = true;
										}
										// #endif
									}
									// #ifndef MP-ALIPAY
									else if (item.name == 'video') {
										var context = uni.createVideoContext(item.attrs.id, component);
										context.id = item.attrs.id;
										this.videoContexts.push(context);
									}
									// #endif
									// #ifdef MP-WEIXIN
									else if (item.name == 'audio' && item.attrs.autoplay)
										wx.createAudioContext(item.attrs.id, component).play();
									// #endif
									// 设置标题
									else if (item.name == "title" && this.autosetTitle && item.children[0].type == "text" && item.children[
											0].text)
										uni.setNavigationBarTitle({
											title: item.children[0].text
										})
									// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
									if (item.attrs && item.attrs.id) {
										this.anchors = this.anchors || [];
										this.anchors.push({
											id: item.attrs.id,
											node: component
										})
									}
									// #endif
								}
							}
							if (component.$children.length)
								getContext(component.$children)
						}
					}
					// #ifdef MP-TOUTIAO
					setTimeout(() => {
						// #endif
						getContext(this.$children);
						// #ifndef APP-PLUS
						this.createSelectorQuery()
						// #endif
						// #ifdef APP-PLUS
						uni.createSelectorQuery().in(this)
						// #endif
						.select("._contain").boundingClientRect().exec(res => {
							this.width = (res[0] ? res[0] : res).width;
							this.$emit("ready", res);
						});
						// #ifdef MP-TOUTIAO
					}, 200)
					// #endif
					// #ifdef APP-PLUS
					setTimeout(() => {
						this.loadVideo = true;
					}, 3000);
					// #endif
				})
			},
			getText(whiteSpace = true) {
				var text = "";
				const DFS = (node) => {
					if (node.type == "text") return text += node.text;
					else {
						if (whiteSpace && (((node.name == 'p' || node.name == "div" || node.name == "tr" || node.name == "li" ||
								/h[1-6]/.test(node.name)) && text && text[text.length - 1] != '\n') || node.name == "br"))
							text += '\n';
						if (node.children)
							for (var i = 0; i < node.children.length; i++)
								DFS(node.children[i]);
						if (whiteSpace && (node.name == 'p' || node.name == "div" || node.name == "tr" || node.name == "li" || /h[1-6]/.test(
								node.name)) && text && text[text.length - 1] != '\n')
							text += '\n';
						else if (whiteSpace && node.name == "td") text += '\t';
					}
				}
				var nodes = ((this.nodes && this.nodes.length) ? this.nodes : (this.html[0] && (this.html[0].name || this.html[0].type) ?
					this.html : []));
				if (!nodes.length) return "";
				for (var i = 0; i < this.data.html.length; i++) DFS(this.data.html[i]);
				return text;
			},
			navigateTo(obj) {
				obj.fail = obj.fail || (() => {});
				if (!this.useAnchor)
					return obj.fail({
						errMsg: "Use-anchor attribute is disabled"
					})
				var Scroll = (selector, component) => {
					const query = uni.createSelectorQuery().in(component ? component : this);
					query.select(selector).boundingClientRect();
					query.selectViewport().scrollOffset();
					query.exec(res => {
						if (!res || !res[0])
							return obj.fail({
								errMsg: "Label not found"
							});
						uni.pageScrollTo({
							scrollTop: res[1].scrollTop + res[0].top,
							success: obj.success,
							fail: obj.fail
						})
					})
				}
				if (!obj.id) Scroll("._contain");
				else {
					// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
					Scroll('._contain >>> #' + obj.id + ', ._contain >>> .' + obj.id);
					// #endif
					// #ifdef MP-BAIDU || MP-ALIPAY || APP-PLUS
					for (var anchor of this.anchors) {
						if (anchor.id == obj.id) {
							Scroll("#" + obj.id + ", ." + obj.id, anchor.node);
						}
					}
					// #endif
				}
			},
			// #endif
			tap(e) {
				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
				if (this.gestureZoom && e.timeStamp - this.lastTime < 300) {
					if (this.zoomIn) {
						this.animation.translateX(0).scale(1).step();
						uni.pageScrollTo({
							scrollTop: (e.touches[0].pageY - e.currentTarget.offsetTop + this.initY) / 2 - e.touches[0].clientY,
							duration: 400
						})
					} else {
						var initX = e.touches[0].pageX - e.currentTarget.offsetLeft;
						this.initY = e.touches[0].pageY - e.currentTarget.offsetTop;
						this.animation = uni.createAnimation({
							transformOrigin: initX + "px " + this.initY + "px 0",
							timingFunction: "ease-in-out"
						});
						// #ifdef MP-TOUTIAO
						this.animation.opacity(1);
						// #endif
						this.animation.scale(2).step();
						this.translateMax = initX / 2;
						this.translateMin = (initX - this.width) / 2;
						this.translateX = 0;
					}
					this.zoomIn = !this.zoomIn;
					this.showAnimation = this.animation.export();
				}
				this.lastTime = e.timeStamp;
				// #endif
			},
			touchstart(e) {
				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
				if (e.touches.length == 1)
					this.initX = this.lastX = e.touches[0].pageX;
				// #endif
			},
			touchmove(e) {
				// #ifndef MP-BAIDU || MP-ALIPAY || APP-PLUS
				var diff = e.touches[0].pageX - this.lastX;
				if (this.zoomIn && e.touches.length == 1 && Math.abs(diff) > 20) {
					this.lastX = e.touches[0].pageX;
					if ((this.translateX <= this.translateMin && diff < 0) || (this.translateX >= this.translateMax && diff > 0))
						return;
					this.translateX += (diff * Math.abs(this.lastX - this.initX) * 0.05);
					if (this.translateX < this.translateMin)
						this.translateX = this.translateMin;
					if (this.translateX > this.translateMax)
						this.translateX = this.translateMax;
					this.animation.translateX(this.translateX).step();
					this.showAnimation = this.animation.export();
				}
				// #endif
			},
			getVideoContext(id) {
				if (!id) return this.videoContexts;
				else
					for (var i = this.videoContexts.length; i--;)
						if (this.videoContexts[i].id == id) return this.videoContexts[i];
				return null;
			}
		}
	}
</script>

<style>
	/* #ifndef MP-WEIXIN || APP-PLUS */
	:host {
		display: block;
		overflow: scroll;
		-webkit-overflow-scrolling: touch;
	}

	._contain {
		display: inherit;
	}

	/* #endif */
</style>
