/******************************************
 * [Dinosaur7]
 * Copyright (c) 2023 kougen sai
 * Released under the MIT license
 * https://opensource.org/licenses/mit-license.php 
 * 
 * @version 3.1
 * @contact cycauo@gmail.com
 * @keyword simple pure html client framework, javascript frontend, React, Vue, Angular, SPA
 * this framework help you to implement SPA with basic skill and no longer need to hard work.
 * The name [Dinosaur7] comes from that My daughter loved dinosaurs when she was 7 years old.
 * 
 * Server side setting
 *   proxy *.* -			# response as is
 *   proxy /{api uri}/* -	# response as is
 *   proxy * /index.html	# response index.html that includes dinosaur7.js
 *
 * All Dinosaur7 defined Attribute in HTML tag
 *   d7app, d7layout, d7page, d7comp[, d7part, d7dummy]
 *   d7[, d7id]
 * 
 *   d7part: 	To declare solid part. Reconstruct whole part even if there are minor changes in child element.
 *   d7dummy: 	Instruct to remove target element from template.
 *   d7id: 		Only for performance improvement when necessary.
 ******************************************/
const {d7app, d7util} = (()=>{
	/*******************************
	 * DEFINE
	 *******************************/
	let _DEV_MODE = 'prod'; 
	let _BASE_URI = '';
	let _BASE_API_URI = '';
	let _DEFAULT_LAYOUT = '';
	let _DEFAULT_EXTENSION = '.html';
	let _ROUTE_MAP = {};
	let _SHOW_PROGRESS = false;
	let _ENABLE_HIST_BACK = false;
	
	let _dev = false;
	let _debug = false;
	
	const _LOGIC    = {start:   "{[%", close: "%]}"};	// logic mark
	const _PRINT    = {start:    "{%", close: "%}" };	// printer mark
	const _PRINT_EN = {start: "_D7p_", close: "_qD7_"};	// encoded printer mark
	/*******************************
	 * Dinosaur7 APP
	 *******************************/
	let routeIntercept = async (nameOrPath, query) => {return true};
	class App {
		constructor() {
			this._MP_APP = null;	// mount point, main app element
			this._HIST_INFO = [];
			this._CURR_PATH = '';
			this._CURR_QUERY = {};
		}
	
		setting = (opt)=>{
			if (!opt) opt = {};
	
			_DEV_MODE = opt.devMode || _DEV_MODE;
			_BASE_URI = opt.baseHtmlUri || _BASE_URI;
			_BASE_API_URI = opt.baseApiUri || _BASE_API_URI;
			_DEFAULT_LAYOUT = opt.defaultLayout || _DEFAULT_LAYOUT;
			_DEFAULT_EXTENSION = opt.defaultExtension || _DEFAULT_EXTENSION;
			_SHOW_PROGRESS = opt.showProgress || _SHOW_PROGRESS;
			_ENABLE_HIST_BACK = opt.enableHistBack || _ENABLE_HIST_BACK;

			if(_BASE_URI === '/') _BASE_URI = '';
			if(!_DEFAULT_EXTENSION.startsWith('.')) _DEFAULT_EXTENSION = '.' + _DEFAULT_EXTENSION;
	
			_dev   = (_DEV_MODE !== 'prod');
			_debug = (_DEV_MODE === 'debug');
	
			for(let row of (opt.route || [])) {
				_ROUTE_MAP[row.name.toUpperCase()] = {
					name: row.name,
					path: util.parseUrl(row.path).path, // Can only be a PATH! not URL
					description: row.description,
					preload: row.preload,
				};
			}
	
			this._topPage = opt.topPage;
		}
	
		// when document load
		start = async ()=>{
			this._entryPage = util.parseUrl(window.location.href);
			if (this._entryPage.path === '' || this._entryPage.path === '/'
				|| this._entryPage.path ===  '/index'
				|| this._entryPage.path === ('/index' + _DEFAULT_EXTENSION)
			) {
				this._entryPage = util.parseUrl(this._topPage);
			}
	
			if (_SHOW_PROGRESS) util.inProgress();
			const loadResults = [];
			for(let key in _ROUTE_MAP) {
				if (!_ROUTE_MAP[key].preload) continue;
				loadResults.push(Template.load(_ROUTE_MAP[key].path));
			}
			await Promise.all(loadResults);
			if (_SHOW_PROGRESS) util.inProgress(false);
	
			this._MP_APP = document.querySelector('[d7app]');
			if (!this._MP_APP) console.error('Not found [d7app] in root html.');
			this.route(this._entryPage.path, this._entryPage.query, 0);
	
			window.onpopstate = (e)=>{
				if (!e.state?.histIdx) return;
				this.route(e.state.path, e.state.query, e.state.histIdx);
			};
		}

		setRouteInterceptor = (interceptor)=>{
			routeIntercept = interceptor;
		}

		route = async (nameOrPath, query, histIdx)=>{
			const pageInfo = util.parseUrl(nameOrPath);
			this._CURR_PATH = pageInfo.path.replace(/\.\w+$/,'');
			this._CURR_QUERY = {...pageInfo.query, ...query};
			
			if (!await routeIntercept(nameOrPath, query)) return;

			if (histIdx && !_ENABLE_HIST_BACK) {
				let lastPage = this._HIST_INFO[this._HIST_INFO.length -1];
				history.replaceState(null, 'dummy', util.stringifyUrl(lastPage.path, lastPage.query));
				return;
			}

			// save prev status;
			if (this._histIdx) {
				let histDom = this._MP_APP.cloneNode(true);
				cloneExtraInfo(this._MP_APP, histDom);

				let histInfo = this._HIST_INFO[this._histIdx-1];
				histInfo.dom = histDom;
				histInfo.page = this._page;
				histInfo.layout = cloneLayoutData(this._page._COMP.layout);
			}

			// load hist page when hist back.
			if (histIdx && histIdx <= this._HIST_INFO.length) {
				let histInfo = this._HIST_INFO[histIdx-1];

				this._MP_APP.replaceWith(histInfo.dom);
				this._MP_APP = document.querySelector('[d7app]');
				restoreExtraInfo(histInfo.dom);
				this._page = histInfo.page;
				this._page._COMP.layout = restoreLayoutData(this._page._COMP.layout, histInfo.layout);
				this._page.rebind();
				this._histIdx = histIdx;
				PREV_LAYOUT_COMP = this._page._COMP.layout;
				return;
			}

			this._page = await Page.load(pageInfo.path);
			await this._page.mount(this._CURR_QUERY, this._MP_APP);
			this._HIST_INFO.push({path: this._CURR_PATH, query: this._CURR_QUERY});
			this._histIdx = this._HIST_INFO.length;
			PREV_LAYOUT_COMP = this._page._COMP.layout;
			history.pushState({ path: this._CURR_PATH, query: this._CURR_QUERY, histIdx: this._histIdx}, 'dummy', util.stringifyUrl(this._CURR_PATH, this._CURR_QUERY));
		}
	
		currentPath = () => {
			return this._CURR_PATH;
		}
	
		currentQuery = () => {
			return this._CURR_QUERY;
		}
	
		storeData = (key, val)=>{
			try {
				localStorage.setItem('dummytest', '123');
				if (localStorage.getItem('dummytest') === '123') {
					if (typeof val === 'undefined') return localStorage.getItem(key);
	
					if (val != null) return localStorage.setItem(key, val);
	
					return localStorage.removeItem(key);
				}
			} catch(e) {;}
	
			if (typeof val === 'undefined') {
				let keyvals = {};
				(document.cookie || '').split('; ').forEach((keyval)=>{
					let data = keyval.split('=');
					keyvals[data[0]] = decodeURIComponent(data[1]);
				});
				return keyvals[key];
			}
	
			let expires = new Date();
			if (val != null) expires.setTime(expires.getTime() + 365*24*60*60*1000);
			else expires.setTime(expires.getTime() - 1000); // for delete
			document.cookie = `${key}=${encodeURIComponent(val)}; expires=${expires.toGMTString()}`;
		}
	}
	let PREV_LAYOUT_COMP = null;
	const cloneExtraInfo = (pageDom, histDom) => {
		for(let idx=0; idx< pageDom.childNodes.length; idx++) {
			let tarNode = pageDom.childNodes[idx];
			let histNode = histDom.childNodes[idx];
			if(tarNode.SeQ) histNode.SeQ = tarNode.SeQ;
			if(tarNode.EiF) histNode.EiF = tarNode.EiF;
			cloneExtraInfo(tarNode, histNode);
		}
	}
	const restoreExtraInfo = (histDom) => {
		for(let histNode of histDom.childNodes) {
			if(histNode.EiF) {
				let eIf = histNode.EiF;
				delete histNode.EiF;
				for(let eName in eIf) {
					let eInfo = eIf[eName];
					bindEvent(histNode, eInfo, eInfo.tarComp);
				}
			}
			restoreExtraInfo(histNode);
		}
	}
	const cloneLayoutData = (comp) => {
		if (!comp) return null;

		let model = {main: util.cloneVal(comp._MODEL), tplt: [util.cloneVal(comp._TEMPLATE.oldVDom), util.cloneVal(comp._TEMPLATE.oldModel)], children:{}};
		for(let compId in comp.children) {
			model.children[compId] = [comp.children[compId], cloneLayoutData(comp.children[compId])];
		}
		return model;
	}
	const restoreLayoutData = (comp, model) => {
		if (!comp) return null;

		for(let key in comp._MODEL) {
			delete comp._MODEL[key];
		}
		for(let key in model.main) {
			comp._MODEL[key] = model.main[key];
			comp._TEMPLATE.oldVDom = model.tplt[0];
			comp._TEMPLATE.oldModel = model.tplt[1];
		}
		comp.children = {};
		for(let compId in model.children) {
			comp.children[compId] = restoreLayoutData(model.children[compId][0], model.children[compId][1]);
		}
		return comp;
	}
	/*******************************
	 * PAGE
	 *******************************/
	class Page {
		constructor(comp) {
			this._COMP = comp;
		}
	
		static load = async (path)=>{
			const comp = await Component.load(path, _DEFAULT_LAYOUT);
			let page = new Page(comp);
			return page;
		}
	
		mount = async (query, mountPoint)=>{
			await this._COMP.mount(query, mountPoint);
			this._COMP._MP_SELF.setAttribute('_d7page', '');
			return this;
		}
	
		rebind = ()=>{
			this._COMP.rebind();
		}
	}
	/*******************************
	 * LAYOUT
	 *******************************/
	class Layout {
		constructor(comp) {
			this._COMP = comp;
		}
	
		static load = async (path)=>{
			const comp = await Component.load(path, '-BAN-');
			let layout = new Layout(comp);
			return layout;
		}
	
		mount = async (query, mountPoint)=>{
			await this._COMP.mount(query, mountPoint);
			this._COMP._MP_LAYOUT.setAttribute('_d7layout', '');
			return this;
		}
	}
	/*******************************
	 * MODAL
	 *******************************/
	class Modal {
		constructor(comp) {
			this._COMP = comp;
	
			let tmpDiv = document.createElement('div');
			tmpDiv.innerHTML = "<div class='d7_modal_overlay'><div class='d7_modal_form'></div></div>";
			this.form = tmpDiv.querySelector('.d7_modal_form');
			this.container = tmpDiv.querySelector('.d7_modal_overlay');
			this.form.onclick = (e)=>{e.stopPropagation();}
		}
	
		static load = async (owner, nameOrPath, autoClose)=>{
			const pageInfo = util.parseUrl(nameOrPath);
	
			const comp = await Component.load(pageInfo.path);
			const modal = new Modal(comp);
	
			comp.close = async (modalVal)=>{ 
				modal.close(); 
				if (!comp.callback) return false;

				let rtnVal = await comp.callback(modalVal);
				afterInvoke(rtnVal, owner);
			}

			if (autoClose) {
				let fnClose = async ()=>{
					modal.close(); 
					if (!comp.callback) return false;
					
					return await comp.callback();
				}
				//modal.container.onclick = fnClose;
				bindEvent(modal.container, {name: 'click', func:fnClose, params:[], tars:''}, owner);
			}
	
			return modal;
		}
	
		open = async (query, callback)=>{
			this.container.classList.remove("d7_modal_overlay_closing");
			setTimeout((container)=>{
				container.classList.add("d7_modal_overlay_show");
			}, 1, this.container);

			this._COMP.callback = callback;
			await this._COMP.mount(query, this.form, 310);
	
			document.body.appendChild(this.container);
		}
	
		close = ()=>{
			let container = document.querySelector('.d7_modal_overlay');
			container.classList.remove("d7_modal_overlay_show");
			container.classList.add("d7_modal_overlay_closing");
	
			setTimeout((container)=>{
				document.body.removeChild(container);
			}, 300, container);
		}
	}
	/*******************************
	 * COMPONENT
	 *******************************/
	let _compId = 100;
	const _newCompId = ()=>{return 'd7c' + _compId++;}
	class Component {
	
		constructor(template, defaultLayoutUrl) {
			this.id = _newCompId();
			this.name = null;
			this.parent = null;
			this.children = {};

			this._TEMPLATE = template;
			this._DEFAULT_LAYOUT_URL = defaultLayoutUrl;
			this._MP_LAYOUT = null;
			this._MP_SELF = null;
			this._QUERY = null;
			this._MODEL = {};

			this._STORE = {};
			this._EX_FUNCS = {};
		}
	
		static load = async (path, defaultLayoutUrl)=>{
			const template = await Template.load(path);
			return new Component(template, defaultLayoutUrl);
		}
	
		mount = async (query, mountPoint, initWaittime)=>{
			this._QUERY = util.cloneVal(query);
			this._MP_LAYOUT = mountPoint;
			this._MP_SELF = mountPoint;
			this.name = mountPoint.getAttribute("name");
			//this._MP_LAYOUT.style.display = "none";

			this._TEMPLATE.bind(this);
			await this.renderLayout(); // call fnAfterInit in Layout's renderSelf.
			let fnAfterInit = await this.renderSelf();
			this._MP_SELF.setAttribute('_d7comp', util.stringifyUrl(this._TEMPLATE._PATH, query));
	
			if (!fnAfterInit) return;
			if (!initWaittime) {fnAfterInit(); return;}
	
			setTimeout(()=>{
				fnAfterInit();
			}, initWaittime);
		}

		close = () => {
			this._REMOVED = true;
			this._MP_LAYOUT.innerHTML = '';
			delete this.parent?.children[this.id];
		}

		rebind = ()=>{
			if(this.layout) this.layout.rebind();
			this._MP_SELF = document.querySelector(`._${this.id}`);
			for(let compName in this.children) {
				let child = this.children[compName];
				child.rebind();
				child.parent = this;
			}
		}

		renderLayout = async ()=>{
			if (this._DEFAULT_LAYOUT_URL === '-BAN-') return;
	
			const layoutNameOrUrl = this._TEMPLATE.fnLayoutUrl(this._MODEL) || this._DEFAULT_LAYOUT_URL;
			if (!layoutNameOrUrl) return;

			const layoutInfo = util.parseUrl(layoutNameOrUrl);
			if (!layoutInfo.path.match(/\.\w+$/)) layoutInfo.path = layoutInfo.path + _DEFAULT_EXTENSION;
			const currLayoutInfo = util.parseUrl(this._MP_LAYOUT.getAttribute('_d7comp'));

			if (layoutInfo.path !== currLayoutInfo.path || util.hasDiff(layoutInfo.query, currLayoutInfo.query)) {
				const layout = await Layout.load(layoutInfo.path);
				await layout.mount(layoutInfo.query, this._MP_LAYOUT);
				this.layout = layout._COMP;
			} else {
				if (!this._MP_LAYOUT.hasAttribute('d7app')) { // just component structure. not page layout.
					let layoutId = 'XXX';
					for(let claz of this._MP_LAYOUT.classList) {
						if (claz.startsWith('_d7c')) layoutId = claz;
					}
					this._MP_SELF = this._MP_LAYOUT.querySelector(`[d7page]:not(.${layoutId}ex *)`);
					return;
				}

				this.layout = PREV_LAYOUT_COMP;
			}

			this._MP_SELF = this._MP_LAYOUT.querySelector(`[d7page]:not(._${this.layout.id}ex *)`);
			if (!this._MP_SELF) console.error('No [d7page] defined in layout. ' + layoutInfo.path);
		}
	
		renderSelf = async ()=>{
			for(let clazz of this._MP_SELF.classList) {
				if (clazz.startsWith('_d7c') && !clazz.endsWith('ex')) this._MP_SELF.classList.remove(clazz);
			}
			this._MP_SELF.classList.add(`_${this.id}`);
			this._MP_SELF.innerHTML = "";
	
			let fnAfterInit = await this._TEMPLATE.render();
			this._MP_LAYOUT.style.display = "block";
			return fnAfterInit;
		}
	
		reflect = async (selectors, currPart)=>{
			if (this._REMOVED) return;

			// reflect all;
			if (!selectors || selectors.length < 1) {
				await this._TEMPLATE.reflect();
				return;
			}

			// replace specified parts
			let tarPart;
			for(let selector of selectors) {
				if (_dev) console.time(`<ReplacePart> ${selector}`);
	
				if (selector === 'this') {
					await this._TEMPLATE.replacePart(currPart);
				} else {
					tarPart = this.element(selector);
					if (!tarPart) console.error(`Part element not exists. ${selector} in ${this.name}`);
					await this._TEMPLATE.replacePart(tarPart);
				}
				if (_dev) console.timeEnd(`<ReplacePart> ${selector}`);
			}
		}
	
		loadComponent = async (nameOrPath, query, selector, compName)=>{
			let referencePoint = this.element(selector);
			let mountPoint = document.createElement('div');
			referencePoint.appendChild(mountPoint);
	
			const pageInfo = util.parseUrl(nameOrPath);
			const comp = await Component.load(pageInfo.path);
			await comp.mount(query, mountPoint);
			mountPoint.classList.add(`_${this.id}ex`);
	
			comp.name = compName || comp.name || pageInfo.name || comp.id;
			this.children[comp.id] = comp;
			comp.parent = this;
	
			return comp;
		}
	
		getComponent = (idOrName)=>{
			let comp = this.children[idOrName];
			if (comp) return comp;

			for(let key in this.children) {
				comp = this.children[key];
				if (comp.name === idOrName) return comp;
			}
			console.error('Component not exists. ' + idOrName);
		}

		removeComponent = (idOrName)=>{
			let comp = this.children[idOrName];
			if (comp) {
				comp._REMOVED = true;
				comp._MP_LAYOUT.remove();
				delete this.children[idOrName];
				return;
			}

			for(let key of Object.keys(this.children)) {
				comp = this.children[key];
				if (comp.name !== idOrName) continue;

				comp._REMOVED = true;
				comp._MP_LAYOUT.innerHTML = '';
				delete this.children[key];
			}
		}
		
		element = (selector, indexOrNone)=>{
			if (!selector) return this._MP_SELF;
			if (selector.startsWith('d7')) selector = `[d7id="${selector.substring(2)}"]`;
			if (typeof indexOrNone === 'undefined') {
				return this._MP_SELF.querySelector(`${selector}:not(._${this.id}ex *)`);
			}
	
			let elements = this._MP_SELF.querySelectorAll(`${selector}:not(._${this.id}ex *)`);
			if (elements.length < (indexOrNone+1)) {
				console.warn(`[WARN] Target element not exists. ${selector} [${indexOrNone}]`);
			}
			return elements[indexOrNone];
		}
	}
	class ComponentIf {
		constructor(component) {
			this.comp = component;
		}
		getId = ()=>{
			return this.comp.id;
		}
		getName = ()=>{
			return this.comp.name;
		}
		reflect = async (selectors)=>{
			return await this.comp.reflect(selectors);
		}
		close = (val)=>{
			return this.comp.close(val);
		}
		getParent = ()=>{
			return new ComponentIf(this.comp.parent);
		}
		loadComponent = async (path, query, selector, compName)=>{
			let child =  await this.comp.loadComponent(path, query, selector, compName);
			return new ComponentIf(child);
		}
		getComponent = (idOrName)=>{
			return new ComponentIf(this.comp.getComponent(idOrName));
		}
		removeComponent = (idOrName)=>{
			this.comp.removeComponent(idOrName);
		}

		set = (key, val)=>{
			this.comp._STORE[key] = val;
		}
		get = (key)=>{
			return this.comp._STORE[key];
		}
		regist = (funcName, func)=>{
			this.comp._EX_FUNCS[funcName] = func;
		}
		invoke = async (funcName, ...params)=>{
			let definedFunc = this.comp._EX_FUNCS[funcName];
			if (!definedFunc) console.error('Function not exists. ' + funcName);

			let rtnVal = await definedFunc.apply(null, params);

			afterInvoke(rtnVal, this.comp);
		}
		emitEvent = (selector, eventName, val)=>{
			let tarEle = this.comp.element(selector);
			if (!tarEle) console.error("Target not found for [emitEvent]. " + selector);
			if (eventName.startsWith("on")) eventName = eventName.substring(2);
			let e = null;
			if (eventName.startsWith('key')) {
				e = new KeyboardEvent(eventName, val);
			} else if (eventName.includes('click') || eventName.includes('mouse') || eventName.includes('pointer')) {
				e = new MouseEvent(eventName, val);
			} else {
				e = new CustomEvent(eventName, {detail: val});
			}
			tarEle.dispatchEvent(e);
		}
		show = (selector)=>{
			if (typeof selector === 'undefined') {
				if (this.comp._MP_SELF.style.display_bak) {
					this.comp._MP_SELF.style.display = this.comp._MP_SELF.style.display_bak;
				} else {
					this.comp._MP_SELF.style.display = null;
				}
				return;
			}
			let target = this.comp.element(selector);
			if (!target) console.error('Element not exists. ' + selector);
			if (target.style.display_bak) {
				target.style.display = target.style.display_bak;
			} else {
				target.style.display = null;
			}
			return;
		}
		hide = (selector)=>{
			if (typeof selector === 'undefined') {
				if (this.comp._MP_SELF.style.display && this.comp._MP_SELF.style.display !== 'none') {
					this.comp._MP_SELF.style.display_bak = this.comp._MP_SELF.style.display;
				}
				this.comp._MP_SELF.style.display = "none";
				return;
			}
			let target = this.comp.element(selector);
			if (!target) console.error('Element not exists. ' + selector);
			if (target.style.display && target.style.display !== 'none') {
				target.style.display_bak = target.style.display;
			}
			target.style.display = "none";
			return;
		}
		s = (selector, indexOrNone)=>{
			return this.comp.element(selector, indexOrNone);
		}
		sAll = (selector)=>{
			if (!selector) return [this.comp._MP_SELF];
			if (selector.startsWith('d7')) selector = `[d7id="${selector.substring(2)}"]`;
			return this.comp._MP_SELF.querySelectorAll(`${selector}:not(._${this.comp.id}ex *)`);
		}
		modal = async (nameOrPath, autoClose)=>{
			return await Modal.load(this.comp, nameOrPath, autoClose);
		}
	}
	const afterInvoke = (rtnVal, tarComp) => {
		if (rtnVal === false) return;

		if (typeof rtnVal === 'undefined') {
			tarComp.reflect();
			return;
		}

		if (rtnVal.constructor.name !== 'Promise') {
			tarComp.reflect().then(()=>{
				if (typeof rtnVal === 'function') rtnVal();				
			});
			return;
		}
		
		rtnVal.then((resolvedVal)=> {
			if (resolvedVal === false) return;

			tarComp.reflect().then(()=>{
				if (typeof resolvedVal === 'function') resolvedVal();				
			});
		});
	}
	/*******************************
	 * TEMPLATE
	 *******************************/
	const _CACHE_TPLT = {};
	class Template {
		constructor(path, htmlText) {
			if (!path) return;

			this._PATH = path;
			const { layoutExpr, styleCode, scriptCode, rawDom } = analyze(htmlText)
	
			this.fnLayoutUrl = compileHtml(layoutExpr);
	
			this.fnStyle = styleCode ? compileStyle(styleCode) : null;
	
			let code = `/*** script: ${this._PATH} ***/\n`;
			code += scriptCode;
			code += "\n\n" + genFnVirtualDom(rawDom, path);
			code += "\n\nreturn {init: typeof init === 'function' ? init : null, genVirtualDom: genVirtualDom};";
			const AsyncFunction = Object.getPrototypeOf(async ()=>{}).constructor;
			this.fnUserScript = new AsyncFunction("me", "_m", "query", code);
		}
	
		static load = async (path)=>{
			if (!path.match(/\.\w+$/)) path = path + _DEFAULT_EXTENSION;

			let template = _CACHE_TPLT[path];
			if (template) {
				let newTplt = new Template(path, '');
				newTplt.fnLayoutUrl = template.fnLayoutUrl;
				newTplt.fnStyle = template.fnStyle;
				newTplt.fnUserScript = template.fnUserScript;
				return newTplt;
			}
	
			if (_debug) console.log(`[DEBUG] Loading html. ${path}`);
			let response = await httpClient.loadHtml(_BASE_URI + path);
			if (response._STATUS !== 'OK') {
				if (path.replace(/\.\w+$/,'') === app.currentPath()) {
					console.error('Not found page. ' + path);
					window.location.href = '/';
				} else {
					console.error('Loading HTML failed. ' + path);
				}
				return;
			}

			template = new Template(path, response.data);
			_CACHE_TPLT[path] = template;
	
			return template;
		}

		bind = async (tarComp)=>{
			let {init, genVirtualDom} = await this.fnUserScript(new ComponentIf(tarComp), tarComp._MODEL, tarComp._QUERY);
			this.tarComp = tarComp;
			this.genVirtualDom = genVirtualDom;
			this.fnUserInit = init;
			return this;
		}

		render = async ()=>{

			let fnAfterInit = null;
			if (this.fnUserInit) {
				if (_dev) console.log(`Running init().       ${this._PATH}`);
				fnAfterInit = await this.fnUserInit();
			}

			await this.refresh();

			return fnAfterInit;
		}

		refresh = async ()=>{
			let vDom = this.genVirtualDom(this.tarComp._MODEL);
	
			if (_dev) console.time(   `<RenderDom>           ${this._PATH}`);
			let dom = await genDom(vDom, this.tarComp);
			if (this.fnStyle) dom.dom.childNodes[0].innerHTML = this.fnStyle(`._${this.tarComp.id}`, `._${this.tarComp.id}ex`);
			let nLen = dom.dom.childNodes.length;
			while(nLen-- > 0) {
				this.tarComp._MP_SELF.appendChild(dom.dom.childNodes[0]);
			}
			if (_dev) console.timeEnd(`<RenderDom>           ${this._PATH}`);
			runAfterRender(dom.aftTars);
	
			this.oldVDom = vDom;
			this.oldModel = util.cloneVal(this.tarComp._MODEL);
		}
	
		reflect = async ()=>{
			if (!util.hasDiff(this.tarComp._MODEL, this.oldModel)) return;

			let vDomNew = this.genVirtualDom(this.tarComp._MODEL);
	
			if (_dev) console.time(   `<ReflectDom>          ${this._PATH}`);
			let aftTars = await reflectDom(vDomNew, this.oldVDom, this.tarComp._MP_SELF, this.tarComp);
			if (_dev) console.timeEnd(`<ReflectDom>          ${this._PATH}`);
			runAfterRender(aftTars);
	
			this.oldVDom = vDomNew;
			this.oldModel = util.cloneVal(this.tarComp._MODEL);
		}
	
		/***
		 * optional function
		 * Replace or Remove element.
		 ***/
		replacePart = async (tarEle)=>{
	
			// all parent nodes
			let nodes = [];
			while(true) {
				nodes.push(tarEle);
				if (tarEle.hasAttribute('_d7comp')) break;
				tarEle = tarEle.parentNode;
			}
	
			// consecutively connected nodes(no custom element)
			let parents = [tarEle];
			for(let idx=nodes.length-2; idx>-1; idx--) {
				let n = nodes[idx];
				if (!n.SeQ) break;
				parents.push(n);
			}
			if (parents.length < 2) {
				console.error('Not found appropriate target element for replacePart.');
				return;
			}
	
			// get location.
			let SeQs = [];
			for(let idx=0; idx<(parents.length-1); idx++) {
				tarEle = parents[idx+1]; // target element
				tarEle.TarE = true;
				let SeQidx = 0;
				for(let n of parents[idx].childNodes) {
					if (n.SeQ < tarEle.SeQ) continue;
					if (n.SeQ > tarEle.SeQ) break;
					if (n.TarE) {n.TarE = null; break;}
					SeQidx++;
				}
				SeQs.push([tarEle.SeQ, SeQidx]);
			}
			if (_dev) console.log(`Replacing part. ${this._PATH} ${SeQs.join('-')}`);
	
			// find target vdom
			let newVDom = this.genVirtualDom(this.tarComp._MODEL);
			let tarVdom = newVDom;
			for(let s of SeQs) {
				if (!tarVdom) console.error('Should specificate parent selector for replacePart.');
	
				let SeQidx = 0, vTar = null;
				for(let v of tarVdom.children) {
					if (v.SeQ > s[0]) break;
					if (v.SeQ < s[0]) continue;
					if (s[1] === SeQidx) {vTar = v; break;};
					SeQidx++;
				}
				tarVdom = vTar;
			}
			this.oldVDom = newVDom;
			this.oldModel = util.cloneVal(this.tarComp._MODEL);
	
			// execute
			if (!tarVdom) {
				if (_debug) console.log(`[DEBUG] Replacing part. ${this._PATH} ${SeQs.join('-')} [delete]`);
				tarEle.remove(); return;
			}
	
			let dom = await genDom(tarVdom, this.tarComp);
			tarEle.replaceWith(dom.dom);
			runAfterRender(dom.aftTars);
			if (_debug) console.log(`[DEBUG] Replacing part. ${this._PATH} ${SeQs.join('-')} [replace]`);
			if (_debug) console.log(dom.dom);
		}
	}
	/*******************************
	 * Element Extend
	 *******************************/
	(()=>{
		const Ex = Element.prototype;
		// querySelector
		Ex.s = function(selector, indexOrNone){
			if (!selector) return this;
			if (selector.startsWith('d7')) selector = `[d7id="${selector.substring(2)}"]`;
			if (typeof indexOrNone === 'undefined') {
				return this.querySelector(selector);
			}
		
			let elements = this.querySelectorAll(selector);
			if (elements.length < (indexOrNone+1)) {
				console.warn(`[WARN] Target element not exists. ${selector} [${indexOrNone}]`);
			}
			return elements[indexOrNone];
		}
		// querySelectorAll
		Ex.sAll = function(selector){
			if (!selector) return [this];
			if (selector.startsWith('d7')) selector = `[d7id="${selector.substring(2)}"]`;
			return this.querySelectorAll(selector);
		}
		// show | hide
		Ex.show = function(){
			if (this.style.display_bak) {
				this.style.display = this.style.display_bak;
			} else {
				this.style.display = null;
			}
			return;
		}
		Ex.hide = function(){
			if (this.style.display && this.style.display !== 'none') {
				this.style.display_bak = this.style.display;
			}
			this.style.display = "none";
			return;
		}
		// value
		Ex.getVal = function(){
			if (typeof this.value !== 'undefined') return this.value;
			return this.textContent;
		}
		Ex.setVal = function(val){
			if (typeof this.value !== 'undefined') {this.value = val; return this;}
			this.textContent = val;
			return this;
		}
		// attribute
		Ex.getAttr = function(prop){
			if (!this.hasAttribute(prop)) return null;
			return this.getAttribute(prop) || '';
		}
		Ex.setAttr = function(prop, val){
			this.setAttribute(prop, val);
			return this;
		}
		Ex.rmvAttr = function(prop){
			this.removeAttribute(prop);
			return this;
		}
		// style
		Ex.getStyle = function(propOrNone){
			if (propOrNone) return this.style[propOrNone];
		
			let css = {};
			(this.style.cssText || '').split(';').forEach((str)=>{
				let cssunit = str.split(':');
				if (cssunit.length < 2) return;
				css[cssunit[0].trim()] = cssunit[1].trim();
			})
			return css;
		};
		Ex.setStyle = function(propOrMap, val){
			if (typeof propOrMap === 'string') {
				this.style[propOrMap] = val;
				return this;
			}
			for(let key in propOrMap) {
				this.style[key] = propOrMap[key];
			}
			return this;
		};
		Ex.rmvStyle = function(...prop){
			for(let p of prop) {
				//delete this.style[prop];
				this.style[p] = null;
			}
			return this;
		};
		// class
		Ex.getClass = function(clazzOrNone){
			if (clazzOrNone) {
				if (this.classList.contains(clazzOrNone)) return clazzOrNone;
				return null;
			}
			let clazz = [];
			for(let claz of this.classList) {
				clazz.push(claz);
			}
			return clazz;
		}
		Ex.setClass = function(...clazz){
			for(let claz of clazz) {
				this.classList.add(claz);
			}
			return this;
		}
		Ex.rmvClass = function(...clazz){
			for(let claz of clazz) {
				this.classList.remove(claz);
			}
			return this;
		}
	})();
	/*******************************
	 * UTIL
	 *******************************/
	const _HTMLENCODE = {
		"<": "&lt;", 
		">": "&gt;", 
		"&": "&amp;", 
		'"': "&quot;", 
		"'": "&#39;",
		" ": "&nbsp;",
		"　": "&emsp;",
	};
	const _HTMLDECODE = {
		"&lt;":   "<", 
		"&gt;":   ">", 
		"&amp;":  "&", 
		"&quot;": '"', 
		"&#39;":  "'",
		"&nbsp;": " ",
		"&emsp;": "　",
	};
	const util = {
		inProgress: (show)=>{
			if (!document.body) return;
	
			if (!document.querySelector('[id="_d7_progress"]')) {
				let tmpDiv = document.createElement('div');
				tmpDiv.innerHTML = "<div id='_d7_progress' style='display:none;' class='d7_progress_overlay'><div class='d7_progress_form'><span class='d7_progress'>in progress</span></div></div>";
				document.body.appendChild(tmpDiv.firstChild);
			}

			let progress = document.querySelector('[id="_d7_progress"]');
			if (show === false) {
				progress.style.display = 'none';
			} else {
				progress.style.display = 'block';
			}
		},
		parseUrl: (nameOrUrl)=>{
			if (!nameOrUrl) return {};

			let strUrl;
			if (nameOrUrl.startsWith('/') || nameOrUrl.startsWith('http'))
				 strUrl = nameOrUrl;
			else strUrl = '/' + nameOrUrl; // by name
	
			let query = {};
			strUrl = strUrl.startsWith("http") ? strUrl : (window.location.origin + strUrl);
			const url = new URL(strUrl);
			(new URLSearchParams(url.search)).forEach((value, key)=>{
				query[key] = value;
			});
			
			if (nameOrUrl.startsWith('/') || nameOrUrl.startsWith('http')) {
				return {path: url.pathname, query: query, origin: url.origin, name: null};
			}
			
			const routeInfo = _ROUTE_MAP[url.pathname.substring(1).toUpperCase()];
			if (!routeInfo) console.error(`Setting.route not defined. ${nameOrUrl}`);
			return {path: routeInfo.path, query: query, origin: url.origin, name: routeInfo.name};
		},
		stringifyUrl: (url, query)=>{
			if (!query) return url;
			if (!Object.keys(query).length) return url;
	
			return url + "?" + Object.keys(query).map((key)=>{
				return key + "=" + encodeURIComponent(query[key]);
			}).join("&");
		},
		encodeHtml: (strHtml)=>{
			strHtml = strHtml || '';
			strHtml = strHtml.replace(/./g, (c)=>{
				return _HTMLENCODE[c] || c; 
			});
			return strHtml
		},
		decodeHtml: (strHtml)=>{
			strHtml = strHtml || '';
			strHtml = strHtml.replace(/&(lt|gt|amp|quot|#39|nbsp|emsp);/g, (c)=>{
				return _HTMLDECODE[c]; 
			});
			return strHtml
		},
		escapeReg: (str)=>{
			return str
				.replace(/\(/g, "\\(")
				.replace(/\)/g, "\\)")
				.replace(/\[/g, "\\[")
				.replace(/\]/g, "\\]")
				.replace(/\{/g, "\\{")
				.replace(/\}/g, "\\}")
				.replace(/\^/g, "\\^")
				.replace(/\$/g, "\\$")
				.replace(/\\/g, "\\")
				.replace(/\-/g, "\\-")
			;
		},
		str2hex: (str) => {
			if (!str) return "";

			let hex = "";
			str = encodeURIComponent(str);
			for(let idx=0; idx<str.length; idx++) {
				hex += str.charCodeAt(idx).toString(16);
			}
			return hex;
		},
		hex2str: (hex) => {
			let str = "";
			for(let idx=0; idx<hex.length; idx+=2) {
				str += String.fromCharCode(parseInt(hex.substring(idx, idx+2), 16));
			}
			return decodeURIComponent(str);
		},
		decode: (key, ...keyVal)=>{
			for(let idx=0; idx<keyVal.length; idx=idx+2) {
				if (keyVal[idx] === key) return keyVal[idx+1];
			}
			if(keyVal.length % 2 == 1) return keyVal[keyVal.length-1];
			return null;
		},
		cloneVal: (v) => {
			if (v instanceof Object) {
				if (v instanceof Array) {
					let a = [];
					for(let o of v) a.push(util.cloneVal(o));
					return a;
				}
				let m = {};
				for(let k in v) m[k] = util.cloneVal(v[k]);
				return m;
			}
			return v;
		},
		hasDiff: (v1, v2) => {
			if (v1 instanceof Object) {
				if (!(v2 instanceof Object)) return true;
		
				if (v1 instanceof Array) {
					if (!(v2 instanceof Array)) return true;
			
					if (v1.length !== v2.length) return true;
					for(let idx in v1) if (util.hasDiff(v1[idx], v2[idx])) return true;
					return false;
				}
			
				if (v2 instanceof Array) return true;
			
				if (Object.keys(v1).length !== Object.keys(v2).length) return true;
				for(let k in v1) if (util.hasDiff(v1[k], v2[k])) return true;
				return false;
			}
			return v1 !== v2;
		},
		or: (srcData, tarData)=>{
			if (typeof srcData === 'undefined') return tarData;
			// trim?
			return srcData;
		},
	}
	/*******************************
	 * HTTP API
	 *******************************/
	let requestIntercept = (method, url, query, header) => {return {url, query, header};};
	let responseIntercept = (method, url, query, responseData, request) => {return responseData;};
	const httpClient = {
		setRequestInterceptor: (interceptor)=>{
			requestIntercept = interceptor;
		},
		setResponseInterceptor: (interceptor)=>{
			responseIntercept = interceptor;
		},
		loadHtml: (htmlUrl)=>{
			return new Promise((resolve)=>{
				let request = new XMLHttpRequest();
				request.open('GET', htmlUrl);
				request.setRequestHeader('d7req', 'html');
		
				request.onload = ()=>{
					resolve({
						_STATUS: request.status  === 200 ? 'OK' : 'WARN',
						_MSGCODE: request.status === 200 ? '' : `HC${request.status}`,
						_MESSAGE: request.status === 200 ? '' : 'Looks like a network error has occurred, Please wait a while and try again.',
						data: request.responseText,
					});
				}

				request.onerror = ()=>{
					console.error(`LoadHtml error. GET ${url} [onerror] triggered.`);

					resolve({
						_STATUS:  'ERROR',
						_MSGCODE: `HC${request.status}`,
						_MESSAGE: '[onerror] triggered.',
					});
				}

				request.send();
			});
		},
		loadHtmlSync: (htmlUrl)=>{
			let request = new XMLHttpRequest();
			request.open('GET', htmlUrl, false);
			request.setRequestHeader('d7req', 'html');
			request.send();
	
			return {
				_STATUS: request.status  === 200 ? 'OK' : 'WARN',
				_MSGCODE: request.status === 200 ? '' : `HC${request.status}`,
				_MESSAGE: request.status === 200 ? '' : 'Looks like a network error has occurred, Please wait a while and try again.',
				data: request.responseText,
			};
		},
		get: (pUrl, pQuery, pHeader)=>{
			let {url, query, header} = requestIntercept('GET', pUrl, pQuery, pHeader);

			return new Promise((resolve)=>{
				const request = httpRequest('GET', url, query, resolve);

				if (header) {
					for(let key in header) request.setRequestHeader(key, header[key]);
				}
				request.send();
			});
		},
		post: (pUrl, pData, pHeader)=>{
			let {url, query, header} = requestIntercept('POST', pUrl, {}, pHeader);

			return new Promise((resolve)=>{
				const request = httpRequest('POST', url, query, resolve);

				if (!header) header = {};
				if (!header["Content-Type"]) header["Content-Type"] = "application/json; charset=utf-8";
				for(let key in header) request.setRequestHeader(key, header[key]);

				if (header["Content-Type"].toLowerCase().includes('json')) {
					return request.send(JSON.stringify(pData));
				}

				request.send(pData);
			});
		},
		put: (pUrl, pData, pHeader)=>{
			let {url, query, header} = requestIntercept('PUT', pUrl, {}, pHeader);

			return new Promise((resolve)=>{
				const request = httpRequest('PUT', url, query, resolve);

				if (!header) header = {};
				if (!header["Content-Type"]) header["Content-Type"] = "application/json; charset=utf-8";
				for(let key in header) request.setRequestHeader(key, header[key]);

				if (header["Content-Type"].toLowerCase().includes('json')) {
					return request.send(JSON.stringify(pData));
				}

				request.send(pData);
			});
		},
		delete: (pUrl, pQuery, pHeader)=>{
			let {url, query, header} = requestIntercept('DELETE', pUrl, pQuery, pHeader);

			return new Promise((resolve)=>{
				const request = httpRequest('DELETE', url, query, resolve);

				if (header) {
					for(let key in header) request.setRequestHeader(key, header[key]);
				}
				request.send();
			});
		}
	}
	const httpRequest = (method, url, query, resolve)=>{
		const request = new XMLHttpRequest();
		request.onload = ()=>{
			if (_SHOW_PROGRESS) util.inProgress(false);
			let contentType = request.getResponseHeader("Content-Type") || '';
			contentType = contentType.toLowerCase();

			if (!contentType.includes("json")) { // xml .etc
				let respData = {
					_STATUS: request.status  === 200 ? 'OK' : 'WARN',
					_MSGCODE: request.status === 200 ? '' : `HC${request.status}`,
					_MESSAGE: request.status === 200 ? '' : 'Unknown.',
					data: request.responseText,
				};
				respData = responseIntercept(method, url, query, respData, request);
				return resolve(respData);
			}

			try{
				let respData = JSON.parse(request.responseText);
				if (respData instanceof Array) {
					respData = {data: respData};
				}
				if (typeof respData._STATUS === 'undefined') {
					respData._STATUS  = request.status === 200 ? 'OK' : 'WARN';
					respData._MSGCODE = request.status === 200 ? '' : `HC${request.status}`;
					respData._MESSAGE = request.status === 200 ? '' : (respData.message || respData.error);
				}
				respData = responseIntercept(method, url, query, respData, request);
				return resolve(respData);
			}catch (e) {
				console.error(e);
				let respData = {
					_STATUS: 'ERROR',
					_MSGCODE: `HC${request.status}`,
					_MESSAGE: e.message,
					data: request.responseText,
				};
				respData = responseIntercept(method, url, query, respData, request);
				return resolve(respData);
			}
		}

		request.onerror = (e)=>{
			if (_SHOW_PROGRESS) util.inProgress(false);
			console.error(`Api error. ${method} ${url} [request.onerror] triggered.`);

			let respData = {
				_STATUS: 'ERROR',
				_MSGCODE: `HC${request.status}`,
				_MESSAGE: 'Looks like a network error has occurred, Please wait a while and try again.',
			};
			respData = responseIntercept(method, url, query, respData, request);
			return resolve(respData);
		}

		if (_SHOW_PROGRESS) util.inProgress();

		let uri = util.stringifyUrl(url, query);
		if (!uri.startsWith('http')) uri = _BASE_API_URI + uri;
		request.open(method, uri);
		
		return request;
	}
	/*******************************
	 * analyze html
	 *******************************/
	/* example)
	 * 	 <div d7="if|for" class="red {% %}" onclick="clkFunciont(e, idx)">
	 *   if (condition) continue | break
	 *   if (condition) xxx; yyy; zzz; => if (condition) {xxx; yyy; zzz;
	 *   for(condition) xxx; yyy; zzz; => for(condition) {xxx; yyy; zzz;
	*/
	const _LG_REG = new RegExp(`(${util.escapeReg(_LOGIC.start)})([\\s\\S]*?)(${util.escapeReg(_LOGIC.close)})`, "gm");
	const _LG_REG_START = new RegExp(`(<!\\-\\-\\s*)?(${util.escapeReg(_LOGIC.start)})`, 'gm');
	const _LG_REG_CLOSE = new RegExp(`(${util.escapeReg(_LOGIC.close)})(\\s*\\-\\->)?`, 'gm');
	const _PR_REG = new RegExp(`(["']?)(${util.escapeReg(_PRINT.start)})(.*?)(${util.escapeReg(_PRINT.close)})(["']?)`, "gm");
	const _PR_REG_EN = new RegExp(`(${util.escapeReg(_PRINT_EN.start)})(.*?)(${util.escapeReg(_PRINT_EN.close)})`, "gm");
	const _STYLE_REG = new RegExp('(.+?)(\\{.*?\\})', "gm");
	const _EVENT_REG = new RegExp('\\son(\\w+)\\s*=', 'igm');
	
	const analyze = (htmlText) => {
		let firstTag = htmlText.match(/^.*?<(.+?)>/)
		if (firstTag) firstTag = firstTag[1];
		else firstTag = 'div';
		if (firstTag.toLowerCase().includes('html')) {
			let d7body = htmlText.match(/<d7body.*?>([\s\S]+?)<\/d7body>/i);
			if (!d7body) console.error("<d7body> bust be defined in template when it's a html page.");
			htmlText = d7body[1];
			firstTag = 'div';
		} else {
			let idx = firstTag.indexOf(' ');
			if (idx > 0) firstTag = firstTag.substring(0, idx);
		}
	
		// {[% xxx %]} normalize to <!-- {[% xxx %]} -->
		htmlText = htmlText.replace(_LG_REG_START, (m, cmtStart, start)=>{return "<!--" + start;})
		htmlText = htmlText.replace(_LG_REG_CLOSE, (m, close, cmtClose)=>{return close + "-->";})
		htmlText = htmlText.replace(_PR_REG, (m, qStart, start, expr, close, qClose)=>{
			if (qStart === qClose) return _PRINT_EN.start + util.str2hex(expr) + _PRINT_EN.close;
			return qStart + _PRINT_EN.start + util.str2hex(expr) + _PRINT_EN.close + qClose;
		})
		htmlText = htmlText.replace(_EVENT_REG, (m, eventName)=>{return ' _d7e' + eventName.toLowerCase() + '=';})

		let divTemp = makeContainer(firstTag);
		divTemp.innerHTML = htmlText;
		let layoutExpr = "";
		let scriptCode = "";
		let styleCode = "";
	
		// layout
		divTemp.querySelectorAll('[d7layout]').forEach((ele)=>{
			layoutExpr = ele.getAttribute('d7layout') || ele.innerHTML;
			ele.remove();
		})
		// dummy
		divTemp.querySelectorAll('[d7dummy]').forEach((ele)=>{
			ele.remove();
		})
		// comp内クリア
		divTemp.querySelectorAll('[d7comp]').forEach((ele)=>{
			ele.innerHTML = '';
		})
		// style
		divTemp.querySelectorAll('style').forEach((ele)=>{
			styleCode += ele.innerHTML + "\n\n\n"
			ele.remove();
		})
		// javascript
		divTemp.querySelectorAll('script').forEach((ele)=>{
			scriptCode += ele.innerHTML + "\n;\n;\n;"
			ele.remove();
		})
		divTemp.querySelectorAll('d7script').forEach((ele)=>{
			scriptCode += ele.innerHTML + "\n;\n;\n;"
			ele.remove();
		})
	
		let rawDom = cloneDom(divTemp);
		rawDom.prepend(document.createElement('style'));
	
		return {
			layoutExpr: layoutExpr,
			styleCode: styleCode,
			scriptCode: scriptCode,
			rawDom: rawDom,
		};
	}
	const compileHtml = (expr) => {
		expr = expr.replace(_LG_REG_START, (m, cmtStart, start)=>{return start;})
		expr = expr.replace(_LG_REG_CLOSE, (m, close, cmtClose)=>{return close;})
		expr = expr.replace(_PR_REG_EN, (m, start, expr, close)=>{return _LOGIC.start + "=" + util.hex2str(expr) + _LOGIC.close;})
	
		expr = expr.replace(/[\r\n]/g, "").replace(/'/g, "\\'");
		let tpl = expr.replace(_LG_REG, (m, start, expr, close)=>{
			expr = util.decodeHtml(expr.replace(/\\'/g, "'"));
			if (expr.startsWith("=")) {
				return `'\n+ d7util.or(${expr.substring(1)}, '')\n+'`;
			}
			return `';\n${expr.trim()}\nout+='`;
		});
	
		tpl = `let out='';\nout+='${tpl}';\nreturn out;\n`;
	
		try {
			return new Function("_m", tpl);
		} catch(e) {
			console.error(e.message + '\n' + tpl);
		}
	}
	const compileStyle = (expr) => {
		if (!expr) return;
	
		expr = expr.replace(/[\r\n]/g, "").replace(/'/g, "\\'");
		let tpl = expr.replace(_STYLE_REG, (m, selector, detail)=>{
			selector = selector.trim();
			return `out += scope + ' ${selector}:not(' + exclusion + ' *) ${detail}\\n';\n`;
		});
	
		tpl = `let out='';\n${tpl}\n return out;`;
	
		try {
			return new Function("scope", "exclusion", tpl);
		} catch(e) {
			console.error(e.message + '\n' + tpl);
		}
	}
	const cloneDom = (tarDom) => {
		let SeQ = 1;
		let newDom = tarDom.cloneNode();
		for(let node of tarDom.childNodes) {
			if (node.nodeName.startsWith("#")) {
				let text = node.textContent.trim();
				if (!text) continue;
			}
			let newNode = cloneDom(node);
			newNode.SeQ = SeQ++;
			newDom.appendChild(newNode);
		}
		return newDom;
	}
	const _CONTAINER_TYPE = {
		"CAPTION": "table", 
		"THEAD": "table",
		"TFOOT": "table",
		"TBODY": "table",
		"TR": "table",
		"COLGROUP": "table tr",
		"COL: ": "table tr",
		"TH": "table tr",
		"TD": "table tr",
		"LI": "ul",
		"DT": "dl",
		"DD": "dl",
		"OPTION": "select",
		"OPTGROUP": "select",
		"AREA": "map",
		"LEGEND": "fieldset",
	};
	const makeContainer = (tagName) => {
		let tmpDiv = document.createElement("div");
		let strContainer = _CONTAINER_TYPE[tagName.toUpperCase()];
		if (!strContainer) return tmpDiv;
	
		let tags = strContainer.split(" ");
		for(let tag of tags) {
			let tempTag = document.createElement(tag);
			tmpDiv.appendChild(tempTag);
			tmpDiv = tempTag;
		}
		return tmpDiv;
	}
	/*******************************
	 * virtual dom
	 *******************************/
	const genFnVirtualDom = (tpltDom, tpltPath) => {
		let fnStr = "const genVirtualDom = function(_m) {\n";
		if (_dev) fnStr += `console.time('<VirtualDom>          ${tpltPath}');\n`;
	
		fnStr += "let ele, p = {children:[]}, ps=[p];\n";
		fnStr += "const cp = {\n";
		fnStr += "  stepIn:() => {ps.push(ele); p = ele;},\n";
		fnStr += "  stepOut:() => {ps.pop(); p = ps[ps.length-1];},\n";
		fnStr += "  add:(n) => {p.children.push(n); ele = n; return cp;},\n";
		fnStr += "  attr:(key, val) => {if(!key) return cp; ele.attrs[key] = val; if(key==='d7id') ele.D7id = key; return cp;},\n";
		fnStr += "  event:(key, val) => {ele.events[key] = val; return cp;},\n";
		fnStr += "  d7part:() => {ele.d7part = true; ele.attrs['d7part'] = ''; return cp;},\n";
		fnStr += "  aft:(func) => {ele.aft = func; return cp;},\n";
		fnStr += "}\n\n";
	//	fnStr += "try {\n";
	
		fnStr += genFnVirtualElement(tpltDom);
		if (_dev) fnStr += `console.timeEnd('<VirtualDom>          ${tpltPath}');\n`;
	
	//	fnStr += "} catch(ex) { console.error('CompileError: ', ex.message); }\n";
		fnStr += "return ps[0].children[0]; }\n";
		//return new Function("_m", fnStr);
		return fnStr;
	}
	const genFnVirtualElement = (tpltDom) => {
		let fnStr = "";
		let node = tpltDom;
	
		if (node.nodeName.startsWith('#')) {
			if (node.nodeName == '#text') {
				fnStr += `cp.add({type:'TxT', value: ${nmlzPrt(node.textContent)}, SeQ: ${node.SeQ}});\n`;
			} else if (node.nodeName == '#comment') {
				if (node.textContent.startsWith(_LOGIC.start) && node.textContent.endsWith(_LOGIC.close)) {
					fnStr += node.textContent.substring(_LOGIC.start.length, node.textContent.length-_LOGIC.close.length) + '\n';
				} else {
					fnStr += `cp.add({type:'CmT', value: ${nmlzPrt(node.textContent)}, SeQ: ${node.SeQ}});\n`;
				}
			} else {
				console.error('Not supported node type. ' + node.nodeName);
			}
			return fnStr;
		}
	
		let ctl = nmlzCtl(node.getAttribute('d7'));
		if (ctl) {
			fnStr += ctl[0] + "\n";
		}
	
		fnStr += `cp.add({type:'ElE', name:'${node.nodeName}', SeQ: ${node.SeQ}, attrs: {}, events: {}, children:[]})`;
		for(let attr of node.attributes) {
			if (attr.name === 'd7') continue;

			if (attr.name === 'd7part') {
				fnStr += `.d7part()`;
				continue;
			}

			if (attr.name === 'didrender') {
				fnStr += `.aft(function fn(){${attr.value};})`;
				continue;
			}
	
			if (!attr.name.startsWith('_d7e')) {
				fnStr += `.attr(${nmlzPrt(attr.name)}, ${nmlzPrt(attr.value)})`;
				continue;
			}
	
			let eventName = attr.name.substring(4);
			//let eventInfo = nmlzEvent(attr.value);
			//fnStr += `.event('${eventName}', {name:'${eventName}', func:${eventInfo.funcStr}, tars:${nmlzPrt(eventInfo.tars)}})`;
			let eventInfo = nmlzEventRes(attr.value);
			fnStr += `.event('${eventName}', {name:'${eventName}', func:${eventInfo.funcStr}, params:[${eventInfo.params}], tars:${nmlzPrt(eventInfo.tars)}})`;
		}
		fnStr += ";\n";
	
		if (tpltDom.childNodes.length > 0) {
			fnStr += "cp.stepIn();\n";
			for(let node of tpltDom.childNodes) {
				fnStr += genFnVirtualElement(node);
			}
			fnStr += "cp.stepOut();\n";
		}
	
		if (ctl) {
			fnStr += ctl[1] ? "}\n" : "";
		}
	
		return fnStr;
	}
	const nmlzCtl = (d7expr) => {
		if (!d7expr) return null;
	
		let pos = bracketsPos(d7expr, 0, '(', ')');
		if (!pos) console.error("Not applicable d7Logic: " + d7expr);
	
		let ctl = d7expr.substring(0,pos[0]).trim();
		let control = d7expr.substring(0,pos[1]+1).trim();
		let logic = d7expr.substring(pos[1]+1).trim();
	
		if (logic.length>1 && !logic.endsWith(';')) {
			logic = logic + ';';
			d7expr = d7expr + ';';
		}
	
		if (ctl === 'if') {
			if (logic.match(/^continue|break/)) return [d7expr, false];
			return [logic.startsWith('{') ? d7expr : (control + '{' + logic), true];
		}
		if (ctl === 'for') {
			return [logic.startsWith('{') ? d7expr : (control + '{' + logic), true];
		}
	
		if (!ctl) console.error('Normalyze: [d7] must starts with [if or for]. ' + d7expr);
	}
	const nmlzPrt = (prtExpr) => {
		prtExpr = prtExpr.replaceAll('\'', '\\\'');
		prtExpr = prtExpr.replaceAll('\r', '\\r');
		prtExpr = prtExpr.replaceAll('\n', '\\n');
		prtExpr = prtExpr.replace(_PR_REG_EN, (m, start, expr, close)=>{return "'+ d7util.or(" + util.hex2str(expr) + ", '') +'";});
		return "'" + prtExpr + "'";
	}
	const nmlzEvent = (eventAttr) => {
		//_d7eclick="let var=obj.x(a,b,c); return var;|select1|selector2..."
	
		let funcStr = null;
		let selectors = null;
		let idx = eventAttr.indexOf('|');
		if (idx < 0) {
			funcStr = eventAttr.replace(/(\W)this(\W)/g, (m, c1, pName, c2)=>{return c1 + "'_d7this'" + c2;});
		} else {
			funcStr += eventAttr.substring(0, idx).replace(/(\W)this(\W)/g, (m, c1, pName, c2)=>{return c1 + "'_d7this'" + c2;});
			selectors = eventAttr.substring(idx+1);
		}
		if (funcStr.endsWith(';')) 	funcStr = '(e, _d7this) => {' + funcStr + '}';
		else						funcStr = '(e, _d7this) => {return ' + funcStr + ';}';

		return {
			funcStr: funcStr,
			tars: selectors,
		};
	}
	const nmlzEventRes = (eventAttr) => { // restricted version(consider performance)
		//_d7eclick="obj.x(a,b,c)|select1|selector2..."
	
		let pos = bracketsPos(eventAttr, 0, '(', ')');
		if (!pos) console.error("Not valid event attribute: " + eventAttr);
		let paramStr = eventAttr.substring(pos[0]+1, pos[1]);
		paramStr = paramStr.replace(/(^|,)\s*(e)\s*(,|$)/g, (m, c1, pName, c2)=>{return c1 + "'_d7e'" + c2;});
		paramStr = paramStr.replace(/(^|,)\s*(this)\s*(,|$)/g, (m, c1, pName, c2)=>{return c1 + "'_d7this'" + c2;});
		return {
			funcStr: eventAttr.substring(0, pos[0]).trim(),
			params: paramStr,
			tars: eventAttr.substring(pos[1]+2)
		};
	}
	/*******************************
	 * operate dom
	 *******************************/
	const genDom = async (vDom, tarComp) => {
		let newComps = [];
		let newEvents = [];
		let aftTars = [];
		let dom = genElement(vDom, newComps, newEvents, aftTars);

		for(let meta of newComps) {
			let param = util.parseUrl(meta[1]);
			let comp = await Component.load(param.path);
			await comp.mount(param.query, meta[0]);
			meta[0].classList.add(`_${tarComp.id}ex`);

			comp.name = comp.name || param.name || comp.id;
			tarComp.children[comp.id] = comp;
			comp.parent = tarComp;
		}
		for(let meta of newEvents) {
			bindEvent(meta[0], meta[1], tarComp);
		}

		return {
			dom: dom,
			aftTars: aftTars,
		}
	}
	const genElement = (vDom, newComps, newEvents, aftTars) => {
		if (vDom.type === "TxT") {
			let txt = document.createTextNode(vDom.value);
			txt.SeQ = vDom.SeQ;
			return txt;
		}
	
		if (vDom.type === "CmT") {
			let cmt = document.createComment(vDom.value);
			cmt.SeQ = vDom.SeQ;
			return cmt;
		}
	
		let ele = document.createElement(vDom.name);
		for(let key in vDom.attrs) {
			let val = vDom.attrs[key];
	
			if (key === 'd7comp') {
				newComps.push([ele, val]);
				continue;
			}

			ele.setAttribute(key, val);
			if (key === 'd7id') ele.D7id = val;
		}
		for(let vNode of vDom.children) {
			ele.appendChild(genElement(vNode, newComps, newEvents, aftTars));
		}
		for(let name in vDom.events) {
			newEvents.push([ele, vDom.events[name]]);
		}
		if (vDom.aft) aftTars.push([vDom.aft, ele]);
	
		ele.SeQ = vDom.SeQ;
		return ele;
	}
	const bindEvent = (tarEle, eIf, tarComp) => {
		if (_debug) {console.log(`[DEBUG] Bind event[${eIf.name}].`); console.log(tarEle); }

		if (!tarEle.EiF) tarEle.EiF = {};
		const eventInfo = {name: eIf.name, func: eIf.func, params: eIf.params, tars: eIf.tars, tarComp: tarComp};

		let eventName = eventInfo.name;
		if (tarEle.EiF[eventName]) {
			tarEle.EiF[eventName] = eventInfo;
			return;
		}
	
		if (eventName === 'change' && !["INPUT", "SELECT", "TEXTAREA"].includes(tarEle.tagName)) {
			let adaptor = new MutationObserver((mutationList)=>{
				eventAdaptor({currentTarget: tarEle, type: eventName, mutation: mutationList});
			});
			adaptor.observe(tarEle, { attributes: true, characterData: true, childList: true, subtree: true });
			eventInfo.removeListener = () => {
				adaptor.disconnect();
			}
		} else {
			tarEle.addEventListener(eventName, eventAdaptor);
			eventInfo.removeListener = () => {
				tarEle.removeEventListener(eventName, eventAdaptor);
			}
		}
		tarEle.EiF[eventName] = eventInfo;
	};
	const _EVENT_Q = [];
	const eventAdaptor = (e) => {

		let tarEle = e.currentTarget;
		if (e !== 'next') {
			_EVENT_Q.push({curEle:e.currentTarget, e:e});
			if (_EVENT_Q.length > 1) return;
		} else {
			_EVENT_Q.shift();
			if (_EVENT_Q.length < 1) return;

			e = _EVENT_Q[0].e;
			tarEle = _EVENT_Q[0].curEle;
		}

		let eventInfo = tarEle.EiF[e.type];

		let params = [];
		for(let idx=0; idx < eventInfo.params.length; idx++) {
			if (eventInfo.params[idx] === '_d7e') { params.push(e); continue;}
			if (eventInfo.params[idx] === '_d7this') { params.push(tarEle); continue;}
			params.push(eventInfo.params[idx]);
		}

		let afterEvent = null;
		if (_debug) {
			console.log(`[DEBUG] Running event[${eventInfo.name}].`);
			console.log(tarEle);
			afterEvent = eventInfo.func.apply(tarEle, params);
		} else {
			try {
				afterEvent = eventInfo.func.apply(tarEle, params);
			} catch(ex) {
				console.error(`Event[${eventInfo.name}] ${ex.message}`);
				console.error(tarEle);
				_EVENT_Q.splice(0);
			}
		}

		if (afterEvent === false) return eventAdaptor('next');

		let selectors = [];
		for(let selector of eventInfo.tars.split("|")) {
			selector = selector.replace(/(^\s*('|"))|(('|")\s*$)/g, "");
			if (!selector) continue;
			selectors.push(selector);
		}

		if (typeof afterEvent === 'undefined') {
			eventInfo.tarComp.reflect(selectors, tarEle).then(()=>{
				eventAdaptor('next');
			});
			return;
		}
		if (afterEvent.constructor.name !== 'Promise') {
			eventInfo.tarComp.reflect(selectors, tarEle).then(()=>{
				if (typeof afterEvent === 'function') afterEvent();
				eventAdaptor('next');
			})
			return;
		}
		afterEvent.then((afterResolve)=>{
			if (afterResolve === false) return eventAdaptor('next');

			eventInfo.tarComp.reflect(selectors, tarEle).then(()=>{
				if (typeof afterResolve === 'function') afterResolve();
				eventAdaptor('next');
			})
			/*
			.catch((ex)=>{
				console.error(`Reflect Event[${eventInfo.name}] ${ex.message}`);
				console.error(tarEle);
				_EVENT_Q.splice(0);
			});
			*/
		});
	};
	const runAfterRender = (aftTars) => {
		if (aftTars.length < 1) return;

		if (_dev) console.time(`<AfterRender>         `);
		for(let aft of aftTars) {
			aft[0].call(aft[1]);
		}
		if (_dev) console.timeEnd(`<AfterRender>         `);
	}
	const reflectDom = async (vdomNew, vdomOld, parent, tarComp) => {
		let newComps = [];
		let newEvents = [];
		let aftTars = [];
		reflectElement(vdomNew, vdomOld, parent, newComps, newEvents, aftTars);
		for(let meta of newComps) {
			let param = util.parseUrl(meta[0]);
			let comp = await Component.load(param.path);
			await comp.mount(param.query, meta[1]);
			meta[1].classList.add(`_${tarComp.id}ex`);

			comp.name = comp.name || param.name || comp.id;
			tarComp.children[comp.id] = comp;
			comp.parent = tarComp;
		}
		for(let meta of newEvents) {
			bindEvent(meta[0], meta[1], tarComp);
		}
		
		return aftTars;
	}
	const reflectElement = (vdomNew, vdomOld, parent, newComps, newEvents, aftTars) => {

		let curNew = 0, curOld = 0, curEle = 0;
		let counter = vdomNew.children.length + vdomOld.children.length;
		for(let idx=0; idx<counter; idx++) {
			if ((curNew + curOld) >= counter) break;
	
			let vNew = vdomNew.children[curNew] || {SeQ:999999};
			let vOld = vdomOld.children[curOld] || {SeQ:999999};
			let cEle = parent.childNodes[curEle];
			if (cEle && cEle.SeQ !== vOld.SeQ) cEle = null; // No need to search The SeQ for rare case! 
	
			/*** replace ***/
			if (vNew.SeQ === vOld.SeQ) {
				// option(improve performance, otherwise spread all left rows to refresh)
				if (vNew.D7id && vNew.D7id !== vOld.D7id) {
					if (cEle.D7id !== vOld.D7id) cEle = null;
					if (!cEle) {
						return 11;
					}
	
					let addEle = true;
					for(let idx2=curOld+1; idx2<vdomOld.children.length; idx2++) {
						if (vNew.D7id === vdomOld.children[idx2].D7id) {
							addEle = false; break;
						}
					}
					if (addEle) {
						let newEle = genElement(vNew, newComps, newEvents, aftTars);
						cEle.before(newEle);
						if (_debug) {console.log(`[DEBUG] Reflet add Element. d7id[${vNew.D7id}]`); console.log(newEle);}
						curNew++; curEle++;
					} else {
						cEle.remove();
						if (_debug) {console.log(`[DEBUG] Reflet remove Child. d7id[${vOld.D7id}]`); console.log(parent);}
						curOld++;
					}
					continue;
				}
	
				curNew++; curOld++, curEle++;
	
				if (vNew.type !== "ElE") {
					if (vNew.value !== vOld.value) {
						if (!cEle) {
							return 12;
						}

						cEle.textContent = vNew.value;
						if (_debug) {console.log(`[DEBUG] Reflet set textContent. ${vNew.value}`); console.log(cEle);}
						if (parent.tagName === 'TEXTAREA') {
							parent.value = vNew.value;
							if (_debug) {console.log(`[DEBUG] Reflet set TEXTAREA.value. [${vNew.value}]`); console.log(parent);}
						}
					}
					continue;
				}

				// replace whole part when children is altered.
				if (vNew.d7part) {
					 if (util.hasDiff(vNew, vOld)) {
						if (!cEle) {
							return 13;
						}

						let newEle = genElement(vNew, newComps, newEvents, aftTars);
						cEle.replaceWith(newEle);
						if (_debug) {console.log(`[DEBUG] Reflet reconstruct Part.`); console.log(newEle);}
					}
					continue;
				}

				let bindForce = null;
				if (cEle && cEle.EiF) {
					let eiF = cEle.EiF['change'];
					if (eiF) {
						if (!util.hasDiff(vNew, vOld)) continue;
						
						eiF.removeListener();
						delete cEle.EiF['change'];
						bindForce = 'change';
					}
				}

				const len1 = newComps.length, len2 = newEvents.length, len3 = aftTars.length;
				let altType = reflectElement(vNew, vOld, cEle || {childNodes:[]}, newComps, newEvents, aftTars);
				if (altType > 10) {
					if (_debug) {console.log(`[DEBUG] Reflet will reconstruct Child.[${altType}]`);}

					if (!cEle) {
						return 14;
					}

					newComps.slice(len1);
					newEvents.slice(len2);
					aftTars.slice(len3);

					cEle.innerHTML = '';
					for(let vNewChild of vNew.children) {
						cEle.appendChild(genElement(vNewChild, newComps, newEvents, aftTars));
					}
					if (_debug) {console.log(`[DEBUG] Reflet reconstruct Child.`); console.log(cEle);}
				}

				let attrCnt = 0;
				let attrAlt = false;
				for(let key in vNew.attrs) {
					attrCnt++;
					let newVal = vNew.attrs[key];
					let oldVal = vOld.attrs[key];
					//delete vOld.attrs[key];
					if (newVal === oldVal) continue;

					attrAlt = true;
					if (!cEle) {
						return 15;
					}

					cEle.setAttribute(key, newVal);
					if (_debug) {console.log(`[DEBUG] Reflet set attribute. ${key}:${newVal}`); console.log(cEle);}
					if (key === 'value') {
						if (cEle.value !== 'undefined') {
							cEle.value = newVal;
							if (_debug) {console.log(`[DEBUG] Reflet set value. ${newVal}`); console.log(cEle);}
						}
					}

					if (key === 'd7comp') {
						newComps.push([newVal, cEle]);
					}
				}

				let oldKeys = Object.keys(vOld.attrs);
				if (attrAlt || oldKeys.length !== attrCnt) {
					for(let key of oldKeys) {
						if (key in vNew.attrs) continue;
	
						if (!cEle) {
							return 16;
						}
	
						cEle.removeAttribute(key);
						if (_debug) {console.log(`[DEBUG] Reflet remove attribute. ${key}`); console.log(cEle);}
					}
				}

				for(let name in vNew.events) {
					let eventNew = vNew.events[name];
					let eventOld = vOld.events[name];
					if (!util.hasDiff(eventNew, eventOld) && name !== bindForce) continue;

					if (!cEle) {
						return 17;
					}
	
					let eIf = cEle.EiF[name];
					if (eIf) {
						eIf.params = eventNew.params;
						eIf.tars = eventNew.tars;
					} else {
						newEvents.push([cEle, eventNew]);
					}
					if (_debug) {console.log(`[DEBUG] Reflet rebind Event[${name}]`); console.log(cEle);}
				}

				continue;
			}

			/*** add ***/
			if (vNew.SeQ < vOld.SeQ) {
				curNew++, curEle++;
				if ((!cEle && vOld.SeQ !== 999999) || !parent.nodeName) {
					return 18;
				}
	
				let newEle = genElement(vNew, newComps, newEvents, aftTars);
				if (cEle) {
					cEle.before(newEle);
				} else {
					parent.appendChild(newEle);
				}
				if (_debug) {console.log(`[DEBUG] Reflet add Element.`); console.log(newEle);}
				continue;
			}


			/*** remove ***/
			//if (vNew.SeQ > vOld.SeQ)

			curOld++;
			if (!cEle) {
				return 19;
			}

			cEle.remove();
			if (_debug) {console.log(`[DEBUG] RefletScan remove Child. ${vOld.SeQ}[${curOld}]`); console.log(parent);}
		}

		return 0;
	}
	const bracketsPos = (str, startPos, cStart, cEnd) => {
		if (!str) return null;
	
		let deeps = 0;
		let posStart = -1;
		const bStartLen = cStart.length;
		const bEndLen = cEnd.length;
		for(let idx = startPos; idx < str.length; idx++) {
			if (str.substring(idx, idx+bStartLen) === cStart) {
				if (deeps === 0) posStart = idx;
				deeps++;
			}
			if (str.substring(idx, idx+bEndLen) === cEnd) {
				if (deeps === 1) return [posStart, idx];
				deeps--;
			}
		}
	
		return null;
	}
	
	App.prototype.api = httpClient;
	
	return {
		d7app: new App(),
		d7util: util,
	};
})();
/*******************************
 * PUBLISH
 *******************************/
const app = d7app;
const d7Util = d7util;
(function (g) {
	switch (document.readyState) {
		case "loading":
			document.addEventListener('DOMContentLoaded', ()=>{ d7app.start(); });
			//window.addEventListener("load", ()=>{ d7App.start(); }); // after image css loaded.
			break;
		default : // [interactive, complete]
			d7app.start();
			break;
	}
})(this);