001package org.hl7.fhir.utilities.xhtml; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031 032 033 034import java.io.FileOutputStream; 035import java.io.IOException; 036import java.io.OutputStream; 037import java.io.OutputStreamWriter; 038import java.io.StringWriter; 039import java.io.Writer; 040 041import org.hl7.fhir.utilities.Utilities; 042import org.hl7.fhir.utilities.xml.IXMLWriter; 043import org.w3c.dom.Element; 044 045public class XhtmlComposer { 046 047 public static final String XHTML_NS = "http://www.w3.org/1999/xhtml"; 048 private boolean pretty; 049 private boolean xml; 050 051 public static final boolean XML = true; 052 public static final boolean HTML = false; 053 054 public XhtmlComposer(boolean xml, boolean pretty) { 055 super(); 056 this.pretty = pretty; 057 this.xml = xml; 058 } 059 060 public XhtmlComposer(boolean xml) { 061 super(); 062 this.pretty = false; 063 this.xml = xml; 064 } 065 066 private Writer dst; 067 068 public String compose(XhtmlDocument doc) throws IOException { 069 StringWriter sdst = new StringWriter(); 070 dst = sdst; 071 composeDoc(doc); 072 return sdst.toString(); 073 } 074 075 public String compose(XhtmlNode node) throws IOException { 076 StringWriter sdst = new StringWriter(); 077 dst = sdst; 078 writeNode("", node, false); 079 return sdst.toString(); 080 } 081 082 public void compose(OutputStream stream, XhtmlDocument doc) throws IOException { 083 byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF }; 084 stream.write(bom); 085 dst = new OutputStreamWriter(stream, "UTF-8"); 086 composeDoc(doc); 087 dst.flush(); 088 } 089 090 private void composeDoc(XhtmlDocument doc) throws IOException { 091 // headers.... 092// dst.append("<html>" + (pretty ? "\r\n" : "")); 093 for (XhtmlNode c : doc.getChildNodes()) { 094 writeNode(" ", c, false); 095 } 096// dst.append("</html>" + (pretty ? "\r\n" : "")); 097 } 098 099 private void writeNode(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException { 100 if (node.getNodeType() == NodeType.Comment) { 101 writeComment(indent, node, noPrettyOverride); 102 } else if (node.getNodeType() == NodeType.DocType) { 103 writeDocType(node); 104 } else if (node.getNodeType() == NodeType.Instruction) { 105 writeInstruction(node); 106 } else if (node.getNodeType() == NodeType.Element) { 107 writeElement(indent, node, noPrettyOverride); 108 } else if (node.getNodeType() == NodeType.Document) { 109 writeDocument(indent, node); 110 } else if (node.getNodeType() == NodeType.Text) { 111 writeText(node); 112 } else if (node.getNodeType() == null) { 113 throw new IOException("Null node type"); 114 } else { 115 throw new IOException("Unknown node type: "+node.getNodeType().toString()); 116 } 117 } 118 119 private void writeText(XhtmlNode node) throws IOException { 120 for (char c : node.getContent().toCharArray()) 121 { 122 if (c == '&') { 123 dst.append("&"); 124 } else if (c == '<') { 125 dst.append("<"); 126 } else if (c == '>') { 127 dst.append(">"); 128 } else if (xml) { 129 if (c == '"') 130 dst.append("""); 131 else 132 dst.append(c); 133 } else { 134 if (c == XhtmlNode.NBSP.charAt(0)) 135 dst.append(" "); 136 else if (c == (char) 0xA7) 137 dst.append("§"); 138 else if (c == (char) 169) 139 dst.append("©"); 140 else if (c == (char) 8482) 141 dst.append("™"); 142 else if (c == (char) 956) 143 dst.append("μ"); 144 else if (c == (char) 174) 145 dst.append("®"); 146 else 147 dst.append(c); 148 } 149 } 150 } 151 152 private void writeComment(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException { 153 dst.append(indent + "<!-- " + node.getContent().trim() + " -->" + (pretty && !noPrettyOverride ? "\r\n" : "")); 154} 155 156 private void writeDocType(XhtmlNode node) throws IOException { 157 dst.append("<!" + node.getContent() + ">\r\n"); 158} 159 160 private void writeInstruction(XhtmlNode node) throws IOException { 161 dst.append("<?" + node.getContent() + "?>\r\n"); 162} 163 164 private String escapeHtml(String s) { 165 if (s == null || s.equals("")) 166 return null; 167 StringBuilder b = new StringBuilder(); 168 for (char c : s.toCharArray()) 169 if (c == '<') 170 b.append("<"); 171 else if (c == '>') 172 b.append(">"); 173 else if (c == '"') 174 b.append("""); 175 else if (c == '&') 176 b.append("&"); 177 else 178 b.append(c); 179 return b.toString(); 180 } 181 182 private String attributes(XhtmlNode node) { 183 StringBuilder s = new StringBuilder(); 184 for (String n : node.getAttributes().keySet()) 185 s.append(" " + n + "=\"" + escapeHtml(node.getAttributes().get(n)) + "\""); 186 return s.toString(); 187 } 188 189 private void writeElement(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException { 190 if (!pretty || noPrettyOverride) 191 indent = ""; 192 193 // html self closing tags: http://xahlee.info/js/html5_non-closing_tag.html 194 boolean concise = node.getChildNodes().size() == 0; 195 if (node.hasEmptyExpanded() && node.getEmptyExpanded()) { 196 concise = false; 197 } 198 if (!xml && Utilities.existsInList(node.getName(), "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr")) { 199 concise = true; 200 } 201 202 if (concise) 203 dst.append(indent + "<" + node.getName() + attributes(node) + "/>" + (pretty && !noPrettyOverride ? "\r\n" : "")); 204 else { 205 boolean act = node.allChildrenAreText(); 206 if (act || !pretty || noPrettyOverride) 207 dst.append(indent + "<" + node.getName() + attributes(node)+">"); 208 else 209 dst.append(indent + "<" + node.getName() + attributes(node) + ">\r\n"); 210 if (node.getName() == "head" && node.getElement("meta") == null) 211 dst.append(indent + " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>" + (pretty && !noPrettyOverride ? "\r\n" : "")); 212 213 214 for (XhtmlNode c : node.getChildNodes()) 215 writeNode(indent + " ", c, noPrettyOverride || node.isNoPretty()); 216 if (act) 217 dst.append("</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : "")); 218 else if (node.getChildNodes().get(node.getChildNodes().size() - 1).getNodeType() == NodeType.Text) 219 dst.append((pretty && !noPrettyOverride ? "\r\n"+ indent : "") + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : "")); 220 else 221 dst.append(indent + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : "")); 222 } 223 } 224 225 private void writeDocument(String indent, XhtmlNode node) throws IOException { 226 indent = ""; 227 for (XhtmlNode c : node.getChildNodes()) 228 writeNode(indent, c, false); 229 } 230 231 232 public void compose(IXMLWriter xml, XhtmlNode node) throws IOException { 233 compose(xml, node, false); 234 } 235 236 public void compose(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException { 237 if (node.getNodeType() == NodeType.Comment) 238 xml.comment(node.getContent(), pretty && !noPrettyOverride); 239 else if (node.getNodeType() == NodeType.Element) 240 composeElement(xml, node, noPrettyOverride); 241 else if (node.getNodeType() == NodeType.Text) 242 xml.text(node.getContent()); 243 else 244 throw new Error("Unhandled node type: "+node.getNodeType().toString()); 245 } 246 247 private void composeElement(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException { 248 for (String n : node.getAttributes().keySet()) { 249 if (n.equals("xmlns")) 250 xml.setDefaultNamespace(node.getAttributes().get(n)); 251 else if (n.startsWith("xmlns:")) 252 xml.namespace(n.substring(6), node.getAttributes().get(n)); 253 else 254 xml.attribute(n, node.getAttributes().get(n)); 255 } 256 xml.enter(XHTML_NS, node.getName()); 257 for (XhtmlNode n : node.getChildNodes()) 258 compose(xml, n, noPrettyOverride || node.isNoPretty()); 259 xml.exit(XHTML_NS, node.getName()); 260 } 261 262 public String composePlainText(XhtmlNode x) { 263 StringBuilder b = new StringBuilder(); 264 composePlainText(x, b, false); 265 return b.toString().trim(); 266 } 267 268 private boolean composePlainText(XhtmlNode x, StringBuilder b, boolean lastWS) { 269 if (x.getNodeType() == NodeType.Text) { 270 String s = x.getContent(); 271 if (!lastWS & (s.startsWith(" ") || s.startsWith("\r") || s.startsWith("\n") || s.endsWith("\t"))) { 272 b.append(" "); 273 lastWS = true; 274 } 275 String st = s.trim().replace("\r", " ").replace("\n", " ").replace("\t", " "); 276 while (st.contains(" ")) 277 st = st.replace(" ", " "); 278 if (!Utilities.noString(st)) { 279 b.append(st); 280 lastWS = false; 281 if (!lastWS & (s.endsWith(" ") || s.endsWith("\r") || s.endsWith("\n") || s.endsWith("\t"))) { 282 b.append(" "); 283 lastWS = true; 284 } 285 } 286 return lastWS; 287 } else if (x.getNodeType() == NodeType.Element) { 288 if (x.getName().equals("li")) { 289 b.append("* "); 290 lastWS = true; 291 } 292 293 for (XhtmlNode n : x.getChildNodes()) { 294 lastWS = composePlainText(n, b, lastWS); 295 } 296 if (x.getName().equals("p")) { 297 b.append("\r\n\r\n"); 298 lastWS = true; 299 } 300 if (x.getName().equals("br") || x.getName().equals("li")) { 301 b.append("\r\n"); 302 lastWS = true; 303 } 304 return lastWS; 305 } else 306 return lastWS; 307 } 308 309 public void compose(Element div, XhtmlNode x) { 310 for (XhtmlNode child : x.getChildNodes()) { 311 appendChild(div, child); 312 } 313 } 314 315 private void appendChild(Element e, XhtmlNode node) { 316 if (node.getNodeType() == NodeType.Comment) 317 e.appendChild(e.getOwnerDocument().createComment(node.getContent())); 318 else if (node.getNodeType() == NodeType.DocType) 319 throw new Error("not done yet"); 320 else if (node.getNodeType() == NodeType.Instruction) 321 e.appendChild(e.getOwnerDocument().createProcessingInstruction("", node.getContent())); 322 else if (node.getNodeType() == NodeType.Text) 323 e.appendChild(e.getOwnerDocument().createTextNode(node.getContent())); 324 else if (node.getNodeType() == NodeType.Element) { 325 Element child = e.getOwnerDocument().createElementNS(XHTML_NS, node.getName()); 326 e.appendChild(child); 327 for (String n : node.getAttributes().keySet()) { 328 child.setAttribute(n, node.getAttribute(n)); 329 } 330 for (XhtmlNode c : node.getChildNodes()) { 331 appendChild(child, c); 332 } 333 } else 334 throw new Error("Unknown node type: "+node.getNodeType().toString()); 335 } 336 337 public void compose(OutputStream stream, XhtmlNode x) throws IOException { 338 byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF }; 339 stream.write(bom); 340 dst = new OutputStreamWriter(stream, "UTF-8"); 341 dst.append("<html><head><link rel=\"stylesheet\" href=\"fhir.css\"/></head><body>\r\n"); 342 writeNode("", x, false); 343 dst.append("</body></html>\r\n"); 344 dst.flush(); 345 } 346 347 public void composeDocument(FileOutputStream f, XhtmlNode xhtml) throws IOException { 348 byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF }; 349 f.write(bom); 350 dst = new OutputStreamWriter(f, "UTF-8"); 351 writeNode("", xhtml, false); 352 dst.flush(); 353 dst.close(); 354 } 355 356 public String composeEx(XhtmlNode node) { 357 try { 358 return compose(node); 359 } catch (IOException e) { 360 throw new Error(e); 361 } 362 } 363 364}