/*
* Copyright 2011 cruxframework.org.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.cruxframework.crux.core.utils;
import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.cruxframework.crux.core.rebind.screen.widget.ViewFactoryCreator;
import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
*
* Helper class for HTML generation, according with {@link http://dev.w3.org/html5/spec/syntax.html#elements-0}
* @author Thiago da Rosa de Bustamante
*
*/
public class HTMLUtils
{
private static Set<String> rawTextElements = new HashSet<String>();
private static Set<String> rcDataElements = new HashSet<String>();
private static Set<String> voidElements = new HashSet<String>();
static
{
voidElements.addAll(Arrays.asList(new String[]{
"area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"
}));
rawTextElements.addAll(Arrays.asList(new String[]{"style", "script"}));
rcDataElements.addAll(Arrays.asList(new String[]{"textarea", "title"}));
}
/**
* @param document
*/
private HTMLUtils()
{
}
/**
* @param s
* @return
*/
public static String escapeHTML(String s)
{
return escapeHTML(s, true);
}
/**
* @param s
* @return
*/
public static String escapeHTMLAttribute(String s)
{
return escapeHTML(s, false);
}
/**
* @param s
* @return
*/
public static String escapeJavascriptString(String s, boolean escapeXML)
{
StringBuilder sb = new StringBuilder();
int n = s.length();
for (int i = 0; i < n; i++)
{
char c = s.charAt(i);
switch (c)
{
case '\\': sb.append("\\\\"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
case '<':
if (escapeXML) sb.append("<");
else sb.append("<");
break;
case '>':
if (escapeXML) sb.append(">");
else sb.append(">");
break;
default: sb.append(c); break;
}
}
s = sb.toString().replace(""", "\\\"");
return s;
}
/**
* @param viewId
* @param device
* @param nativeControllers
* @param nativeBindings
* @param node
* @param out
* @throws IOException
*/
public static void write(String viewId, String device, StringBuilder nativeControllers,
StringBuilder nativeBindings, Node node, Writer out) throws IOException
{
write(viewId, device, nativeControllers, nativeBindings, node, out, false);
}
/**
* @param viewId
* @param device
* @param nativeControllers
* @param nativeBindings
* @param node
* @param out
* @param indentOutput
* @throws IOException
*/
public static void write(String viewId, String device, StringBuilder nativeControllers, StringBuilder nativeBindings,
Node node, Writer out, boolean indentOutput) throws IOException
{
if (node.getNodeType() == Node.ELEMENT_NODE)
{
String name = ((Element)node).getNodeName().toLowerCase();
out.write("<");
out.write(name);
writeAttributes(viewId, device, nativeControllers, nativeBindings, node, out);
if (voidElements.contains(name))
{
out.write("/>");
}
else
{
out.write(">");
NodeList children = node.getChildNodes();
if (children != null)
{
for (int i=0; i< children.getLength(); i++)
{
Node child = children.item(i);
if (rawTextElements.contains(name))
{
writeRawText(child, out);
}
else if (rcDataElements.contains(name))
{
writeRcData(child, out);
}
else
{
write(viewId, device, nativeControllers, nativeBindings, child, out, indentOutput);
}
}
}
out.write("</");
out.write(name);
out.write(">");
}
}
else if (node.getNodeType() == Node.TEXT_NODE)
{
if (indentOutput)
{
out.write(escapeIndentedHTML(node.getNodeValue()));
}
else
{
out.write(escapeHTML(node.getNodeValue()));
}
}
}
/**
* @param viewId
* @param device
* @param nativeControllers
* @param nativeBindings
* @param node
* @param out
* @throws IOException
*/
public static void writeAttributes(String viewId, String device, StringBuilder nativeControllers,
StringBuilder nativeBindings, Node node, Writer out) throws IOException
{
NamedNodeMap attributes = node.getAttributes();
Node idAttr = node.getAttributes().getNamedItem("id");
String id = null;
if (idAttr != null)
{
id = idAttr.getNodeValue();
}
for (int i=0; i<attributes.getLength(); i++)
{
Node attribute = attributes.item(i);
String name = attribute.getNodeName();
String nameLowerCase = name.toLowerCase();
if (!nameLowerCase.startsWith("xmlns"))
{
String attributeValue = attribute.getNodeValue();
if (!writeAttributeWithController(viewId, device, nativeControllers, out, name, nameLowerCase, attributeValue))
{
AttributeDataBindingStatus status = processAttributeDataBinding(viewId, device, nativeBindings, out, name, nameLowerCase, attributeValue, id);
if (!status.status)
{
out.write(" "+name+"=\""+escapeHTMLAttribute(attributeValue)+"\"");
}
else
{
id = status.id;
}
}
}
}
}
/**
* @param child
* @param out
* @throws IOException
* @throws DOMException
*/
public static void writeRawText(Node child, Writer out) throws DOMException, IOException
{
boolean isCDATA = child instanceof CDATASection;
if (isCDATA)
{
out.write("<![CDATA[");
}
out.write(child.getNodeValue());
if (isCDATA)
{
out.write("]]>");
}
}
/**
* @param child
* @param out
* @throws DOMException
* @throws IOException
*/
public static void writeRcData(Node child, Writer out) throws DOMException, IOException
{
out.write(child.getNodeValue());
}
/**
* @param s
* @return
*/
private static String escapeHTML(String s, boolean normalizeSpaces)
{
StringBuilder sb = new StringBuilder();
s = s.replaceAll("
", " ");
s = s.replaceAll("
", " ");
int n = s.length();
boolean lastIsSpace = false;
for (int i = 0; i < n; i++)
{
char c = s.charAt(i);
if (c != ' ' && c != '\n' && c != '\r' && c != '\t')
{
lastIsSpace = false;
}
switch (c)
{
case '\n':
case '\r':
case '\t':
case ' ':
if (normalizeSpaces)
{
if (!lastIsSpace)
{
sb.append(" ");
lastIsSpace = true;
}
}
else
{
sb.append(" ");
}
break;
case '&': sb.append("&"); break;
case '"': sb.append("""); break;
case '\'': sb.append("'"); break;
case '<': sb.append("<"); break;
case '>': sb.append(">"); break;
default: sb.append(c); break;
}
}
return (sb.toString());
}
/**
* @param s
* @return
*/
private static String escapeIndentedHTML(String s)
{
StringBuilder sb = new StringBuilder();
int n = s.length();
for (int i = 0; i < n; i++)
{
char c = s.charAt(i);
switch (c)
{
case '&': sb.append("&"); break;
case '"': sb.append("""); break;
case '\'': sb.append("'"); break;
case '<': sb.append("<"); break;
case '>': sb.append(">"); break;
default: sb.append(c); break;
}
}
return (sb.toString());
}
private static boolean writeAttributeWithController(String viewId, String device, StringBuilder nativeControllers, Writer out,
String attributeName, String attributeNameLower, String attributeValue) throws IOException
{
if (nativeControllers != null && attributeNameLower.startsWith("on")
&& RegexpPatterns.REGEXP_CRUX_CONTROLLER_CALL.matcher(attributeValue.trim()).matches())
{
String controllerCall = attributeValue.trim();
String controllerNativeCall = controllerCall.substring(1, controllerCall.length()-1);
String methodCall = viewId.replaceAll("\\W", "_") + "_" + device + "_" + controllerNativeCall.replace('.', '_');
out.write(" "+attributeName+"=\""+methodCall+"(event)"+"\"");
if (nativeControllers.length() > 1)
{
nativeControllers.append(",");
}
nativeControllers.append("{\"method\": \""+methodCall+"\","
+ "\"controllerCall\" : \""+controllerCall+"\"}");
return true;
}
return false;
}
private static AttributeDataBindingStatus processAttributeDataBinding(String viewId, String device, StringBuilder nativeBindings, Writer out,
String attributeName, String attributeNameLower, String attributeValue, String id) throws IOException
{
if (!attributeNameLower.startsWith("on")
&& RegexpPatterns.REGEXP_CRUX_OBJECT_DATA_BINDING.matcher(attributeValue.trim()).find())
{
String dataBinding = attributeValue.trim();
if (id == null)
{
id = ViewFactoryCreator.createVariableName(viewId.replaceAll("\\W", "_") + "_" + device + "_elem");
out.write(" id=\""+escapeHTMLAttribute(id)+"\"");
}
if (nativeBindings != null)
{
if (nativeBindings.length() > 1)
{
nativeBindings.append(",");
}
nativeBindings.append("{\"elementId\": \""+id+"\","
+ "\"dataBinding\" : \""+dataBinding+"\","
+ "\"attributeName\" : \""+attributeName+"\"}");
}
return new AttributeDataBindingStatus(id, true);
}
return new AttributeDataBindingStatus(id, false);
}
private static class AttributeDataBindingStatus
{
private AttributeDataBindingStatus(String id, boolean status)
{
this.id = id;
this.status = status;
}
private boolean status;
private String id;
}
}