package client.net.sf.saxon.ce.dom; import java.util.logging.Logger; import client.net.sf.saxon.ce.Configuration; import client.net.sf.saxon.ce.Controller; import client.net.sf.saxon.ce.Controller.APIcommand; import client.net.sf.saxon.ce.event.PipelineConfiguration; import client.net.sf.saxon.ce.event.Receiver; import client.net.sf.saxon.ce.lib.NamespaceConstant; import client.net.sf.saxon.ce.om.NamePool; import client.net.sf.saxon.ce.om.NamespaceBinding; import client.net.sf.saxon.ce.trans.XPathException; import client.net.sf.saxon.ce.value.Whitespace; import com.google.gwt.core.client.JavaScriptException; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Text; /** * DOMWriter is a Receiver that attaches the result tree to a specified Node in the HTML DOM Document */ public class HTMLWriter implements Receiver { private PipelineConfiguration pipe; private NamePool namePool; private Node currentNode; private Document document; private Node nextSibling; private int level = 0; private String systemId; private Node containerNode; private static Logger logger = Logger.getLogger("XSLT20Processor"); /** * Native Javascript method to create a namespaced element. Not available in GWT because * it's not supported in IE. But needed for SVG/mathML support * @param ns the namespace URI * @param name the local name * @return the constructed element or null if method not available */ private native Element createElementNS(Document doc, final String ns, final String name) /*-{ if (doc.createElementNS) { return doc.createElementNS(ns, name); } return null; }-*/; private native Node createProcessingInstruction(Document doc, final String target, final String data) /*-{ return doc.createProcessingInstruction(target, data); }-*/; private native Node createComment(Document doc, final String data) /*-{ return doc.createComment(data); }-*/; private static native boolean attNSSupported(Document doc) /*-{ return (typeof doc.createNode == "function" || typeof doc.createAttributeNS == "function"); }-*/; public static void setAttribute(Document doc, Element element, String name, String URI, String value, WriteMode wMode) { // fix for IE issue with colspan etc #1570 name = tableAttributeFix(name, wMode); if (attNSSupported(doc)) { setAttributeJs(doc,element,name,URI,value); } else { String prefix = name.substring(0, name.indexOf(":")); String x = "xmlns"; String nsDeclaraction = (prefix.length() == 0)? x : x + ":" + prefix; if (!element.hasAttribute(nsDeclaraction)) { addNamespace(element, prefix, URI); } element.setAttribute(name, value); setAttributeProps(element, name, value); } } public static String tableAttributeFix(String name, WriteMode wMode){ if (wMode != WriteMode.XML && Configuration.getIeVersion() > 0 && name.length() > 5){ if (name.equals("rowspan")){ name = "rowSpan"; } else if (name.equals("colspan")){ name = "colSpan"; } else if (name.equals("cellpadding")){ name = "cellPadding"; } else if (name.equals("cellppacing")){ name = "cellSpacing"; } } return name; } // This throws an exception in IE7 that must be handled, in IE, you can only create a namespace-qualified // attribute using the createNode method of the DOMDocument. /** * Creates an attribute with a namespace. * This throws an exception in IE7 that (it seems) must be handled by the caller, * in IE, you can only create a namespace-qualified attribute using the createNode method * of the DOMDocument. * */ public static native void setAttributeJs(Document doc, final Node element, final String name, final String URI, final String value) /*-{ var att; if (doc.createNode) { att = doc.createNode(2, name, URI); att.value = value; } else { att = doc.createAttributeNS(URI, name); att.nodeValue = value; } element.setAttributeNode(att); }-*/; /** * Set the pipelineConfiguration */ public void setPipelineConfiguration(PipelineConfiguration pipe) { this.pipe = pipe; namePool = pipe.getConfiguration().getNamePool(); } /** * Get the pipeline configuration used for this document */ public PipelineConfiguration getPipelineConfiguration() { return pipe; } /** * Set the System ID of the destination tree */ public void setSystemId(String systemId) { this.systemId = systemId; } /** * Get the system identifier that was set with setSystemId. * * @return The system identifier that was set with setSystemId, * or null if setSystemId was not called. */ public String getSystemId() { return systemId; } /** * Start of the document. */ public void open () {} /** * End of the document. */ public void close () {} /** * Start of a document node. */ public void startDocument() throws XPathException { // not required - setNode is called instead: //document = XMLDOM.createDocument(); } /** * Notify the end of a document node */ public void endDocument() throws XPathException {} /** * Start of an element. */ public void startElement(int nameCode, int properties) throws XPathException { String localName = namePool.getLocalName(nameCode); String prefix = namePool.getPrefix(nameCode); String uri = namePool.getURI(nameCode); // TODO: For XML Writer it should write prefixes in a // way compliant with the XSLT2.0 specification - using xsl:output attributes Element element = null; if (uri != null && !uri.isEmpty()) { if(mode == WriteMode.XML && !prefix.equals("")) { element = createElementNS(document, uri, prefix+":"+localName); } else { // no svg specific prefix now used, for compliance with HTML5 element = createElementNS(document, uri, localName); } } // if there's no namespace - or no namespace support if (element == null) { element = document.createElement(localName); } // special case for html element: write to the document node Controller controller = pipe.getController(); if (controller != null && controller.getApiCommand() == APIcommand.UPDATE_HTML && (localName.equals("html") || localName.equals("head") || localName.equals("body"))) { if (localName.equals("html")){ element = (Element)document.getFirstChild(); } else { element = (Element)document.getElementsByTagName(localName.toUpperCase()).getItem(0); NodeList<Node> nodes = element.getChildNodes(); for (int n = 0; n < nodes.getLength(); n++) { Node node = nodes.getItem(n); node.removeFromParent(); } } currentNode = element; level++; return; } if (nextSibling != null && level == 0) { currentNode.insertBefore(element, nextSibling); } else { try { currentNode.appendChild(element); } catch(JavaScriptException err) { if(uri.equals(NamespaceConstant.IXSL)) { XPathException xpe = new XPathException("Error on adding IXSL element to the DOM, the IXSL namespace should be added to the 'extension-element-prefixes' list."); throw(xpe); } else { throw(new XPathException(err.getMessage())); } } catch(Exception exc) { XPathException xpe = new XPathException("Error on startElement in HTMLWriter for element '" + localName + "': " + exc.getMessage()); throw(xpe); } } currentNode = element; level++; } private static void addNamespace(Element element, String prefix, String uri) { String attName = (prefix.isEmpty() ? "xmlns" : "xmlns:" + prefix); element.setAttribute(attName, uri); } public void namespace (NamespaceBinding nsBinding, int properties) throws XPathException { if(mode == WriteMode.XML) { String prefix = nsBinding.getPrefix(); String uri = nsBinding.getURI(); Element element = (Element)currentNode; if (!(uri.equals(NamespaceConstant.XML))) { addNamespace(element, prefix, uri); } } } public void attribute(int nameCode, CharSequence value) throws XPathException { String localName = namePool.getLocalName(nameCode); String uri = namePool.getURI(nameCode); String val = value.toString(); Element element = (Element)currentNode; // must be HTML write mode if (mode != WriteMode.XML && NamespaceConstant.HTML_PROP.equals(uri)) { element.setPropertyString(localName, val); } else if (mode != WriteMode.XML && NamespaceConstant.HTML_STYLE_PROP.equals(uri)) { // if localName starts with '_-' then remove the underscore e.g _-webkit-transition if(localName.length() > 1 && localName.charAt(0) == '_' && localName.charAt(1) == '-') { localName = localName.substring(1); } localName = HTMLWriter.getCamelCaseName(localName); element.getStyle().setProperty(localName, val); } else if (uri != null && !uri.isEmpty()){ String fullname = namePool.getDisplayName(nameCode); setAttribute(document, element, fullname, uri, val, mode); } else { localName = tableAttributeFix(localName, mode); element.setAttribute(localName, val); setAttributeProps(element, localName, val); } } /** * Method for backward compatibility with IE8 and previous where * properties and attributes were handled separately */ public static void setAttributePropsOriginal(Element element, String localName, String val){ if (Configuration.getIeVersion() > 0 && Configuration.getIeVersion() < 9) { if (localName.length() == 5) { if (localName.equals("style")) { // In IE, setting the style attribute dynamically has no effect on the individual style properties, // and does not affect the rendition of the element. So we parse out the content of the attribute, // and use it to set the individual properties. if (hasStyle(element)) { setStyleProperties(element, val); } } else if (localName.equals("class")) { setClass(element, val); } else if (localName.equals("title")) { setTitle(element, val); // } else if (localName.equals("align") || localName.equals("width")) { // setElementProperty(element, localName, val); } else { setElementProperty(element, localName, val); } } else if (localName.length() == 2 && localName.equals("id")) { setId(element, val); // } else if (localName.length() == 7 && localName.equals("colSpan") || localName.equals("rowSpan") ) { // setElementProperty(element, localName, val); } else { setElementProperty(element, localName, val); } } } /** * following setElementProperty method call doesn't work consistently * because in IE some element are initially undefined? */ public static void setAttributeProps(Element element, String localName, String val){ if (Configuration.getIeVersion() > 0 && Configuration.getIeVersion() < 9) { if (localName.equals("style")) { if (hasStyle(element)) { setStyleProperties(element, val); } } else { localName = (localName == "class")? "className" : localName; try { setElementProperty(element, localName, val); } catch(Exception e) { // some IE8 properties exist but appear to be read-only logger.warning("Unable to set '" + localName + "' property for element."); } } } } private static native boolean hasStyle(Element element) /*-{ return (typeof element.style !== "undefined"); }-*/; private static native boolean setClass(Element element, String value) /*-{ if (typeof element.className !== "undefined") { element.className = value; } }-*/; private static native boolean setId(Element element, String value) /*-{ if (typeof element.id !== "undefined") { element.id = value; } }-*/; private static native boolean setTitle(Element element, String value) /*-{ if (typeof element.title !== "undefined") { element.title = value; } }-*/; private static native void setElementProperty(Element element, String name, String value) /*-{ if (typeof element[name] !== "undefined") { element[name] = value; } }-*/; /** * Parse the value of the style attribute and use it to set individual properties of the style object * @param element the element whose style properties are to be updated * @param styleAttribute the raw value of the style attribute * @throws XPathException */ public static void setStyleProperties(Element element, String styleAttribute) { int semi = styleAttribute.indexOf(';'); String first = (semi < 0 ? styleAttribute : styleAttribute.substring(0, semi)); int colon = first.indexOf(':'); if (colon > 0 && colon < first.length() - 1) { String prop = first.substring(0, colon).trim(); // Turn the style name into camelCase prop = getCamelCaseName(prop); String value = first.substring(colon+1).trim(); try { element.getStyle().setProperty(prop, value); } // IE throws illegal argument exception if property name is // not valid - ignore exception for consistency catch (JavaScriptException jex) {} } if (semi > 0 && semi < styleAttribute.length() - 2) { setStyleProperties(element, styleAttribute.substring(semi+1)); } } public static String getCamelCaseName(String prop) { while (prop.contains("-")) { int h = prop.indexOf('-'); if (h > 0) { // preserve first char String p = prop.substring(0, h) + Character.toUpperCase(prop.charAt(h+1)); if (h+2 < prop.length()) { p += prop.substring(h+2); } prop = p; } } return prop; } public void startContent() throws XPathException {} /** * End of an element. */ public void endElement () throws XPathException { currentNode = currentNode.getParentNode(); level--; } /** * Character data. */ public void characters(CharSequence chars) throws XPathException { if (level == 0 && nextSibling == null && Whitespace.isWhite(chars)) { return; // no action for top-level whitespace } try { Text text = document.createTextNode(chars.toString()); if (nextSibling != null && level == 0) { currentNode.insertBefore(text, nextSibling); } else { currentNode.appendChild(text); } } catch(Exception e) { String desc = (nextSibling != null && level == 0) ? "inserting" : "appending"; throw(new XPathException("DOM error " + desc + " text node with value: '" + chars.toString() + "' to node with name: " + currentNode.getNodeName())); } } /** * Handle a processing instruction. */ public void processingInstruction(String target, CharSequence data) throws XPathException { // Processing instructions not in HTML DOM - but added for XML DOM // TODO: Note that Node is a GWT HTML DOM object so an exception may be raised // by appending the wrong node type! if (mode == WriteMode.XML) { JavaScriptObject pi = createProcessingInstruction(document, target, data.toString()); addNode(pi, "processing-instruction"); } } /** * Handle a comment. */ public void comment(CharSequence chars) throws XPathException { // Added for XML compatibility if (mode == WriteMode.XML) { JavaScriptObject comment = createComment(document, chars.toString()); addNode(comment, "comment"); } } public void addNode(JavaScriptObject newNode, String nodeType) throws XPathException { try { if (nextSibling != null && level == 0) { insertBefore(nextSibling, newNode); } else { appendChild(currentNode, newNode); } } catch(Exception e) { String desc = (nextSibling != null && level == 0) ? "inserting" : "appending"; throw(new XPathException("DOM error " + desc + " " + nodeType + " node to node with name: " + currentNode.getNodeName())); } } public final native JavaScriptObject appendChild(JavaScriptObject parent, JavaScriptObject newChild) /*-{ return parent.appendChild(newChild); }-*/; public final native JavaScriptObject insertBefore(JavaScriptObject targetNode, JavaScriptObject newNode) /*-{ return targetNode.insertBefore(newNode); }-*/; /** * Set the attachment point for the new subtree * @param node the node to which the new subtree will be attached */ public enum WriteMode { NONE, XML, HTML } private WriteMode mode = WriteMode.NONE; public void setNode (Node node) { if (node == null) { return; } currentNode = node; if (node.getNodeType() == Node.DOCUMENT_NODE) { document = (Document)node; } else { document = currentNode.getOwnerDocument(); } if (mode == WriteMode.NONE) { Controller.APIcommand cmd = pipe.getController().getApiCommand(); mode = (cmd == APIcommand.TRANSFORM_TO_DOCUMENT || cmd == APIcommand.TRANSFORM_TO_FRAGMENT)? WriteMode.XML : WriteMode.HTML; } } public Node getNode() { // though this is changed by startElement it's reset by endElement and therefore, // when called after close, should always return the initial container node // - the document node or the documentFragment node. return currentNode; } /** * Set next sibling * @param nextSibling the node, which must be a child of the attachment point, before which the new subtree * will be created. If this is null the new subtree will be added after any existing children of the * attachment point. */ public void setNextSibling(Node nextSibling) { this.nextSibling = nextSibling; } } // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.