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("&amp;");
140      else if (c == '<')
141        dst.append("&lt;");
142      else if (c == '>')
143        dst.append("&gt;");
144      else if (xml) {
145        if (c == '"')
146          dst.append("&quot;");
147        else 
148          dst.append(c);
149      } else {
150        if (c == XhtmlNode.NBSP.charAt(0))
151          dst.append("&nbsp;");
152        else if (c == (char) 0xA7)
153          dst.append("&sect;");
154        else if (c == (char) 169)
155          dst.append("&copy;");
156        else if (c == (char) 8482)
157          dst.append("&trade;");
158        else if (c == (char) 956)
159          dst.append("&mu;");
160        else if (c == (char) 174)
161          dst.append("&reg;");
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("&lt;");
187      else if (c == '>')
188        b.append("&gt;");
189      else if (c == '"')
190        b.append("&quot;");
191      else if (c == '&')
192        b.append("&amp;");
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}