/* Parser.java

	Purpose:
		
	Description:
		
	History:
		Tue May 31 14:26:18     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.metainfo;

import java.io.File;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;

import org.zkoss.idom.Attribute;
import org.zkoss.idom.CData;
import org.zkoss.idom.Comment;
import org.zkoss.idom.Document;
import org.zkoss.idom.Element;
import org.zkoss.idom.Item;
import org.zkoss.idom.Namespace;
import org.zkoss.idom.ProcessingInstruction;
import org.zkoss.idom.Text;
import org.zkoss.lang.ClassResolver;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.PotentialDeadLockException;
import org.zkoss.util.CollectionsX;
import org.zkoss.util.resource.Locator;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.xel.taglib.Taglib;
import org.zkoss.xel.util.Evaluators;
import org.zkoss.xel.util.MethodFunction;
import org.zkoss.xml.Locators;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.WebApps;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Native;
import org.zkoss.zk.ui.impl.RequestInfoImpl;
import org.zkoss.zk.ui.impl.ZScriptInitiator;
import org.zkoss.zk.ui.metainfo.impl.AnnotationHelper;
import org.zkoss.zk.ui.metainfo.impl.ComponentDefinitionImpl;
import org.zkoss.zk.ui.metainfo.impl.ShadowDefinitionImpl;
import org.zkoss.zk.ui.sys.RequestInfo;
import org.zkoss.zk.ui.sys.UiFactory;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.ConditionImpl;
import org.zkoss.zk.ui.util.Configuration;

/**
 * Used to parse the ZUL file
 * @author tomyeh
 */
public class Parser {
	private static final Logger log = LoggerFactory.getLogger(Parser.class);

	private final WebApp _wapp;
	private final Locator _locator;
	private final List<NamespaceParser> _nsParsers;

	/** Constructor.
	 *
	 * @param locator the locator used to locate taglib and other resources.
	 * If null, wapp is assumed ({@link WebApp} is also assumed).
	 */
	public Parser(WebApp wapp, Locator locator) {
		if (wapp == null)
			throw new IllegalArgumentException("null");
		_wapp = wapp;
		_nsParsers = wapp.getConfiguration().getNamespaceParsers();

		// Higher is the first
		Collections.sort(_nsParsers, new Comparator<NamespaceParser>() {
			public int compare(NamespaceParser o1, NamespaceParser o2) {
				int op1 = o1.getPriority();
				int op2 = o2.getPriority();
				return (op1 < op2) ? 1 : (op1 > op2) ? -1 : 0;
			}
		});
		_locator = locator != null ? locator : wapp;
	}

	/** Parses the specified file.
	 *
	 * @param path the request path.
	 * It is used as {@link org.zkoss.zk.ui.Page#getRequestPath}, or null
	 * if not available.
	 */
	public PageDefinition parse(File file, String path) throws Exception {
		//if (log.isDebugEnabled()) log.debug("Parsing "+file);
		String extension = Servlets.getExtension(file.getName());
		final PageDefinition pgdef = parse(TreeBuilderFactory.makeBuilder(extension).parse(file), extension);
		pgdef.setRequestPath(path);
		return pgdef;
	}

	/** Parses the specified URL.
	 *
	 * @param path the request path.
	 * It is used as {@link org.zkoss.zk.ui.Page#getRequestPath}, or null
	 * if not available.
	 */
	public PageDefinition parse(URL url, String path) throws Exception {
		//if (log.isDebugEnabled()) log.debug("Parsing "+url);
		String extension = Servlets.getExtension(url.toExternalForm());
		final PageDefinition pgdef = parse(TreeBuilderFactory.makeBuilder(extension).parse(url), extension);
		pgdef.setRequestPath(path);
		return pgdef;
	}

	/** Parses from the specified reader.
	 *
	 * @param extension the default extension if doc (of reader) doesn't specify
	 * an language. Ignored if null.
	 * If extension is null and the content doesn't specify a language,
	 * the language called "xul/html" is assumed.
	 */
	public PageDefinition parse(Reader reader, String extension) throws Exception {
		//if (log.isDebugEnabled()) log.debug("Parsing "+reader);
		return parse(TreeBuilderFactory.makeBuilder(extension).parse(reader), extension);
	}

	/** Parse the raw content directly from a DOM tree.
	 *
	 * @param extension the default extension if doc doesn't specify
	 * an language. Ignored if null.
	 * If extension is null and the content doesn't specify a language,
	 * the language called "xul/html" is assumed.
	 */
	public PageDefinition parse(Document doc, String extension) throws Exception {
		//1. parse the page and import directive if any
		final List<ProcessingInstruction> pis = new LinkedList<ProcessingInstruction>();
		final List<String[]> imports = new LinkedList<String[]>();
		final List<String> impclses = new LinkedList<String>();
		String lang = null;
		for (Object o : doc.getChildren()) {
			if (!(o instanceof ProcessingInstruction))
				continue;

			final ProcessingInstruction pi = (ProcessingInstruction) o;
			final String target = pi.getTarget();
			if ("page".equals(target)) {
				//we handle only the language attribute here
				final Map<String, String> params = pi.parseData();
				final String l = params.remove("language");
				if (l != null) {
					noEL("language", l, pi);
					lang = l;
				}

				if (!params.isEmpty())
					pis.add(pi); //process it later
			} else if ("import".equals(target)) { //import
				final Map<String, String> params = pi.parseData();
				final String src = params.remove("src");
				final String dirs = params.remove("directives");
				final String cls = params.remove("class");
				if (src != null) {
					noELnorEmpty("src", src, pi);
					noEL("directives", dirs, pi);
					imports.add(new String[] { src, dirs });
				}
				if (cls != null) {
					noELnorEmpty("class", cls, pi);
					impclses.add(cls);
				}
				for (Map.Entry<String, String> me : params.entrySet()) {
					final String nm = me.getKey();
					final String val = me.getValue();
					if (val == null) {
						noELnorEmpty(nm, nm, pi);
						impclses.add(nm);
					} else {
						log.warn(message("Ignored unknown attribute for import: " + nm, pi));
					}
				}
			} else {
				pis.add(pi);
			}
		}

		//2. Create PageDefinition
		final LanguageDefinition langdef = extension != null && lang == null
				? LanguageDefinition.getByExtension(extension) : LanguageDefinition.lookup(lang);
		final PageDefinition pgdef = new PageDefinition(langdef, getLocator());

		//3a. resolve imports
		if (!imports.isEmpty()) {
			final RequestInfo ri = new RequestInfoImpl(_wapp, null, null, null, getLocator());
			final UiFactory uf = ((WebAppCtrl) _wapp).getUiFactory();
			for (final String[] imprt : imports) {
				final String path = imprt[0], dirs = imprt[1];
				try {
					final PageDefinition pd = uf.getPageDefinition(ri, path);
					if (pd == null)
						throw new UiException("The imported page not found: " + path);
					pgdef.imports(pd, parseToArray(dirs));
				} catch (PotentialDeadLockException ex) {
					throw new UiException("Recursive import not allowed: " + path, ex);
				}
			}
		}
		//3b. resolve imported classes
		for (String impcls : impclses)
			pgdef.addImportedClass(impcls);

		//4. Processing the rest of processing instructions at the top level
		for (ProcessingInstruction pi : pis)
			parse(pgdef, pi);

		//5. Processing from the root element
		final Element root = doc.getRootElement();
		if (root != null)
			parseItem(pgdef, pgdef, root, new AnnotationHelper(), false, ParsingState.ROOT); //ZK-2632: Parser support disorder template tag

		return pgdef;
	}

	/** Parse processing instruction.
	 */
	private void parse(PageDefinition pgdef, ProcessingInstruction pi) throws Exception {
		final String target = pi.getTarget();
		final Map<String, String> params = pi.parseData();
		if ("page".equals(target)) {
			parsePageDirective(pgdef, pi, params);
		} else if ("init".equals(target)) {
			parseInitDirective(pgdef, pi, params);
		} else if ("variable-resolver".equals(target) || "function-mapper".equals(target)) {
			final String clsnm = params.remove("class");
			if (isEmpty(clsnm))
				throw new UiException(message("The class attribute is required", pi));

			final Map<String, String> args = new LinkedHashMap<String, String>(params);
			if ("variable-resolver".equals(target))
				pgdef.addVariableResolverInfo(new VariableResolverInfo(clsnm, args));
			else
				pgdef.addFunctionMapperInfo(new FunctionMapperInfo(clsnm, args));
		} else if ("component".equals(target)) { //declare a component
			parseComponentDirective(pgdef, pi, params);
		} else if ("taglib".equals(target)) {
			final String uri = params.remove("uri");
			final String prefix = params.remove("prefix");
			if (!params.isEmpty())
				log.warn(message("Ignored unknown attributes: " + params.keySet(), pi));
			if (uri == null || prefix == null)
				throw new UiException(message("Both uri and prefix attribute are required", pi));
			//if (log.isDebugEnabled()) log.debug("taglib: prefix="+prefix+" uri="+uri);
			noEL("prefix", prefix, pi);
			noEL("uri", uri, pi); //not support EL (kind of chicken-egg issue)
			pgdef.addTaglib(new Taglib(prefix, toAbsoluteURI(uri)));
		} else if ("evaluator".equals(target)) {
			parseEvaluatorDirective(pgdef, pi, params);
		} else if ("xel-method".equals(target)) {
			parseXelMethod(pgdef, pi, params);
		} else if ("link".equals(target) || "meta".equals(target) || "script".equals(target)
				|| "style".equals(target)) { //declare a header element
			pgdef.addHeaderInfo(new HeaderInfo(target, params,
					ConditionImpl.getInstance(params.remove("if"), params.remove("unless"))));
		} else if ("header".equals(target)) { //declare a response header
			pgdef.addResponseHeaderInfo(new ResponseHeaderInfo(params.remove("name"), params.remove("value"),
					params.remove("append"), ConditionImpl.getInstance(params.remove("if"), params.remove("unless"))));
		} else if ("root-attributes".equals(target)) {
			for (Map.Entry<String, String> stringStringEntry : pi.parseData().entrySet()) {
				final Map.Entry me = (Map.Entry) stringStringEntry;
				pgdef.setRootAttribute((String) me.getKey(), (String) me.getValue());
			}
		} else if ("forward".equals(target)) { //forward
			final String uri = params.remove("uri");
			final String ifc = params.remove("if");
			final String unless = params.remove("unless");
			if (!params.isEmpty())
				log.warn(message("Ignored unknown attributes: " + params.keySet(), pi));
			noEmpty("uri", uri, pi);
			pgdef.addForwardInfo(new ForwardInfo(uri, ConditionImpl.getInstance(ifc, unless)));
		} else if ("import".equals(target)) { //import
			throw new UiException(message("The import directive can be used only at the top level", pi));
		} else if ("template".equals(target)) { //ZK-2623: page scope template
			final String name = params.remove("name");
			final String src = params.remove("src");
			if (name != null && name.length() != 0 && src != null && src.length() != 0)
				pgdef.addTemplateInfo(new TemplateInfo(null, name, src, params, null));
		} else {
			log.warn(message("Unknown processing instruction: " + target, pi));
		}
	}

	/** Parses a list of string separated by comma, into a String array.
	 */
	private static String[] parseToArray(String s) {
		if (s == null)
			return null;
		Collection<String> ims = CollectionsX.parse(null, s, ',', false); //NO EL
		return ims.toArray(new String[ims.size()]);
	}

	//-- derive to override --//
	/** returns locator for locating resources. */
	public Locator getLocator() {
		return _locator;
	}

	/** Process the init directive. */
	private void parseInitDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
			throws Exception {
		final String clsnm = params.remove("class");
		final String zsrc = params.remove("zscript");

		final Map<String, String> args = new LinkedHashMap<String, String>(params);
		if (clsnm == null) {
			if (zsrc == null)
				throw new UiException(message("Either the class or zscript attribute must be specified", pi));

			checkZScriptEnabled(pi.getLocator());
			ZScript zs = null;
			final String zslang = pgdef.getZScriptLanguage();
			if (!zsrc.contains("${")) {
				final URL url = getLocator().getResource(zsrc);
				if (url != null)
					zs = new ZScript(zslang, url);
				//Bug 2929887: defer the error message since it might not be required
			}
			if (zs == null)
				zs = new ZScript(pgdef.getEvaluatorRef(), zslang, zsrc, getLocator());

			pgdef.addInitiatorInfo(new InitiatorInfo(new ZScriptInitiator(zs), args));
		} else {
			if (zsrc != null)
				throw new UiException(message("You cannot specify both class and zscript", pi));

			pgdef.addInitiatorInfo(new InitiatorInfo(clsnm, args));
		}
	}

	private static String message(String message, Item el) {
		return org.zkoss.xml.Locators.format(message, el != null ? el.getLocator() : null);
	}

	private static String message(String message, org.zkoss.xml.Locator loc) {
		return org.zkoss.xml.Locators.format(message, loc);
	}

	private static org.zkoss.util.resource.Location location(Item el) {
		return org.zkoss.xml.Locators.toLocation(el != null ? el.getLocator() : null);
	}

	private void checkZScriptEnabled(Element el) {
		checkZScriptEnabled(el.getLocator());
	}

	private void checkZScriptEnabled(org.zkoss.xml.Locator loc) {
		if (!_wapp.getConfiguration().isZScriptEnabled())
			throw new UiException(message("zscript is not allowed since <disable-zscript> is configured", loc));
	}

	/** Process the page directive. */
	private static void parsePageDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
			throws Exception {
		for (Map.Entry<String, String> me : pi.parseData().entrySet()) {
			final String nm = me.getKey();
			final String val = me.getValue();
			if ("language".equals(nm)) {
				if (!(pi.getParent() instanceof Document))
					log.warn(
							message("Ignored language attribute since the page directive is not at the top level", pi));
			} else if ("title".equals(nm)) {
				pgdef.setTitle(val);
			} else if ("style".equals(nm)) {
				pgdef.setStyle(val);
			} else if ("viewport".equals(nm)) {
				pgdef.setViewport(val);
			} else if ("id".equals(nm)) {
				pgdef.setId(val);
			} else if ("widgetClass".equals(nm)) {
				pgdef.setWidgetClass(val);
			} else if ("zscriptLanguage".equals(nm) || "zscript-language".equals(nm)) { //backward compatible with 2.4.x
				noELnorEmpty("zscriptLanguage", val, pi);
				pgdef.setZScriptLanguage(val);
			} else if ("cacheable".equals(nm)) {
				noELnorEmpty("cacheable", val, pi);
				pgdef.setCacheable(Boolean.valueOf("true".equals(val)));
			} else if ("automaticTimeout".equals(nm)) {
				noELnorEmpty("automaticTimeout", val, pi);
				pgdef.setAutomaticTimeout(Boolean.valueOf("true".equals(val)));
			} else if ("contentType".equals(nm)) {
				noEmpty("contentType", val, pi);
				pgdef.setContentType(val);
			} else if ("docType".equals(nm)) {
				pgdef.setDocType(isEmpty(val) ? "" : "<!DOCTYPE " + val + '>');
			} else if ("xml".equals(nm)) {
				noEmpty("xml", val, pi);
				pgdef.setFirstLine("<?xml " + val + "?>");
			} else if ("complete".equals(nm)) {
				pgdef.setComplete("true".equals(val));
			} else {
				log.warn(message("Ignored unknown attribute: " + nm, pi));
			}
		}
	}

	/** Process the component directive. */
	private void parseComponentDirective(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
			throws Exception {
		final String name = params.remove("name");
		noELnorEmpty("name", name, pi);

		String macroURI = params.remove("macroURI");
		if (macroURI == null)
			macroURI = params.remove("macro-uri"); //backward compatible (2.4.x)
		final String extds = params.remove("extends");
		final String clsnm = params.remove("class");
		final String lang = params.remove("language");
		final LanguageDefinition langdef = lang != null ? LanguageDefinition.lookup(lang)
				: pgdef.getLanguageDefinition();
		ComponentDefinition compdef;
		String templateURI = params.remove("templateURI");
		if (templateURI != null) { // assume it is shadow like (<apply>)
			noEL("templateURI", templateURI, pi);
			//no EL because pagedef must be loaded to resolve
			//the implementing class before creating an instance of shadow

			compdef = langdef.getShadowDefinition(name, pgdef, toAbsoluteURI(templateURI));
			if (!isEmpty(clsnm)) {
				noEL("class", clsnm, pi);
				compdef.setImplementationClass(clsnm);
				//Resolve later since might defined in zscript
			}
		} else if (macroURI != null) {
			//if (log.finerable()) log.finer("macro component definition: "+name);

			final String inline = params.remove("inline");
			noEL("inline", inline, pi);
			noEL("macroURI", macroURI, pi);
			//no EL because pagedef must be loaded to resolve
			//the implementing class before creating an instance of macro

			final boolean bInline = "true".equals(inline);
			compdef = langdef.getMacroDefinition(name, toAbsoluteURI(macroURI), bInline, pgdef);
			if (!isEmpty(clsnm)) {
				if (bInline)
					throw new UiException(message("class not allowed with inline macros", pi));
				noEL("class", clsnm, pi);
				compdef.setImplementationClass(clsnm);
				//Resolve later since might defined in zscript
			}
		} else {
			ComponentDefinition ref = null;
			if (extds != null) { //extends
				//if (log.finerable()) log.finer("Override component definition: "+name);

				noEL("extends", extds, pi);
				ref = langdef.getComponentDefinition(extds);
			} else {
				try {
					final Class cls = pgdef.getImportedClassResolver().resolveClass(clsnm);
					if (lang != null) {
						ref = langdef.getComponentDefinition(cls);
						//throw exception if not found
					} else {
						ref = pgdef.getComponentDefinition(cls, true);
						if (ref == null) //return null if not found
							ref = Components.getDefinitionByDeviceType(langdef.getDeviceType(), cls);
					}
				} catch (Throwable ex) { //ignore
				}
			}

			if (ref != null) {
				if (ref.isMacro())
					throw new UiException(message("Unable to extend from a macro component", pi));

				compdef = ref.clone(null, name);
				if (!isEmpty(clsnm)) {
					noEL("class", clsnm, pi);
					compdef.setImplementationClass(clsnm);
					//Resolve later since might defined in zscript
				}
			} else {
				//if (log.finerable()) log.finer("Add component definition: name="+name);

				noELnorEmpty("class", clsnm, pi);

				final ComponentDefinitionImpl cdi = new ComponentDefinitionImpl(null, pgdef, name,
						(Class<? extends Component>) null);
				cdi.setCurrentDirectory(getLocator().getDirectory());
				//mold URI requires it
				compdef = cdi;
				compdef.setImplementationClass(clsnm);
				//Resolve later since might be defined in zscript
			}
		}

		String wgtnm = params.remove("widgetClass");
		if (wgtnm == null)
			wgtnm = params.remove("widget-class");
		if (wgtnm != null)
			compdef.setDefaultWidgetClass(wgtnm);

		pgdef.addComponentDefinition(compdef);

		Object o = params.remove("moldURI");
		if (o == null)
			o = params.remove("mold-uri");
		if (o != null)
			throw new UnsupportedOperationException(
					message("moldURI not supported in 5.0. Use <?script?> or lang-addon.xml instead", pi));

		o = params.remove("cssURI");
		if (o != null)
			throw new UnsupportedOperationException(
					message("cssURI not supported in 5.0. Use <?link?> or lang-addon.xml instead", pi));

		compdef.setApply(params.remove("apply"));

		for (Map.Entry<String, String> me : params.entrySet()) {
			compdef.addProperty(me.getKey(), me.getValue());
		}
	}

	/** Parse the evaluator directive. */
	private static void parseEvaluatorDirective(PageDefinition pgdef, ProcessingInstruction pi,
			Map<String, String> params) throws Exception {
		final String clsnm = params.remove("class");
		if (clsnm != null && clsnm.length() > 0) {
			noELnorEmpty("class", clsnm, pi);
			pgdef.setExpressionFactoryClass(pgdef.getImportedClassResolver().resolveClass(clsnm));
		} else { //name has the lower priority
			final String nm = params.remove("name");
			if (nm != null)
				pgdef.setExpressionFactoryClass(Evaluators.getEvaluatorClass(nm));
		}

		final String imports = params.remove("import");
		if (imports != null && imports.length() > 0) {
			Collection<String> ims = CollectionsX.parse(null, imports, ',', false); //No EL
			for (String im : ims) {
				final int k = im.indexOf('=');
				String nm = k > 0 ? im.substring(0, k).trim() : null;
				String cn = (k >= 0 ? im.substring(k + 1) : im).trim();

				if (cn.length() != 0) {
					if (nm == null || nm.length() == 0) {
						final int j = cn.lastIndexOf('.');
						nm = j >= 0 ? cn.substring(j + 1) : cn;
					}
					pgdef.addExpressionImport(nm, Classes.forNameByThread(cn));
					//evaluator's import does not support <?import class?>,
					//since it looks strange.
					//FUTURE: it is better to deprecate this attribute, and
					//have FunctionMapperExt to depend on <?import class?>
					//(however, it is worth since only MVEL/OGNL uses it)
				}
			}
		}
	}

	/** Parse the XEL method. */
	private static void parseXelMethod(PageDefinition pgdef, ProcessingInstruction pi, Map<String, String> params)
			throws Exception {
		final String prefix = params.remove("prefix");
		noELnorEmpty("prefix", prefix, pi);
		final String nm = params.remove("name");
		noELnorEmpty("name", nm, pi);
		final String clsnm = params.remove("class");
		noELnorEmpty("class", clsnm, pi);
		final String sig = params.remove("signature");
		noELnorEmpty("signature", sig, pi);

		final Method mtd;
		try {
			final ClassResolver clsresolver = pgdef.getImportedClassResolver();
			final Class cls = clsresolver.resolveClass(clsnm);
			mtd = Classes.getMethodBySignature(cls, sig, null, clsresolver);
		} catch (ClassNotFoundException ex) {
			throw new UiException(message("Class not found: " + ex.getMessage(), pi));
		} catch (Exception ex) {
			throw new UiException(message("Method not found: " + sig + " in " + clsnm, pi));
		}
		if ((mtd.getModifiers() & Modifier.STATIC) == 0)
			throw new UiException(message("Not a static method: " + mtd, pi));

		pgdef.addXelMethod(prefix, nm, new MethodFunction(mtd));
	}

	private static void noELnorEmpty(String nm, String val, Item item) throws UiException {
		if (isEmpty(val))
			throw new UiException(message(nm + " cannot be empty", item));
		noEL(nm, val, item);
	}

	private static void noEL(String nm, String val, Item item) throws UiException {
		if (val != null && val.contains("${"))
			throw new UiException(message(nm + " does not support EL expressions", item));
	}

	/** Checks whether the value is an empty string.
	 * Note: Like {@link #noEL}, it is OK to be null!!
	 */
	private static void noEmpty(String nm, String val, Item item) throws UiException {
		if (val != null && val.length() == 0)
			throw new UiException(message(nm + " cannot be empty", item));
	}

	private String toAbsoluteURI(String uri) {
		if (uri != null && uri.length() > 0) {
			final char cc = uri.charAt(0);
			if (cc != '/' && cc != '~' && !Servlets.isUniversalURL(uri)) {
				final String dir = getLocator().getDirectory();
				if (dir != null && dir.length() > 0)
					return dir.charAt(dir.length() - 1) == '/' ? dir + uri : dir + '/' + uri;
			}
		}
		return uri;
	}

	/** Parses the specified elements.
	 * @param bNativeContent whether to consider the child element all native
	 * It is true if a component definition with text-as is found
	 */
	private void parseItems(final PageDefinition pgdef, final NodeInfo parent, Collection items,
			AnnotationHelper annHelper, boolean bNativeContent) throws Exception {
		LanguageDefinition parentlang = getLanguageDefinition(parent);
		if (parentlang == null)
			parentlang = pgdef.getLanguageDefinition();
		final boolean bZkSwitch = isZkSwitch(parent);

		ComponentInfo pi = null;
		String textAs = null;
		StringBuilder textAsBuffer = null;
		for (NodeInfo p = parent; p != null; p = p.getParent())
			if (p instanceof ComponentInfo) {
				pi = (ComponentInfo) p;
				textAs = pi.getTextAs();
				if (textAs != null && pi == parent) //only direct child
					textAsBuffer = new StringBuilder();
				break; //found
			}
		final boolean isXHTML = "xhtml".equals(parentlang.getName());
		final boolean isAllBlankPreserved = !"false".equals(Library.getProperty("preserve-all-blank"));
		boolean breakLine = false;
		NativeInfo preNativeInfo = null; //check if brother is native

		//ZK-2632: Parser support disorder template tag
		for (final Object o : items) {
			if (o instanceof Element)
				parseItem(pgdef, parent, (Element) o, annHelper, bNativeContent, ParsingState.FIRST);
		}

		for (final Object o : items) {
			if (o instanceof Element) {
				breakLine = false;
				preNativeInfo = (NativeInfo) parseItem(pgdef, parent, (Element) o, annHelper, bNativeContent,
						ParsingState.SECOND);
			} else if (o instanceof ProcessingInstruction) {
				breakLine = false;
				parse(pgdef, (ProcessingInstruction) o);
			} else if (o instanceof Comment) {
				breakLine = false;
				// keep the comment, like <!--[if lte IE 9]>
				if (parentlang.isNative() || isXHTML) {
					String label = "<!--" + ((Item) o).getText() + "-->";
					final ComponentInfo labelInfo = parentlang.newLabelInfo(parent, label);
					labelInfo.addProperty("encode", "false", null);
				}
			} else if ((o instanceof Text) || (o instanceof CData)) {
				String label = ((Item) o).getText(), trimLabel = !isXHTML ? label.trim() : label; // do not trim with xhtml
				if (breakLine && (o instanceof Text) && isEmpty(label.trim())) {
					// we need to merge the breakLine into the previous one to save memory
					List<NodeInfo> children = parent.getChildren();
					final String labelAttr = parentlang.getLabelAttribute();
					for (Property prop : ((ComponentInfo) children.get(children.size() - 1)).getProperties()) {
						if (prop.getName().equals(labelAttr)) {
							prop.setRawValue(prop.getRawValue() + trimLabel);
						}
					}
					continue;
				}
				if (isEmpty(label))
					continue;

				if (bZkSwitch) {
					if (isEmpty(trimLabel))
						continue;
					throw new UiException(message("Only <zk> can be used in <zk switch>", (Item) o));
				}

				//ZK-2760: if parent is xhtml and no other sibling and length of label is 0, don't create TextInfo to it
				if (isXHTML && items.size() <= 1 && isEmpty(trimLabel.trim())) {
					continue; //don't create Text
				} else if (isEmpty(trimLabel) && (pi != null && !pi.isBlankPreserved() && !isNativeText(pi)))
					continue; //Ignore blank text if no need to preserved
				else if (isEmpty(label.trim()) && !isAllBlankPreserved)
					continue;

				if (!isXHTML && (o instanceof Text) && isEmpty(label.trim()))
					breakLine = true;

				//consider as a label
				if (isNativeText(pi)) {

					// trim the cdata when in the XHTML parser which will retain the syntax
					String newLabel = label.trim();
					if (newLabel.startsWith("<![CDATA[") && newLabel.endsWith("]]>")) {
						label = newLabel.substring(9, newLabel.length() - 3);
					}
					// Bug ZK-2845: ShadowInfo does not allow TextInfo as child
					if (!(parent instanceof ShadowInfo)) {
						// It's possible to replace multiple whitespace characters with single space
						new TextInfo(parent, isEmpty(trimLabel) ? " " : label);
					}
					//Don't trim if native (3.5.0)
				} else {
					if (textAs != null) { //implies pi != null (parent is ComponentInfo)
						if (!isEmpty(trimLabel))
							if (textAsBuffer != null) //implies pi == parent
								textAsBuffer.append(label); //concatenate all together
							else if (!(parent instanceof TemplateInfo))
								throw new UnsupportedOperationException(
										message("Not allowed in text-as", ((Item) o).getParent()));
					} else { // shadow element shouldn't support empty LabelInfo
						if (parent instanceof ShadowInfo) {
							if (isEmpty(trimLabel))
								continue; //ignore
						}
						if (isTrimLabel() && !parentlang.isRawLabel()) {
							if (isEmpty(trimLabel))
								continue; //ignore
							label = trimLabel;
						}
						if (isXHTML && preNativeInfo != null && isEmpty(label.trim())) { //Merge break line between two NativeInfo when using zhtml
							preNativeInfo.addEpilogChild(new TextInfo(null, label));
						} else {
							final ComponentInfo labelInfo = parentlang.newLabelInfo(parent, label);
							if (isEmpty(trimLabel))
								labelInfo.setReplaceableText(" "); //yes, it can be replaced by a text, and it's possible to replace multiple whitespace characters with single space
						}
					}
				}
			} else {
				breakLine = false;
			}
		}

		if (textAsBuffer != null) { //parent might be TempalteInfo
			String trimLabel = textAsBuffer.toString();

			// Bug ZK-1911
			if (pi == null || !pi.isBlankPreserved())
				trimLabel = trimLabel.trim();

			if (!isEmpty(trimLabel))
				pi.addProperty(textAs, trimLabel, null);
		}
	}

	/*package*/ static boolean isNativeText(ComponentInfo pi) { //also called by ComponentInfo
		if (pi instanceof NativeInfo)
			return true;

		if (pi != null) {
			try {
				final Class cls = pi.resolveImplementationClass(null, null);
				return cls != null && Native.class.isAssignableFrom(cls);
			} catch (Throwable ex) { //ignore
			}
		}
		return false;
	}

	/** Returns whether to trim the leading and trailing whitespaces
	 * of labels.
	 * <p>Default: false since 3.0.4.
	 *
	 * <p>If you want to trim like 3.0.4 and earlier did, you can specify
	 * the system property called "org.zkoss.zk.ui.parser.trimLabel"
	 * with a non-empty value.
	 */
	private static boolean isTrimLabel() {
		if (_trimLabel == null) {
			final String s = Library.getProperty("org.zkoss.zk.ui.parser.trimLabel");
			_trimLabel = Boolean.valueOf(s != null && s.length() > 0);
		}
		return _trimLabel.booleanValue();
	}

	private static Boolean _trimLabel;

	private static final LanguageDefinition getLanguageDefinition(NodeInfo node) {
		for (; node != null; node = node.getParent()) {
			if (node instanceof ComponentInfo) {
				LanguageDefinition langdef = ((ComponentInfo) node).getLanguageDefinition();
				if (langdef != null)
					return langdef;
			} else if (node instanceof PageDefinition) {
				return ((PageDefinition) node).getLanguageDefinition();
			} else if (node instanceof ShadowInfo) {
				return ((ShadowInfo) node).getLanguageDefinition();
			}
		}
		return null;
	}


	/** Parse an component definition specified in the given element.
	 * @param bNativeContent whether to consider the child elements all native
	 * It is true if a component definition with text-as is found
	 * @return native item for optimization
	 */
	private Object parseItem(PageDefinition pgdef, NodeInfo parent, Element el, AnnotationHelper annHelper,
			boolean bNativeContent, ParsingState parsingState) throws Exception {
		final String nm = el.getLocalName();
		final Namespace ns = el.getNamespace();
		final String pref = ns != null ? ns.getPrefix() : "";
		final String uri = ns != null ? ns.getURI() : "";
		LanguageDefinition langdef = pgdef.getLanguageDefinition();
		final String langName = langdef.getName();

		if (LanguageDefinition.ANNOTATION_NAMESPACE.equals(uri) || "annotation".equals(uri))
			throw new UiException(message("Namespace, " + uri + ", no longer supported element's annotation", el));

		//ZK-2632: Parser support disorder template tag
		if (parsingState != ParsingState.SECOND) {
			if ("attribute".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
				if (!(parent instanceof ComponentInfo))
					throw new UiException(message("<attribute> cannot be the root element", el));
				parseAttribute(pgdef, (ComponentInfo) parent, el, annHelper);
			} else if ("template".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
				parseItems(pgdef, parseTemplate(parent, el, annHelper), el.getChildren(), annHelper, bNativeContent);
			}
		}

		if (parsingState != ParsingState.FIRST) {
			if (("attribute".equals(nm) || "template".equals(nm)) && isZkElement(langdef, nm, pref, uri))
				return null;
			if ("zscript".equals(nm) && isZkElement(langdef, nm, pref, uri)) {
				checkZScriptEnabled(el);
				parseZScript(parent, el, annHelper);
			} else if ("custom-attributes".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
				parseCustomAttributes(langdef, parent, el, annHelper);
			} else if ("variables".equals(nm) && isZkElement(langdef, nm, pref, uri, bNativeContent)) {
				parseVariables(langdef, parent, el, annHelper);
			} else if ("zk".equals(nm) && isZkElement(langdef, nm, pref, uri)) {
				parseItems(pgdef, parseZk(parent, el, annHelper), el.getChildren(), annHelper, bNativeContent);
			} else if (isShadowElement(langdef, pgdef, nm, pref, uri, bNativeContent)) {
				NodeInfo nodeInfo = parseShadowElement(pgdef, parent, el, annHelper);
				parseItems(pgdef, nodeInfo, el.getChildren(), annHelper, bNativeContent);
				if (nodeInfo instanceof ShadowInfo && ((ShadowInfo) nodeInfo).isAnnotationNamespacedRoot()) {
					annHelper.setIgnoreAnnotNamespace(false);
				} else if (nodeInfo instanceof TemplateInfo && ((TemplateInfo) nodeInfo).isAnnotationNamespacedRoot()) {
					annHelper.setIgnoreAnnotNamespace(false);
				}
			} else {
				//if (log.isDebugEnabled()) log.debug("component: "+nm+", ns:"+ns);
				if (isZkSwitch(parent))
					throw new UiException(message("Only <zk> can be used in <zk switch>", el));

				boolean prefRequired = uri.startsWith(LanguageDefinition.NATIVE_NAMESPACE_PREFIX);
				boolean bNative = bNativeContent || prefRequired || LanguageDefinition.NATIVE_NAMESPACE.equals(uri)
						|| "native".equals(uri);

				if (!bNative && langdef.isNative() && !langdef.getNamespace().equals(uri))
					bNative = prefRequired = ("".equals(pref) && "".equals(uri)) || !LanguageDefinition.exists(uri);
				//Spec: if pref/URI not specified => native
				//		if uri unknown => native

				final ComponentInfo compInfo;
				if (bNative) {
					if (annHelper.clear())
						log.warn(message("Annotations are ignored since native doesn't support them", el));

					final NativeInfo ni;
					compInfo = ni = new NativeInfo(parent, langdef.getNativeDefinition(),
							prefRequired && pref.length() > 0 ? pref + ":" + nm : nm);

					//add declared namespace if starting with native:
					final Collection<Namespace> dns = el.getDeclaredNamespaces();
					if (!dns.isEmpty())
						addDeclaredNamespace(ni, dns, langdef);
				} else {
					final boolean defaultNS = isDefaultNS(langdef, pref, uri);
					final LanguageDefinition complangdef = defaultNS ? langdef : LanguageDefinition.lookup(uri);
					ComponentDefinition compdef = defaultNS ? pgdef.getComponentDefinitionMap().get(nm) : null;
					if (compdef != null) {
						compInfo = new ComponentInfo(parent, compdef, nm);
					} else if (complangdef.hasComponentDefinition(nm)) {
						compdef = complangdef.getComponentDefinition(nm);
						compInfo = new ComponentInfo(parent, compdef, nm);
						langdef = complangdef;
					} else {
						compdef = complangdef.getDynamicTagDefinition();
						if (compdef == null)
							throw new DefinitionNotFoundException(
									message("Component definition not found: " + nm + " in " + complangdef, el));
						compInfo = new ComponentInfo(parent, compdef, nm);
						langdef = complangdef;
					}

					//process use first because addProperty needs it
					String use = el.getAttributeValue("use");
					if (use != null) {
						use = use.trim();
						if (use.length() != 0)
							compInfo.setImplementation(use);
						//Resolve later since might defined in zscript
					}
				}

				String ifc = null, unless = null, forEach = null, forEachBegin = null, forEachEnd = null,
						forEachStep = null;
				AnnotationHelper attrAnnHelper = null;
				//ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
				boolean isMVVM = false;
				boolean shouldIgnoreAnnotNamespace = annHelper.shouldIgnoreAnnotNamespace();
				for (final Attribute attr : el.getAttributeItems()) {
					final Namespace attrns = attr.getNamespace();
					final String attURI = attrns != null ? attrns.getURI() : "";
					final String attnm = attr.getLocalName();
					final String attval = attr.getValue();
					final String attPref = attrns != null ? attrns.getPrefix() : "";

					// ZK-2494: use getName instead of getLocalName when namespace is native or xml and attribute uri 
					// is not one of known namespaces in zk. Exclude xmlns to avoid redefine.
					if (isNativeNamespace(uri) || isXmlNamespace(uri) || "native".equals(langName)
							|| "xml".equals(langName)) {
						if (!isZkAttr(langdef, attrns) && !isZKNamespace(attURI) && !"xmlns".equals(attPref)
								&& !("xmlns".equals(attnm) && "".equals(attPref))
								&& !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {

							// Bug ZK-2995
							boolean handled = false;
							for (NamespaceParser nsParser : _nsParsers) {
								if (nsParser.isMatched(attURI)) {
									if (nsParser.parse(attr, compInfo, pgdef)) {
										handled = true;
										break;
									}
								}
							}
							if (!handled) {
								compInfo.addProperty(attr.getName(), attval, null);
							}
							continue;
						} else if (isClientNamespace(attURI) || isClientAttrNamespace(attURI)) {
							compInfo.addProperty(attnm, attval, null);
							continue;
						} else if (isClientAttrPrefixNamespace(attURI)) { // ZK-4474
							compInfo.addProperty(attPref + ":" + attnm, attval, null);
							continue;
						}
					}

					if ("apply".equals(attnm) && isZkAttr(langdef, attrns)) {
						compInfo.setApply(attval);
					} else if ("forward".equals(attnm) && isZkAttr(langdef, attrns)) {
						compInfo.setForward(attval);
					} else if ("if".equals(attnm) && isZkAttr(langdef, attrns)) {
						ifc = attval;
					} else if ("unless".equals(attnm) && isZkAttr(langdef, attrns)) {
						unless = attval;
					} else if ("forEach".equals(attnm) && isZkAttr(langdef, attrns)) {
						forEach = attval;
					} else if ("forEachStep".equals(attnm) && isZkAttr(langdef, attrns)) {
						forEachStep = attval;
					} else if ("forEachBegin".equals(attnm) && isZkAttr(langdef, attrns)) {
						forEachBegin = attval;
					} else if ("forEachEnd".equals(attnm) && isZkAttr(langdef, attrns)) {
						forEachEnd = attval;
					} else if ("fulfill".equals(attnm) && isZkAttr(langdef, attrns, bNativeContent)) {
						compInfo.setFulfill(attval);
					} else if (!shouldIgnoreAnnotNamespace && (LanguageDefinition.ANNOTATION_NAMESPACE.equals(attURI) || "annotation".equals(attURI))) {
						//ZK 6: annotation namespace mandates annotation
						if (attrAnnHelper == null)
							attrAnnHelper = new AnnotationHelper();
						applyAttrAnnot(attrAnnHelper, compInfo, attnm, attval.trim(), true, location(attr));
					} else if (!"use".equals(attnm) || !isZkAttr(langdef, attrns, bNativeContent)) {
						if (!"xmlns".equals(attPref) && !("xmlns".equals(attnm) && "".equals(attPref))
								&& !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {
							String attvaltrim = attval.trim();
							if ("viewModel".equals(attnm)) {
								if (attval.indexOf("@id") == -1) {
									attvaltrim = "@id('vm') " + attvaltrim;
									attvaltrim = attvaltrim.replace("@(", "@init(");
								}
							} else {
								attvaltrim = modifyAttrValueIfSimplified(attnm, attval.trim(), parent);
							}
							if (!bNativeContent && !bNative && (shouldIgnoreAnnotNamespace
									|| (attURI.length() == 0 || LanguageDefinition.ZK_NAMESPACE.endsWith(attURI)))
									&& AnnotationHelper.isAnnotation(attvaltrim)) {
								if (attrAnnHelper == null)
									attrAnnHelper = new AnnotationHelper();
								applyAttrAnnot(attrAnnHelper, compInfo, attnm, attvaltrim, true, location(attr));
								//ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
								Configuration config = WebApps.getCurrent().getConfiguration();
								if (config.getBinderInitAttribute().equals(attnm))
									isMVVM = true;
								//F80 - store subtree's binder annotation count
								Set<String> binderAnnotations = config.getBinderAnnotations();
								for (String annot : binderAnnotations) {
									if (attvaltrim.contains(annot)) {
										compInfo.enableBindingAnnotation();
										break;
									}
								}
							} else {
								boolean handled = false;
								for (NamespaceParser nsParser : _nsParsers) {
									if (nsParser.isMatched(attURI)) {
										if (nsParser.parse(attr, compInfo, pgdef)) {
											handled = true;
											break;
										}
									}
								}
								if (!handled) {
									addAttribute(compInfo, attrns, attnm, attval, null, attr.getLocator());
									if (attrAnnHelper != null)
										attrAnnHelper.applyAnnotations(compInfo, attnm, true);
								}
							}
						}
					}
				}

				//ZK 8: If the attribute of viewModel being used, auto apply "BindComposer"
				if (isMVVM) {
					String apply = compInfo.getApply();
					String bindComposerStr = "org.zkoss.bind.BindComposer";
					if (apply != null && apply.contains(bindComposerStr) && !apply.contains(",")) {
						log.warn(message("If the attribute of viewModel is being used, then \"" + bindComposerStr
								+ "\" will be applied automatically. "
								+ "Notice that if you want to use another Composer, you will need to apply the \""
								+ bindComposerStr + "\" yourself.", el));
					} else if (apply == null || apply.length() == 0) { //ZK-2763: only auto apply "BindComposer" when the attribute "apply" isn't used.
						compInfo.setApply(bindComposerStr);
					}
				}
				compInfo.setCondition(ConditionImpl.getInstance(ifc, unless));
				compInfo.setForEach(forEach, forEachBegin, forEachEnd, forEachStep);
				annHelper.applyAnnotations(compInfo, null, true);

				//only provide if there already has other annotation
				if (compInfo.getAnnotationMap() != null && el.getLocator() != null) {
					//provide component location info as a annotation with it's location.
					compInfo.addAnnotation(null, "ZKLOC", null, Locators.toLocation(el.getLocator()));
				}

				final Collection<Item> items = el.getChildren();
				String textAs = null;
				if (!bNativeContent && !items.isEmpty() //if empty, no need to check
						&& (textAs = compInfo.getTextAs()) != null) {
					//if textAs is specified, we detect if any child element
					//if so, we parse them as property
					//if not, we handle it normally and text, if any, will be
					//trimmed and assigned as a property (in parseItems)
					if (compInfo.isChildAllowedInTextAs() //don't consider it as text if childable and element found
							|| !textAsAllowed(langdef, items, bNativeContent))
						textAs = null;
				}

				if (textAs != null)
					parseAsProperty(pgdef, compInfo, textAs, items, annHelper, null);
				else
					parseItems(pgdef, compInfo, items, annHelper, bNativeContent);

				//optimize native components
				if (compInfo instanceof NativeInfo && !compInfo.getChildren().isEmpty()) {
					optimizeNativeInfos((NativeInfo) compInfo);
					return compInfo;
				}
			} //end-of-else//
		}
		return null;
	}

	private static String modifyAttrValueIfSimplified(String attrName, String attrValue, NodeInfo parent) {
		if (attrValue.matches("@\\(.*\\)")) {
			if (Events.isValid(attrName)) {
                String commandProperty = attrValue.substring(2, attrValue.length() - 1);
				boolean isNamedParam = false;
				int len = commandProperty.length();
				final StringBuilder modifiedCommandPropertySb = new StringBuilder(len);
				final StringBuilder sb = new StringBuilder(len);
				String nm = null;
				char quot = (char) 0;
				int paramIndex = -1;
				for (int j = 0;; ++j) {
					if (j >= len) {
						modifiedCommandPropertySb.append(modifyAttrValueIfSimplified0(nm, sb.toString().trim(), paramIndex, isNamedParam));
						break; //done
					}
					char cc = commandProperty.charAt(j);
					if (quot == (char) 0) {
						if (cc == ',') {
							sb.append(cc);
							modifiedCommandPropertySb.append(modifyAttrValueIfSimplified0(nm, sb.toString().trim(), paramIndex, isNamedParam));
							isNamedParam = nm != null;
							nm = null; //cleanup
							sb.setLength(0); //cleanup
							paramIndex++;
							continue; //next name=value
						} else if (cc == '=') {
							nm = sb.toString().trim(); //name found
							sb.setLength(0); //cleanup
							continue; //parse value
						} else if (cc == '\'' || cc == '"') {
							quot = cc;
						}
					} else if (cc == quot) {
						quot = (char) 0;
					}
					sb.append(cc);
					if (cc == '\\' && j < len - 1)
						sb.append(commandProperty.charAt(++j));
					//Note: we don't decode \x. Rather, we preserve it such
					//that the data binder can use them
				}
				attrValue = "@command(" + modifiedCommandPropertySb.toString() + ")";
			} else
				attrValue = "@init" + attrValue.substring(1);
		}
		return attrValue;
	}

	public static final String SIMPLIFIED_COMMAND_PARAM_PREFIX = "zk_Param_";

	private static String modifyAttrValueIfSimplified0(String nm, String val, int paramIndex, boolean isNamedParam) {
		if (nm == null) {
			if (isNamedParam)
				throw new UiException("Not allowed to use named parameters before un-named parameters.");
			if (paramIndex != -1) //skip command method name
				val = SIMPLIFIED_COMMAND_PARAM_PREFIX + paramIndex + "=" + val;
		} else {
			val = nm + "=" + val;
		}
		return val;
	}

	private boolean textAsAllowed(LanguageDefinition langdef, Collection<Item> items, boolean bNativeContent) {
		boolean textAsAllowed = true;
		String xmlFound = null; //whether a XML fragment
		String zkElem = null; //a ZK element
		boolean empty = true; //whether there is anything other than whitespace
		for (Iterator<Item> it = items.iterator();;) {
			if (zkElem != null && xmlFound != null)
				throw new UnsupportedOperationException(message(
						"Unable to handle XML fragment, <" + xmlFound + ">, with <" + zkElem
								+ ">. Please use CDATA instead",
						(it.hasNext() ? it.next() : items.iterator().next().getParent())));
			//might be possible to handle but not worth
			if (!it.hasNext())
				break;

			final Item o = it.next();
			if (empty)
				empty = (o instanceof Text || o instanceof CData) && o.getText().trim().length() == 0;

			if (o instanceof Element) {
				final Element e = (Element) o;
				final String n = e.getLocalName();
				if (isZkElement(langdef, e, bNativeContent) && ("attribute".equals(n) || "custom-attributes".equals(n)
						|| "variables".equals(n) || "template".equals(n) || "zscript".equals(n))) { //we have to skip zscript because of B50-3259479
					zkElem = n;
					textAsAllowed = false;
					//unable to handle them because EL/zscript might affect
					//the result
				} else {
					xmlFound = n;
					//including "zk" (risk if allowed to mix zk with attribute...)
				}
			}
		}
		return textAsAllowed && !empty;
		//if empty (i.e., only whitespace), so don't handle textAs (i.e., ignore it)
	}

	/** Parses the items as if they are native and they will become a property
	 * rather than child components.
	 */
	private void parseAsProperty(PageDefinition pgdef, ComponentInfo compInfo, String name, Collection items,
			AnnotationHelper annHelper, ConditionImpl cond) throws Exception {
		final NativeInfo nativeInfo = new NativeInfo(compInfo.getEvaluatorRef(),
				pgdef.getLanguageDefinition().getNativeDefinition(), "");
		//Note: nativeInfo can not be a child. Rather, it will be a property
		parseItems(pgdef, nativeInfo, items, annHelper, true);
		compInfo.addProperty(name, nativeInfo, cond);
	}

	/** @param val the value (it was trimmed before called). */
	private static void applyAttrAnnot(AnnotationHelper attrAnnHelper, ComponentInfo compInfo, String nm, String val,
			boolean selfAllowed, org.zkoss.util.resource.Location loc) {
		attrAnnHelper.addByCompoundValue(val.trim(), loc);
		attrAnnHelper.applyAnnotations(compInfo, selfAllowed && "self".equals(nm) ? null : nm, true);
	}

	/** @param val the value (it was trimmed before called). */
	private static void applyAttrAnnot(AnnotationHelper attrAnnHelper, ShadowInfo compInfo, String nm, String val,
			boolean selfAllowed, org.zkoss.util.resource.Location loc) {
		attrAnnHelper.addByCompoundValue(val.trim(), loc);
		attrAnnHelper.applyAnnotations(compInfo, selfAllowed && "self".equals(nm) ? null : nm, true);
	}

	private static void warnWrongZkAttr(Attribute attr) {
		log.warn(message("Attribute " + attr.getName() + " ignored in <zk>", attr));
	}

	private static boolean isZkSwitch(NodeInfo nodeInfo) {
		return nodeInfo instanceof ZkInfo && ((ZkInfo) nodeInfo).withSwitch();
	}

	private void parseZScript(NodeInfo parent, Element el, AnnotationHelper annHelper) {
		if (el.getAttributeItem("forEach") != null)
			throw new UiException(message("forEach not applicable to <zscript>", el));
		if (annHelper.clear())
			log.warn(message("Annotations are ignored since <zscript> doesn't support them", el));

		final String ifc = el.getAttributeValue("if"), unless = el.getAttributeValue("unless"),
				zsrc = el.getAttributeValue("src");
		final boolean deferred = "true".equals(el.getAttributeValue("deferred"));

		String zslang = el.getAttributeValue("language");
		if (zslang == null) {
			zslang = parent.getPageDefinition().getZScriptLanguage();
			//we have to resolve it in parser since a page might be
			//created by use of createComponents
		} else {
			noEmpty("language", zslang, el);
			noEL("language", zslang, el);
		}

		final ConditionImpl cond = ConditionImpl.getInstance(ifc, unless);
		if (!isEmpty(zsrc)) { //ignore empty (not error)
			ZScriptInfo zs = null;
			if (!zsrc.contains("${")) {
				final URL url = getLocator().getResource(zsrc);
				if (url != null)
					zs = new ZScriptInfo(parent, zslang, url, cond);
				//Bug 2929887: defer the error message since it might not be required
			}
			if (zs == null)
				zs = new ZScriptInfo(parent, zslang, zsrc, getLocator(), cond);

			if (deferred)
				zs.setDeferred(true);
		}

		String script = el.getText(false);
		if (!isEmpty(script.trim())) {
			final org.zkoss.xml.Locator l = el.getLocator();
			int lno = l != null ? l.getLineNumber() : 0;
			if (lno > 1) {
				final StringBuilder sb = new StringBuilder(lno);
				while (--lno > 0)
					sb.append('\n');
				script = sb.append(script).toString();
			}
			final ZScriptInfo zs = new ZScriptInfo(parent, zslang, script, cond);
			if (deferred)
				zs.setDeferred(true);
		}
	}

	private void parseAttribute(PageDefinition pgdef, ComponentInfo parent, Element el, AnnotationHelper annHelper)
			throws Exception {
		if (el.getAttributeItem("forEach") != null)
			throw new UiException(message("forEach not applicable to attribute", el));

		//Test if any element is used
		String elFound = null;
		for (final Item o : el.getChildren()) {
			if (o instanceof Element) {
				elFound = ((Element) o).getName();
				break;
			}
		}

		final Attribute attr = el.getAttributeItem(null, "name", 0); //by local name
		if (attr == null)
			throw new UiException(message("The name attribute required", el));

		final String attnm = attr.getValue();
		noEmpty("name", attnm, el);
		final ConditionImpl cond = ConditionImpl.getInstance(el.getAttributeValue("if"),
				el.getAttributeValue("unless"));
		if (elFound != null) {
			if (Events.isValid(attnm))
				throw new UiException(message("<" + elFound + "> not allowed in an event listener", el));

			parseAsProperty(pgdef, parent, attnm, el.getChildren(), annHelper, cond);
		} else {
			final String trim = el.getAttributeValue("trim");
			noEL("trim", trim, el);
			final String attval = el.getText(trim != null && "true".equals(trim));
			addAttribute(parent, attr.getNamespace(), attnm, attval, cond, el.getLocator());
		}

		annHelper.applyAnnotations(parent, attnm, true);
	}

	private static void parseCustomAttributes(LanguageDefinition langdef, NodeInfo parent, Element el,
			AnnotationHelper annHelper) throws Exception {
		//if (!el.getElements().isEmpty())
		//	throw new UiException(message("Child elements are not allowed for <custom-attributes>", el));

		if (parent instanceof PageDefinition)
			throw new UiException(message("<custom-attributes> must be used under a component", el));
		if (annHelper.clear())
			log.warn(message("Annotations are ignored since <custom-attributes> doesn't support them", el)); //old style annotation not supported

		String ifc = null, unless = null, scope = null, composite = null;
		final Map<String, String> attrs = new LinkedHashMap<String, String>();
		AnnotationHelper attrAnnHelper = null;
		for (final Attribute attr : el.getAttributeItems()) {
			final Namespace attrns = attr.getNamespace();
			final String attnm = attr.getLocalName();
			final String attval = attr.getValue();
			final String attvaltrim;
			if ("if".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				ifc = attval;
			} else if ("unless".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				unless = attval;
			} else if ("scope".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				scope = attval;
			} else if ("composite".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				composite = attval;
			} else if ("forEach".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				throw new UiException(message("forEach not applicable to <custom-attributes>", el));
			} else if (parent instanceof ComponentInfo && AnnotationHelper.isAnnotation(attvaltrim = attval.trim())) {
				if (attrAnnHelper == null)
					attrAnnHelper = new AnnotationHelper();
				applyAttrAnnot(attrAnnHelper, (ComponentInfo) parent, attnm, attvaltrim, false, location(attr));
			} else {
				attrs.put(attnm, attval);
			}
		}

		if (!attrs.isEmpty())
			new AttributesInfo(parent, attrs, scope, composite, ConditionImpl.getInstance(ifc, unless));
	}

	private static void parseVariables(LanguageDefinition langdef, NodeInfo parent, Element el,
			AnnotationHelper annHelper) throws Exception {
		//if (!el.getElements().isEmpty())
		//	throw new UiException(message("Child elements are not allowed for <variables> element", el));

		if (annHelper.clear())
			log.warn(message("Annotations are ignored since <variables> doesn't support them", el)); //old style annotation not supported here

		String ifc = null, unless = null, composite = null;
		boolean local = false;
		final Map<String, String> vars = new LinkedHashMap<String, String>();
		for (final Attribute attr : el.getAttributeItems()) {
			final Namespace attrns = attr.getNamespace();
			final String attnm = attr.getLocalName();
			final String attval = attr.getValue();
			if ("if".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				ifc = attval;
			} else if ("unless".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				unless = attval;
			} else if ("local".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				local = "true".equals(attval);
			} else if ("composite".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				composite = attval;
			} else if ("forEach".equals(attnm) && isZkElementAttr(langdef, attrns)) {
				throw new UiException(message("forEach not applicable to <variables>", el));
			} else {
				vars.put(attnm, attval);
			}
		}
		if (!vars.isEmpty())
			new VariablesInfo(parent, vars, local, composite, ConditionImpl.getInstance(ifc, unless));
	}

	private static NodeInfo parseShadowElement(PageDefinition pgdef, NodeInfo parent, Element el,
			AnnotationHelper annHelper) throws Exception {
		String ifc = null, unless = null, name = el.getLocalName();
		AnnotationHelper attrAnnHelper = null;
		final LanguageDefinition lookup = LanguageDefinition.lookup("xul/html");
		ComponentDefinition shadowDefinition = lookup.hasShadowDefinition(name) ? lookup.getShadowDefinition(name)
				: pgdef.getComponentDefinitionMap().get(name);
		final ShadowInfo compInfo = new ShadowInfo(parent, shadowDefinition, name,
				ConditionImpl.getInstance(ifc, unless));
		boolean annotated = false;
		boolean shouldIgnoreAnnotNamespace = annHelper.shouldIgnoreAnnotNamespace();
		for (final Attribute attr: el.getAttributeItems()) {
			final Namespace attrns = attr.getNamespace();
			final String attURI = attrns != null ? attrns.getURI() : "";
			final String attnm = attr.getLocalName();
			final String attval = attr.getValue();
			if ("if".equals(attnm)) {
				ifc = attval;
			} else if ("unless".equals(attnm)) {
				unless = attval;
			} else if (!shouldIgnoreAnnotNamespace && (LanguageDefinition.ANNOTATION_NAMESPACE.equals(attURI) || "annotation".equals(attURI))) {
				annotated = true;
				//ZK 6: annotation namespace mandates annotation
				if (attrAnnHelper == null)
					attrAnnHelper = new AnnotationHelper();
				applyAttrAnnot(attrAnnHelper, compInfo, attnm, attval.trim(), false, location(attr));
			} else {
				final String attPref = attrns != null ? attrns.getPrefix() : null;
				String attvaltrim;
				if (!"xmlns".equals(attPref) && !("xmlns".equals(attnm) && "".equals(attPref))
						&& !"http://www.w3.org/2001/XMLSchema-instance".equals(attURI)) {
					attvaltrim = modifyAttrValueIfSimplified(attnm, attval.trim(), parent);
					if ((attURI.length() == 0 || LanguageDefinition.ZK_NAMESPACE.endsWith(attURI))
							&& AnnotationHelper.isAnnotation(attvaltrim)) { // annotation
						if (attrAnnHelper == null)
							attrAnnHelper = new AnnotationHelper();
						applyAttrAnnot(attrAnnHelper, compInfo, attnm, attvaltrim, true, location(attr));
						//F80 - store subtree's binder annotation count
						Set<String> binderAnnotations = WebApps.getCurrent().getConfiguration().getBinderAnnotations();
						for (String annot : binderAnnotations) {
							if (attvaltrim.contains(annot)) {
								compInfo.enableBindingAnnotation();
								annotated = true;
								break;
							}
						}
					} else {
						compInfo.addProperty(attnm, attval, null);
						if (attrAnnHelper != null)
							attrAnnHelper.applyAnnotations(compInfo, attnm, true);
					}
				}
			}
		}

		compInfo.setCondition(ConditionImpl.getInstance(ifc, unless));
		annHelper.applyAnnotations(compInfo, null, true);

		Node root = el.getFirstChild();
		while (root != null && !(root instanceof Element)) {
			if (root instanceof Text) {
				if (!isEmpty(((Text) root).getText().trim()))
					break;
			}
			root = root.getNextSibling();
		}
		if (root != null) {
			String rn = root.getLocalName();
			if (!"template".equals(rn)) { // automatically create a template tag for it.
				Node item = root;
				while (item != null) {
					if ("template".equals(item.getLocalName()))
						throw new UiException(message(rn + " does not support between templates", (Item) root));
					item = item.getNextSibling();
				}
				TemplateInfo templateInfo = new TemplateInfo(compInfo, "", null, null, null);
				if (!shouldIgnoreAnnotNamespace && annotated) {
					annHelper.setIgnoreAnnotNamespace(true);
					templateInfo.setAnnotationNamespacedRoot(true);
				}
				return templateInfo;
			}
		}
		if (!shouldIgnoreAnnotNamespace && annotated) {
			annHelper.setIgnoreAnnotNamespace(true);
			compInfo.setAnnotationNamespacedRoot(true);
		}
		return compInfo;
	}

	private static TemplateInfo parseTemplate(NodeInfo parent, Element el, AnnotationHelper annHelper)
			throws Exception {
		if (annHelper.clear())
			log.warn(message("Annotations are ignored since <template> doesn't support them", el));
		if (el.getAttributeItem("forEach") != null)
			log.warn(message("forEach is ignored since <template> doesn't support it", el));

		String ifc = null, unless = null, name = null, src = null;
		final Map<String, String> params = new LinkedHashMap<String, String>(); //reserve the order
		for (final Attribute attr : el.getAttributeItems()) {
			final Namespace attrns = attr.getNamespace();
			final String attURI = attrns != null ? attrns.getURI() : "";
			final String attnm = attr.getLocalName();
			final String attval = attr.getValue();
			if ("name".equals(attnm)) {
				name = attval;
			} else if ("src".equals(attnm)) {
				src = attval;
			} else if ("if".equals(attnm)) {
				ifc = attval;
			} else if ("unless".equals(attnm)) {
				unless = attval;
			} else {
				final String attPref = attrns != null ? attrns.getPrefix() : null;
				if (!"xmlns".equals(attnm) && !"xml".equals(attnm) && !attURI.contains("w3.org")
						&& (attPref == null || (!"xmlns".equals(attPref) && !"xml".equals(attPref))))
					params.put(attnm, attval);
			}
		}
		if (name == null) {
			//throw new UiException(message("The name attribute required", el));
			name = "";
			// support "" an empty string for ZK 8.0.0 shadow element

		}
		return new TemplateInfo(parent, name, src, params, ConditionImpl.getInstance(ifc, unless));
	}

	private static ZkInfo parseZk(NodeInfo parent, Element el, AnnotationHelper annHelper) throws Exception {
		if (annHelper.clear())
			log.warn(message("Annotations are ignored since <zk> doesn't support them", el));

		final ZkInfo zi = new ZkInfo(parent, null);
		String ifc = null, unless = null, forEach = null, forEachBegin = null, forEachEnd = null, forEachStep = null;
		for (final Attribute attr : el.getAttributeItems()) {
			final Namespace attrns = attr.getNamespace();
			final String attURI = attrns != null ? attrns.getURI() : "";
			final String attnm = attr.getLocalName();
			final String attval = attr.getValue();
			if ("if".equals(attnm) || "when".equals(attnm)) {
				ifc = attval;
			} else if ("unless".equals(attnm)) {
				unless = attval;
			} else if ("forEach".equals(attnm)) {
				forEach = attval;
			} else if ("forEachStep".equals(attnm)) {
				forEachStep = attval;
			} else if ("forEachBegin".equals(attnm)) {
				forEachBegin = attval;
			} else if ("forEachEnd".equals(attnm)) {
				forEachEnd = attval;
			} else if ("switch".equals(attnm) || "choose".equals(attnm)) {
				if (isZkSwitch(parent))
					throw new UiException(message("<zk " + attnm + "> cannot be used in <zk switch/choose>", el));
				zi.setSwitch(attval);
			} else if ("case".equals(attnm)) {
				if (!isZkSwitch(parent))
					throw new UiException(message("<zk case> can be used only in <zk switch>", attr));
				zi.setCase(attval);
			} else {
				final String attPref = attrns != null ? attrns.getPrefix() : null;
				if (!"xmlns".equals(attnm) && !"xml".equals(attnm) && !attURI.contains("w3.org")
						&& (attPref == null || (!"xmlns".equals(attPref) && !"xml".equals(attPref))))
					warnWrongZkAttr(attr);
			}
		}
		zi.setCondition(ConditionImpl.getInstance(ifc, unless));
		zi.setForEach(forEach, forEachBegin, forEachEnd, forEachStep);
		return zi;
	}

	/** Whether a string is null or empty. */
	private static boolean isEmpty(String s) {
		return s == null || s.length() == 0;
	}

	/** Whether the name space belongs to the default language. */
	private static final boolean isDefaultNS(LanguageDefinition langdef, String pref, String uri) {
		return (!langdef.isNative() && "".equals(pref) && "".equals(uri)) || langdef.getNamespace().equals(uri);
	}

	/** Returns whether it is a Shadow element.
	 * @param pref namespace's prefix
	 * @param uri namespace's URI
	 * @param bNativeContent whether to ignore if URI not specified explicitly
	 */
	private static final boolean isShadowElement(LanguageDefinition langdef, PageDefinition pgdef, String nm,
			String pref, String uri, boolean bNativeContent) {
		// feature in 8.0.0, no need to check namespace, if any.
		if ("true".equalsIgnoreCase(Library.getProperty("org.zkoss.zk.namespace.tolerant", "false")))
			return langdef.hasShadowDefinition(nm) || (!"xul/html".equals(langdef.getName())
					&& LanguageDefinition.lookup("xul/html").hasShadowDefinition(nm));
		ComponentDefinition componentDefinition = pgdef.getComponentDefinitionMap().get(nm);
		// F80 - support another namespace
		if (componentDefinition instanceof ShadowDefinitionImpl || LanguageDefinition.SHADOW_NAMESPACE.equals(uri)
				|| LanguageDefinition.SHADOW_NAME.equals(uri))
			return true;
		if (isDefaultNS(langdef, pref, uri))
			return !bNativeContent && langdef.hasShadowDefinition(nm);
		return false;
	}

	/** Returns whether it is a ZK element.
	 * @param pref namespace's prefix
	 * @param uri namespace's URI
	 * @param bNativeContent whether to ignore if URI not specified explicitly
	 */
	private static final boolean isZkElement(LanguageDefinition langdef, String nm, String pref, String uri,
			boolean bNativeContent) {
		// feature in 8.0.0, no need to check namespace, if any.
		if ("true".equalsIgnoreCase(Library.getProperty("org.zkoss.zk.namespace.tolerant", "false")))
			return true;
		if (isDefaultNS(langdef, pref, uri))
			return !bNativeContent && !langdef.hasComponentDefinition(nm);
		return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri);
	}

	private static final boolean isZkElement(LanguageDefinition langdef, Element el, boolean bNativeContent) {
		final Namespace ns = el.getNamespace();
		return isZkElement(langdef, el.getLocalName(), ns != null ? ns.getPrefix() : "", ns != null ? ns.getURI() : "",
				bNativeContent);
	}

	private static final boolean isZkElement(LanguageDefinition langdef, String nm, String pref, String uri) {
		return isZkElement(langdef, nm, pref, uri, false);
	}

	/** Returns whether it is a ZK attribute (in a non-ZK element).
	 * @param bNativeContent whether to ignore if URI not specified explicitly
	 */
	private static final boolean isZkAttr(LanguageDefinition langdef, Namespace attrns, boolean bNativeContent) {
		//if native we will make sure URI is ZK or lang's namespace
		if ((bNativeContent || langdef.isNative()) && attrns != null && "".equals(attrns.getPrefix()))
			return false; //if navtive, "" means not ZK

		return isZkElementAttr(langdef, attrns);
	}

	private static final boolean isZkAttr(LanguageDefinition langdef, Namespace attrns) {
		return isZkAttr(langdef, attrns, false);
	}

	/** Similar to {@link #isZkAttr}, except it doesn't care isNative
	 * (i.e., we consider it as a ZK attribute unless a namespace
	 * other than ZK is specified.
	 */
	private static final boolean isZkElementAttr(LanguageDefinition langdef, Namespace attrns) {
		if (attrns == null //not possible; just in case
				|| "".equals(attrns.getPrefix())) //w3c attr: uri is "" if prefix ""
			return true;

		final String uri = attrns.getURI();
		return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri) || langdef.getNamespace().equals(uri);
	}

	/** Parse an attribute and adds it to the definition.
	 */
	private void addAttribute(ComponentInfo compInfo, Namespace attrns, String name, String value, ConditionImpl cond,
			org.zkoss.xml.Locator xl) throws Exception {
		if (Events.isValid(name)) {
			boolean bZkAttr = attrns == null;
			if (!bZkAttr) {
				final String uri = attrns.getURI();
				if (LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client".equals(uri)) {
					compInfo.addWidgetListener(name, value, cond);
					return;
				}
				if (LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "client/attribute".equals(uri)) {
					compInfo.addWidgetAttribute(name, value, cond);
					return;
				}

				final String pref = attrns.getPrefix();
				LanguageDefinition langdef = compInfo.getLanguageDefinition();
				ComponentDefinition compdef = compInfo.getComponentDefinition();

				if (langdef == null)
					bZkAttr = true;
				// refix ZK-2495: skip native components
				else if (isDefaultNS(langdef, pref, uri) && !compdef.isNative())
					bZkAttr = !langdef.isDynamicReservedAttributes("[event]");
				else
					bZkAttr = LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri);
			}
			if (bZkAttr) {
				checkZScriptEnabled(xl);
				final int lno = xl != null ? xl.getLineNumber() : 0;
				final ZScript zscript = ZScript.parseContent(value, lno);
				if (zscript.getLanguage() == null)
					zscript.setLanguage(compInfo.getPageDefinition().getZScriptLanguage());
				//resolve it here instead of runtime since createComponents
				compInfo.addEventHandler(name, zscript, cond);
				return; //done
			}
		} else {
			final String uri = attrns.getURI();
			if (LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client".equals(uri)) {
				if (name.length() == 0)
					throw new UiException(message("Client attribute name required", xl));
				if ("use".equals(name)) {
					if (cond != null)
						throw new UnsupportedOperationException(message("if and unless not allowed for w:use", xl));
					compInfo.setWidgetClass(value);
				} else {
					compInfo.addWidgetOverride(name, value, cond);
				}
				return;
			}
			if (LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "client/attribute".equals(uri)) {
				compInfo.addWidgetAttribute(name, value, cond);
				return;
			}
		}

		// ZK-4474
		if (attrns != null) {
			if (isClientAttrPrefixNamespace(attrns.getURI())) {
				compInfo.addWidgetAttribute(attrns.getPrefix() + ":" + name, value, cond);
				return;
			}
		}
		
		compInfo.addProperty(name, value, cond);
	}

	/** Adds the declared namespaces to the native info, if necessary.
	 */
	private static void addDeclaredNamespace(NativeInfo nativeInfo, Collection<Namespace> namespaces,
			LanguageDefinition langdef) {
		for (Namespace ns : namespaces) {
			final String uri = ns.getURI();
			boolean bNatPrefix = uri.startsWith(LanguageDefinition.NATIVE_NAMESPACE_PREFIX);
			// ZK-2494: Should also consider adding declaredNamespace inside a native component
			if (bNatPrefix || (!isZKNamespace(uri) && !langdef.getNamespace().equals(uri)))
				nativeInfo.addDeclaredNamespace(new Namespace(ns.getPrefix(),
						bNatPrefix ? uri.substring(LanguageDefinition.NATIVE_NAMESPACE_PREFIX.length()) : uri));
		}
	}

	/** Minimizes the native infos such that UiEngine creates
	 * the minimal number of components.
	 */
	private static void optimizeNativeInfos(NativeInfo compInfo) {
		//Optimize 1: merge to prolog, if the first children are
		//native and have no child
		for (Iterator it = compInfo.getChildren().iterator(); it.hasNext();) {
			final NodeInfo o = (NodeInfo) it.next();
			if (o instanceof NativeInfo) {
				final NativeInfo childInfo = (NativeInfo) o;
				if (!childInfo.getChildren().isEmpty())
					break;
				childInfo.setParentDirectly(null);

				// merge annotation if any
				AnnotationMap annotationMap = childInfo.getAnnotationMap();
				if (annotationMap != null) {
					for (String propName : annotationMap.getAnnotatedProperties()) {
						for (Annotation anno : annotationMap.getAnnotations(propName)) {
							compInfo.addAnnotation(propName, anno.getName(), anno.getAttributes(), anno.getLocation());
						}
					}
				}
			} else if (o instanceof ComponentInfo || o instanceof ZkInfo || o instanceof ShadowInfo) {
				break;
			}

			compInfo.addPrologChild(o);
			it.remove(); //detach it from the children list
		}

		//Optimize 2: merge to epilog if the last children, are
		//native and have no child
		int sz = compInfo.getChildren().size();
		if (sz >= 0) {
			final ListIterator it = compInfo.getChildren().listIterator(sz);
			while (it.hasPrevious()) {
				final Object o = it.previous();
				if (o instanceof NativeInfo) {
					final NativeInfo childInfo = (NativeInfo) o;
					if (!childInfo.getChildren().isEmpty()) {
						it.next();
						break;
					}
					childInfo.setParentDirectly(null);
				} else if (o instanceof ComponentInfo || o instanceof ZkInfo || o instanceof ShadowInfo) {
					it.next();
					break;
				}
			}
			while (it.hasNext()) {
				final NodeInfo o = (NodeInfo) it.next();
				compInfo.addEpilogChild(o);
				it.remove();
			}
		}

		//Optimize 3: merge to split child
		//If there is only one native child, we make it a split child and
		//make all its children (grand-children) up one level
		if (compInfo.getChildren().size() == 1 && compInfo.getSplitChild() == null /*just in case*/) {
			Iterator it = compInfo.getChildren().iterator();
			final Object o = it.next();
			if (o instanceof NativeInfo) {
				final NativeInfo childInfo = (NativeInfo) o;
				//FUTURE: enhance UiEngineImpl to handle split's forEach
				if (!childInfo.withForEach() && !childInfo.withCondition()) { // B65-ZK-1626
					childInfo.setParentDirectly(null);
					compInfo.setSplitChild(childInfo);
					it.remove();

					for (it = new ArrayList<NodeInfo>(childInfo.getChildren()).iterator(); it.hasNext();)
						compInfo.appendChild((NodeInfo) it.next());
				}
			}
		}
	}

	/** Returns the language definition of the specified name or namespace.
	 *  Return null if Definition not found
	 */
	private static LanguageDefinition langdefLookup(String name) {
		if (name == null || name.length() == 0) {
			return null;
		}

		try {
			return LanguageDefinition.lookup(name);
		} catch (DefinitionNotFoundException e) {
			return null;
		}
	}

	/** Returns whether the given uri is a known namespace in ZK or not
	 */
	private static boolean isZKNamespace(String uri) {
		return LanguageDefinition.ZK_NAMESPACE.equals(uri) || "zk".equals(uri)
				|| LanguageDefinition.NATIVE_NAMESPACE.equals(uri) || "native".equals(uri) || "annotation".equals(uri)
				|| LanguageDefinition.ANNOTATION_NAMESPACE.equals(uri) || "client".equals(uri)
				|| LanguageDefinition.CLIENT_NAMESPACE.equals(uri) || "client/attribute".equals(uri)
				|| LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri) || "xhtml".equals(uri)
				|| "http://www.w3.org/1999/xhtml/".equals(uri) || "zul".equals(uri)
				|| "http://www.zkoss.org/2005/zul/".equals(uri) || "xml".equals(uri)
				|| "http://www.zkoss.org/2007/xml".equals(uri) || isClientAttrPrefixNamespace(uri) || langdefLookup(uri) != null;
	}

	/** Returns whether the given uri is in native namespace or not
	 */
	private static boolean isNativeNamespace(String uri) {
		return "native".equals(uri) || LanguageDefinition.NATIVE_NAMESPACE.equals(uri);
	}

	/** Returns whether the given uri is in xml namespace or not
	 */
	private static boolean isXmlNamespace(String uri) {
		return "xml".equals(uri) || "http://www.zkoss.org/2007/xml".equals(uri);
	}

	/** Returns whether the given uri is in client namespace or not
	 */
	private static boolean isClientNamespace(String uri) {
		return "client".equals(uri) || LanguageDefinition.CLIENT_NAMESPACE.equals(uri);
	}

	/** Returns whether the given uri is in client attribute namespace or not
	 */
	private static boolean isClientAttrNamespace(String uri) {
		return "client/attribute".equals(uri) || LanguageDefinition.CLIENT_ATTRIBUTE_NAMESPACE.equals(uri);
	}

	/** Returns whether the given uri is in client raw attribute namespace or not
	 */
	private static boolean isClientAttrPrefixNamespace(String uri) {
		return LanguageDefinition.CLIENT_ATTRIBUTE_PREFIX_NAME.equals(uri) || LanguageDefinition.CLIENT_ATTRIBUTE_PREFIX_NAMESPACE.equals(uri);
	}

	//ZK-2632: Parser support disorder template tag
	private enum ParsingState {
		ROOT, FIRST, SECOND
	}
}
