All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xwiki.xml.html.HTMLUtils Maven / Gradle / Ivy

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.xml.html;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.jdom.DocType;
import org.jdom.Element;
import org.jdom.input.DOMBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * HTML Utility methods.
 *
 * @version $Id: cc7ab52deafe00eed701a7c3e06bfbc67791776f $
 * @since 1.8.3
 */
// TODO: Create a separate class for each HTML version (XHTML 1.0, HTML5, etc...)
public final class HTMLUtils
{
    /**
     * In HTML5, some elements must be expanded (for example {@code } instead of {@code }), and
     * some others must not (for example {@code 
} instead of {@code

}. Thus for the list of elements * below we need special handling (not expanding). */ private static final List OMIT_ELEMENT_EXPANDING_SET = Arrays.asList( "area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param"); /** * JDOM's XMLOutputter class converts reserved XML characters ({@code <, >, ' , &, \r and \n}) into their entity * format {@code <, > ' & and \r\n}. However since we're using HTML Cleaner * (http://htmlcleaner.sourceforge.net/) and since it's buggy for character escapes we have turned off character * escaping for it and thus we need to perform selective escaping here. *

* Moreover, since we support HTML5, we need to * expand empty elements on some elements and not on the others. For example: {@code } is valid * meanwhile: *

{@code
     * 
*
}
* is not. See {@code OMIT_ELEMENT_EXPANDING_SET} for the list of elements to not expand. */ // TODO: Remove the complex escaping code when SF HTML Cleaner will do proper escaping public static class XWikiXMLOutputter extends XMLOutputter { /** * Regex to recognize a XML Entity. */ private static final Pattern ENTITY = Pattern.compile("&[a-z]+;|&#[0-9a-zA-Z]+;"); /** * Ampersand character. */ private static final String AMPERSAND = "&"; private static final String[] REPLACE_ELEMENTS_SEARCH = new String[] { "<", ">" }; private static final String[] REPLACE_ELEMENTS_RESULT = new String[] { "<", ">" }; /** * Whether to omit the document type when printing the W3C Document or not. */ private boolean omitDocType; /** * @param format the JDOM class used to control output formats, see {@link org.jdom.output.Format} * @param omitDocType if true then omit the document type when printing the W3C Document * @see XMLOutputter#XMLOutputter(Format) */ public XWikiXMLOutputter(Format format, boolean omitDocType) { super(format); this.omitDocType = omitDocType; } @Override public String escapeElementEntities(String text) { if (text.length() == 0) { return text; } String result; int pos1 = text.indexOf(" -1) { int pos2 = text.indexOf("]]>", pos1 + 9); if (pos2 + 3 == text.length()) { return text; } result = escapeElementEntities(text.substring(0, pos1)); if (pos2 + 3 == text.length()) { result = result + text.substring(pos1); } else { result = result + text.substring(pos1, pos2 + 3) + escapeElementEntities(text.substring(pos2 + 3)); } } else { result = escapeAmpersand(text); StringUtils.replaceEach(text, REPLACE_ELEMENTS_SEARCH, REPLACE_ELEMENTS_RESULT); } return result; } @Override public String escapeAttributeEntities(String text) { String result = escapeElementEntities(text); // Attribute values must have quotes escaped since attributes are defined with quotes... result = StringUtils.replace(result, "\"", """); return result; } /** * Escape ampersand when it's not defining an entity. * * @param text the text to escape * @return the escaped text */ private String escapeAmpersand(String text) { StringBuilder buffer = new StringBuilder(text); // find all occurrences of & int pos = buffer.indexOf(AMPERSAND); while (pos > -1 && pos < buffer.length()) { // Check if the & is an entity Matcher matcher = ENTITY.matcher(buffer.substring(pos)); if (matcher.lookingAt()) { // We've found an entity, don't do anything, just skip it pos = pos + matcher.end() - matcher.start(); } else { // No entity, escape the & buffer.replace(pos, pos + 1, "&"); pos += 5; } pos = buffer.indexOf(AMPERSAND, pos); } return buffer.toString(); } @Override protected void printDocType(Writer out, DocType docType) throws IOException { if (!this.omitDocType) { super.printDocType(out, docType); } } @Override protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) throws IOException { // We override the code from the super class to not expand some empty elements. boolean currentFormatPolicy = currentFormat.getExpandEmptyElements(); try { String elementName = element.getName(); for (String name : OMIT_ELEMENT_EXPANDING_SET) { if (name.equals(elementName)) { // We do not expand this empty element currentFormat.setExpandEmptyElements(false); break; } } // Call the method from the super class super.printElement(out, element, level, namespaces); } finally { // Reset the format currentFormat.setExpandEmptyElements(currentFormatPolicy); } } } /** * Private constructor since this is a utility class that shouldn't be instantiated (all methods are static). */ private HTMLUtils() { // Nothing to do } /** * @param document the W3C Document to transform into a String * @return the XML as a String */ public static String toString(Document document) { return HTMLUtils.toString(document, false, false); } /** * @param document the W3C Document to transform into a String * @param omitDeclaration whether the XML declaration should be printed or not * @param omitDoctype whether the document type should be printed or not * @return the XML as a String */ public static String toString(Document document, boolean omitDeclaration, boolean omitDoctype) { // Note: We don't use javax.xml.transform.Transformer since it prints our valid XHTML as HTML which is not // XHTML compliant. For example it transforms our "
" into "
. DOMBuilder builder = new DOMBuilder(); org.jdom.Document jdomDoc = builder.build(document); Format format = Format.getRawFormat(); // Force newlines to use \n since otherwise the default is \n\r. // See http://www.jdom.org/docs/apidocs/org/jdom/output/Format.html#setLineSeparator(java.lang.String) format.setLineSeparator("\n"); // Make sure all elements are expanded so that they can also be rendered fine in browsers that only support // HTML. format.setExpandEmptyElements(true); format.setOmitDeclaration(omitDeclaration); XMLOutputter outputter = new XWikiXMLOutputter(format, omitDoctype); String result = outputter.outputString(jdomDoc); return result; } /** * Strip the HTML envelope if it exists. Precisely this means removing the head tag and move all tags in the body * tag directly under the html element. This is useful for example if you wish to insert an HTML fragment into an * existing HTML page. * * @param document the w3c Document to strip */ public static void stripHTMLEnvelope(Document document) { org.w3c.dom.Element root = document.getDocumentElement(); if (root.getNodeName().equalsIgnoreCase(HTMLConstants.TAG_HTML)) { // Look for a head element below the root element and for a body element Node bodyNode = null; Node headNode = null; NodeList nodes = root.getChildNodes(); for (int i = 0; i < nodes.getLength(); i++) { Node node = nodes.item(i); if (node.getNodeName().equalsIgnoreCase(HTMLConstants.TAG_HEAD)) { headNode = node; } else if (node.getNodeName().equalsIgnoreCase(HTMLConstants.TAG_BODY)) { bodyNode = node; } } if (headNode != null) { root.removeChild(headNode); } if (bodyNode != null) { // Move all children of body node under the root element NodeList bodyChildrenNodes = bodyNode.getChildNodes(); while (bodyChildrenNodes.getLength() > 0) { root.insertBefore(bodyChildrenNodes.item(0), null); } root.removeChild(bodyNode); } } } /** * Remove the first element inside a parent element and copy the element's children in the parent. * * @param document the w3c document from which to remove the top level paragraph * @param parentTagName the name of the parent tag to look under * @param elementTagName the name of the first element to remove */ public static void stripFirstElementInside(Document document, String parentTagName, String elementTagName) { NodeList parentNodes = document.getElementsByTagName(parentTagName); if (parentNodes.getLength() > 0) { Node parentNode = parentNodes.item(0); // Look for a p element below the first parent element Node pNode = parentNode.getFirstChild(); if (elementTagName.equalsIgnoreCase(pNode.getNodeName())) { // Move all children of p node under the root element NodeList pChildrenNodes = pNode.getChildNodes(); while (pChildrenNodes.getLength() > 0) { parentNode.insertBefore(pChildrenNodes.item(0), null); } parentNode.removeChild(pNode); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy