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