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.IOException;
053import java.io.InputStream;
054import java.io.InputStreamReader;
055import java.io.Reader;
056import java.io.StringReader;
057import java.nio.charset.StandardCharsets;
058import java.util.ArrayList;
059import java.util.HashMap;
060import java.util.HashSet;
061import java.util.List;
062import java.util.Map;
063import java.util.Set;
064
065import org.hl7.fhir.exceptions.FHIRException;
066import org.hl7.fhir.exceptions.FHIRFormatError;
067import org.hl7.fhir.utilities.xhtml.XhtmlNode.Location;
068import org.w3c.dom.Attr;
069import org.w3c.dom.Element;
070import org.w3c.dom.Node;
071import org.xmlpull.v1.XmlPullParser;
072import org.xmlpull.v1.XmlPullParserException;
073
074public class XhtmlParser {
075  public static final String XHTML_NS = "http://www.w3.org/1999/xhtml";
076
077  public class NSMap {
078    private Map<String, String> nslist = new HashMap<String, String>();
079
080    public NSMap(NSMap nsm) {
081      if (nsm != null)
082        nslist.putAll(nsm.nslist);
083    }
084
085    public void def(String ns) {
086      nslist.put("", ns);
087    }
088
089    public void ns(String abbrev, String ns) {
090      nslist.put(abbrev, ns);  
091    }
092
093    public String def() {
094      return nslist.get("");
095    }
096
097    public boolean hasDef() {
098      return nslist.containsKey("");
099    }
100
101    public String get(String abbrev) {
102      return nslist.containsKey(abbrev) ? nslist.get(abbrev) : "http://error/undefined-namespace";
103    }
104  }
105
106  public class QName {
107    private String ns;
108    private String name;
109
110    public QName(String src) {
111      if (src.contains(":")) {
112        ns = src.substring(0, src.indexOf(":"));
113        name = src.substring(src.indexOf(":")+1);
114      } else {
115        ns = null;
116        name = src;
117      }
118    }
119
120    public String getName() {
121      return name;
122    }
123
124    public boolean hasNs() {
125      return ns != null;
126    }
127
128    public String getNs() {
129      return ns;
130    }
131
132    @Override
133    public String toString() {
134      return ns+"::"+name;
135    }
136    
137  }
138
139  private Set<String> elements = new HashSet<String>();
140  private Set<String> attributes = new HashSet<String>();  
141  
142  
143  public XhtmlParser() {
144  super();
145  policy = ParserSecurityPolicy.Accept; // for general parsing
146  
147  // set up sets
148  elements.add("p");
149  elements.add("br");
150  elements.add("div");
151  elements.add("h1");
152  elements.add("h2");
153  elements.add("h3");
154  elements.add("h4");
155  elements.add("h5");
156  elements.add("h6");
157  elements.add("a");
158  elements.add("span");
159  elements.add("b");
160  elements.add("em");
161  elements.add("i");
162  elements.add("strong");
163  elements.add("small");
164  elements.add("big");
165  elements.add("tt");
166  elements.add("small");
167  elements.add("dfn");
168  elements.add("q");
169  elements.add("var");
170  elements.add("abbr");
171  elements.add("acronym");
172  elements.add("cite");
173  elements.add("blockquote");
174  elements.add("hr");
175  elements.add("address");
176  elements.add("bdo");
177  elements.add("kbd");
178  elements.add("q");
179  elements.add("sub");
180  elements.add("sup");
181  elements.add("ul");
182  elements.add("ol");
183  elements.add("li");
184  elements.add("dl");
185  elements.add("dt");
186  elements.add("dd");
187  elements.add("pre");
188  elements.add("table");
189  elements.add("caption");
190  elements.add("colgroup");
191  elements.add("col");
192  elements.add("thead");
193  elements.add("tr");
194  elements.add("tfoot");
195  elements.add("tbody");
196  elements.add("th");
197  elements.add("td");
198  elements.add("code");
199  elements.add("samp");
200  elements.add("img");
201  elements.add("map");
202  elements.add("area");
203  
204  attributes.add("title");
205  attributes.add("style");
206  attributes.add("class");
207  attributes.add("id");
208  attributes.add("lang");
209  attributes.add("xml:lang");
210  attributes.add("dir");
211  attributes.add("accesskey");
212  attributes.add("tabindex");
213    // tables:
214  attributes.add("span");
215  attributes.add("width");
216  attributes.add("align");
217  attributes.add("valign");
218  attributes.add("char");
219  attributes.add("charoff");
220  attributes.add("abbr");
221  attributes.add("axis");
222  attributes.add("headers");
223  attributes.add("scope");
224  attributes.add("rowspan");
225  attributes.add("colspan");
226
227  attributes.add("a.href");
228  attributes.add("a.name");
229  attributes.add("img.src");
230  attributes.add("img.border");
231  attributes.add("div.xmlns");
232  attributes.add("blockquote.cite");
233  attributes.add("q.cite");
234  attributes.add("a.charset");
235  attributes.add("a.type");
236  attributes.add("a.name");
237  attributes.add("a.href");
238  attributes.add("a.hreflang");
239  attributes.add("a.rel");
240  attributes.add("a.rev");
241  attributes.add("a.shape");
242  attributes.add("a.coords");
243  attributes.add("img.src");
244  attributes.add("img.alt");
245  attributes.add("img.longdesc");
246  attributes.add("img.height");
247  attributes.add("img.width");
248  attributes.add("img.usemap");
249  attributes.add("img.ismap");
250  attributes.add("map.name");
251  attributes.add("area.shape");
252  attributes.add("area.coords");
253  attributes.add("area.href");
254  attributes.add("area.nohref");
255  attributes.add("area.alt");
256  attributes.add("table.summary");
257  attributes.add("table.width");
258  attributes.add("table.border");
259  attributes.add("table.frame");
260  attributes.add("table.rules");
261  attributes.add("table.cellspacing");
262  attributes.add("table.cellpadding");
263}
264
265public enum ParserSecurityPolicy {
266    Accept,
267    Drop,
268    Reject
269  }
270
271  private ParserSecurityPolicy policy;
272  
273  private boolean trimWhitespace;
274  private boolean mustBeWellFormed = true;
275  private boolean validatorMode;
276  
277  public boolean isTrimWhitespace() {
278    return trimWhitespace;
279  }
280
281  public void setTrimWhitespace(boolean trimWhitespace) {
282    this.trimWhitespace = trimWhitespace;
283  }
284
285  public boolean isMustBeWellFormed() {
286    return mustBeWellFormed;
287  }
288
289  public XhtmlParser setMustBeWellFormed(boolean mustBeWellFormed) {
290    this.mustBeWellFormed = mustBeWellFormed;
291    return this;
292  }
293  
294
295  public boolean isValidatorMode() {
296    return validatorMode;
297  }
298
299  public XhtmlParser setValidatorMode(boolean validatorMode) {
300    this.validatorMode = validatorMode;
301    return this;
302  }
303
304  public ParserSecurityPolicy getPolicy() {
305   return policy;
306  }
307
308  public void setPolicy(ParserSecurityPolicy policy) {
309  this.policy = policy; 
310  }
311
312  public XhtmlNode parseHtmlNode(Element node) throws FHIRFormatError  {
313    return parseHtmlNode(node, null);
314  }
315  
316  public XhtmlNode parseHtmlNode(Element node, String defaultNS) throws FHIRFormatError  {
317    XhtmlNode res = parseNode(node, defaultNS);
318    if (res.getNsDecl() == null)
319      res.getAttributes().put("xmlns", XHTML_NS);
320    return res;
321  }
322
323  private XhtmlNode parseNode(Element node, String defaultNS) throws FHIRFormatError  {
324    XhtmlNode res = new XhtmlNode(NodeType.Element);
325    res.setName(node.getLocalName());
326    defaultNS = checkNS(res, node, defaultNS);
327    for (int i = 0; i < node.getAttributes().getLength(); i++) {
328      Attr attr = (Attr) node.getAttributes().item(i);
329      if (attributeIsOk(res.getName(), attr.getName(), attr.getValue()) && !attr.getLocalName().startsWith("xmlns"))
330        res.getAttributes().put(attr.getName(), attr.getValue());
331    }
332    Node child = node.getFirstChild();
333    while (child != null) {
334      if (child.getNodeType() == Node.TEXT_NODE) {
335        res.addText(child.getTextContent());
336      } else if (child.getNodeType() == Node.COMMENT_NODE) {
337        res.addComment(child.getTextContent());
338      } else if (child.getNodeType() == Node.ELEMENT_NODE) {
339        if (elementIsOk(child.getLocalName()))
340          res.getChildNodes().add(parseNode((Element) child, defaultNS));
341      } else
342        throw new FHIRFormatError("Unhandled XHTML feature: "+Integer.toString(child.getNodeType())+descLoc());
343      child = child.getNextSibling();
344    }
345    return res;
346  }  
347
348  private String checkNS(XhtmlNode res, Element node, String defaultNS) {
349    if (!validatorMode)
350      return null;
351    String ns = node.getNamespaceURI();
352    if (ns == null)
353      return null;
354    if (!ns.equals(defaultNS)) {
355      res.getAttributes().put("xmlns", ns);
356      return ns;
357    }
358    return defaultNS;
359  }
360
361  public XhtmlNode parseHtmlNode(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError  {
362    XhtmlNode res = parseNode(xpp);
363    if (res.getNsDecl() == null)
364      res.getAttributes().put("xmlns", XHTML_NS);
365    return res;
366
367  }
368  private XhtmlNode parseNode(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError  {
369    XhtmlNode res = new XhtmlNode(NodeType.Element);
370    res.setName(xpp.getName());
371    
372    for (int i = 0; i < xpp.getAttributeCount(); i++) {
373      if (attributeIsOk(xpp.getName(), xpp.getAttributeName(i), xpp.getAttributeValue(i)))
374      res.getAttributes().put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
375    }
376    int eventType = xpp.next();
377    while (eventType != XmlPullParser.END_TAG) {
378      if (eventType == XmlPullParser.TEXT) {
379        res.addText(xpp.getText());
380        xpp.next();
381      } else if (eventType == XmlPullParser.COMMENT) {
382        res.addComment(xpp.getText());
383        xpp.next();
384      } else if (eventType == XmlPullParser.START_TAG) {
385        if (elementIsOk(xpp.getName()))
386          res.getChildNodes().add(parseNode(xpp));
387      } else
388        throw new FHIRFormatError("Unhandled XHTML feature: "+Integer.toString(eventType)+descLoc());
389      eventType = xpp.getEventType();
390    }
391    xpp.next();
392    return res;
393  }  
394
395  private boolean attributeIsOk(String elem, String attr, String value) throws FHIRFormatError  {
396    if (validatorMode)
397      return true;
398  boolean ok = attributes.contains(attr) || attributes.contains(elem+"."+attr);
399  if (ok)
400    return true;
401  else switch (policy) {
402    case Accept: return true;
403    case Drop: return false;
404    case Reject: throw new FHIRFormatError("Illegal HTML attribute "+elem+"."+attr);
405  }
406
407  if ((elem+"."+attr).equals("img.src") && !(value.startsWith("#") || value.startsWith("http:") || value.startsWith("https:"))) {
408    switch (policy) {
409      case Accept: return true;
410      case Drop: return false;
411      case Reject: throw new FHIRFormatError("Illegal Image Reference "+value);
412    }
413  }
414  return false;
415  }
416
417private boolean elementIsOk(String name) throws FHIRFormatError  {
418    if (validatorMode)
419      return true;
420    boolean ok = elements.contains(name);
421  if (ok)
422      return true;
423  else switch (policy) {
424    case Accept: return true;
425    case Drop: return false;
426    case Reject: throw new FHIRFormatError("Illegal HTML element "+name);
427  }
428  return false;
429}
430
431  private String descLoc() {
432    return " at line "+Integer.toString(line)+" column "+Integer.toString(col);
433  }
434
435  private Reader rdr;
436  private String cache = "";
437  private XhtmlNode unwindPoint;
438  private String lastText = "";
439  private int line = 1;
440  private int col = 0;
441  private char lastChar;
442  private Location lastLoc;
443  
444  public XhtmlDocument parse(String source, String entryName) throws FHIRFormatError, IOException  {
445    rdr = new StringReader(source);
446    return parse(entryName);
447  }
448  
449  public XhtmlDocument parse(InputStream input, String entryName) throws FHIRFormatError, IOException  {
450    rdr = new InputStreamReader(input, StandardCharsets.UTF_8);
451    return parse(entryName);
452  }
453   
454  private XhtmlDocument parse(String entryName) throws FHIRFormatError, IOException 
455  {
456    XhtmlDocument result = new XhtmlDocument();
457    skipWhiteSpaceAndComments(result);
458    if (peekChar() != '<')
459      throw new FHIRFormatError("Unable to Parse HTML - does not start with tag. Found "+peekChar()+descLoc());
460    readChar();
461    markLocation();
462    QName n = new QName(readName().toLowerCase());
463    if ((entryName != null) && !n.getName().equals(entryName))
464      throw new FHIRFormatError("Unable to Parse HTML - starts with '"+n+"' not '"+entryName+"'"+descLoc());
465    XhtmlNode root = result.addTag(n.getName());
466    root.setLocation(markLocation());
467    parseAttributes(root);
468    markLocation();
469    NSMap nsm = checkNamespaces(n, root, null, true);
470    if (readChar() == '/') {
471      if (peekChar() != '>')
472        throw new FHIRFormatError("unexpected non-end of element "+n+" "+descLoc());
473      readChar();
474    } else {
475      unwindPoint = null;
476      List<XhtmlNode> p = new ArrayList<>();
477      parseElementInner(root, p, nsm, true);
478    }
479    return result;
480  }
481  
482  private Location markLocation() {
483    Location res = lastLoc;
484    lastLoc = new Location(line, col);
485    return res;
486  }
487
488  private NSMap checkNamespaces(QName n, XhtmlNode node, NSMap nsm, boolean root) {
489    // what we do here is strip out any stated namespace attributes, putting them in the namesapce map
490    // then we figure out what the namespace of this element is, and state it explicitly if it's not the default
491    
492    NSMap result = new NSMap(nsm);
493    List<String> nsattrs = new ArrayList<String>();
494    for (String an : node.getAttributes().keySet()) {
495      if (an.equals("xmlns")) {
496        result.def(node.getAttribute(an));
497        nsattrs.add(an);
498      }
499      if (an.startsWith("xmlns:")) {
500        result.ns(an.substring(6), node.getAttribute(an));
501        nsattrs.add(an);
502      }
503    }
504    for (String s : nsattrs)
505      node.getAttributes().remove(s);
506    if (n.hasNs()) {
507      String nns = result.get(n.getNs());
508      if (!nns.equals(result.def())) {
509        node.getAttributes().put("xmlns", nns);
510        result.def(nns);
511      }
512    } else if (root && result.hasDef()) {
513      node.getAttributes().put("xmlns", result.def());
514    }
515    return result;
516  }
517
518  private void addTextNode(XhtmlNode node, StringBuilder s)
519  {
520    String t = isTrimWhitespace() ? s.toString().trim() : s.toString();
521    if (t.length() > 0)
522    {
523      lastText = t;
524      // System.out.println(t);
525      node.addText(t).setLocation(markLocation());
526      s.setLength(0);
527    }
528  }
529  private void parseElementInner(XhtmlNode node, List<XhtmlNode> parents, NSMap nsm, boolean escaping) throws FHIRFormatError, IOException 
530  {
531    StringBuilder s = new StringBuilder();
532    while (peekChar() != '\0' && !parents.contains(unwindPoint) && !(node == unwindPoint))
533    {
534      if (peekChar() == '<')
535      {
536        addTextNode(node, s);
537        readChar();
538        if (peekChar() == '!') {
539          String sc = readToCommentEnd();
540          if (sc.startsWith("DOCTYPE"))
541            throw new FHIRFormatError("Malformed XHTML: Found a DocType declaration, and these are not allowed (XXE security vulnerability protection)");
542          node.addComment(sc).setLocation(markLocation());
543        } else if (peekChar() == '?')
544          node.addComment(readToTagEnd()).setLocation(markLocation());
545        else if (peekChar() == '/') {
546          readChar();
547          QName n = new QName(readToTagEnd());
548          if (node.getName().equals(n.getName()))
549            return;
550          else
551          {
552            if (mustBeWellFormed)
553              throw new FHIRFormatError("Malformed XHTML: Found \"</"+n.getName()+">\" expecting \"</"+node.getName()+">\""+descLoc());
554            for (int i = parents.size() - 1; i >= 0; i--)
555            {
556              if (parents.get(i).getName().equals(n))
557                unwindPoint = parents.get(i);
558            }
559            if (unwindPoint != null)
560            {
561              for (int i = parents.size(); i > 0; i--)
562              {
563                if (i < parents.size() && parents.get(i) == unwindPoint)
564                  return;
565                if (i == parents.size())
566                {
567                  parents.get(i - 1).getChildNodes().addAll(node.getChildNodes());
568                  node.getChildNodes().clear();
569                }
570                else
571                {
572                  parents.get(i - 1).getChildNodes().addAll(parents.get(i).getChildNodes());
573                  parents.get(i).getChildNodes().clear();
574                }
575              }
576            }
577          }
578        }
579        else if (Character.isLetterOrDigit(peekChar()))
580        {
581          parseElement(node, parents, nsm);
582        }
583        else
584          throw new FHIRFormatError("Unable to Parse HTML - node '" + node.getName() + "' has unexpected content '"+peekChar()+"' (last text = '"+lastText+"'"+descLoc());
585      }
586      else if (peekChar() == '&') // escaping && 
587      {
588        parseLiteral(s);
589      }
590      else
591        s.append(readChar());
592    }
593    addTextNode(node, s);
594  }
595
596  private void parseElement(XhtmlNode parent, List<XhtmlNode> parents, NSMap nsm) throws IOException, FHIRFormatError 
597  {
598    markLocation();
599    QName name = new QName(readName());
600    XhtmlNode node = parent.addTag(name.getName());
601    node.setLocation(markLocation());
602    List<XhtmlNode> newParents = new ArrayList<XhtmlNode>();
603    newParents.addAll(parents);
604    newParents.add(parent);
605    parseAttributes(node);
606    markLocation();
607    nsm = checkNamespaces(name, node, nsm, false);
608    if (readChar() == '/') {
609      if (peekChar() != '>')
610        throw new FHIRFormatError("unexpected non-end of element "+name+" "+descLoc());
611      readChar();
612    } else {
613       parseElementInner(node, newParents, nsm, "script".equals(name.getName()));
614    }
615  }
616  
617  private void parseAttributes(XhtmlNode node) throws FHIRFormatError, IOException 
618  {
619    while (Character.isWhitespace(peekChar()))
620      readChar();
621    while (peekChar() != '>' && peekChar() != '/' && peekChar() != '\0')
622    {
623      String name = readName();
624      if (name.length() == 0)
625      {
626        throw new FHIRFormatError("Unable to read attribute on <"+node.getName()+">"+descLoc());
627      }
628      while (Character.isWhitespace(peekChar()))
629        readChar();
630
631      if (isNameChar(peekChar()) || peekChar() == '>' || peekChar() == '/')
632        node.getAttributes().put(name, null);
633      else if (peekChar() != '=')
634      {
635        throw new FHIRFormatError("Unable to read attribute '"+name+"' value on <"+node.getName()+">"+descLoc());
636      }
637      else
638      {
639        readChar();
640        while (Character.isWhitespace(peekChar()))
641          readChar();
642        if (peekChar() == '"' || peekChar() == '\'')
643          node.getAttributes().put(name, parseAttributeValue(readChar()));
644        else
645          node.getAttributes().put(name, parseAttributeValue('\0'));
646      }
647      while (Character.isWhitespace(peekChar()))
648        readChar();
649    }
650  }
651
652  private String parseAttributeValue(char term) throws IOException, FHIRFormatError 
653  {
654    StringBuilder b = new StringBuilder();
655    while (peekChar() != '\0' && peekChar() != '>' && (term != '\0' || peekChar() != '/') && peekChar() != term)
656    {
657      if (peekChar() == '&')
658      {
659        parseLiteral(b);
660      }
661      else
662        b.append(readChar());
663    }
664    if (peekChar() == term)
665      readChar();
666    return b.toString();
667  }
668
669  
670  private void skipWhiteSpaceAndComments(XhtmlNode focus) throws IOException, FHIRFormatError  {
671    while (Character.isWhitespace(peekChar()) || (peekChar() == 0xfeff))
672      readChar();
673    if (peekChar() == '<')
674    {
675      char ch = readChar();
676      if (peekChar() == '!') {
677        readChar();
678        if (peekChar() == '-') {
679          readChar();
680          if (peekChar() == '-') {
681            readChar();
682            if (peekChar() == ' ')
683              readChar();
684            focus.addComment(readToCommentEnd());
685          } else 
686            throw new FHIRFormatError("unrecognised element type <!"+peekChar()+descLoc());
687        } else
688          focus.addDocType(readToDocTypeEnd());
689        skipWhiteSpaceAndComments(focus);
690      } else if (peekChar() == '?') {
691        String r = readToTagEnd();
692        focus.addInstruction(r.substring(1, r.length()-1));
693        skipWhiteSpaceAndComments(focus);
694      } else 
695        pushChar(ch);
696    }
697  }  
698  
699  private void skipWhiteSpace() throws IOException {
700    if (trimWhitespace)
701      while (Character.isWhitespace(peekChar()) || (peekChar() == 0xfeff))
702        readChar();
703  }
704  
705  private void pushChar(char ch) {
706    cache = Character.toString(ch)+cache;    
707  }
708
709  private char peekChar() throws IOException
710  {
711    if (cache.length() > 0)
712      return cache.charAt(0);
713    else if (!rdr.ready())
714      return '\0';
715    else
716    {
717      char c = (char)rdr.read();
718      if (c == (char)-1)
719      {
720        cache = "";
721        return '\0';
722      }
723      cache =  Character.toString(c);
724      return c;
725    }
726  }
727
728  private char readChar() throws IOException
729  {
730    char c;
731    if (cache.length() > 0)
732    {
733      c = cache.charAt(0);
734      cache = cache.length() == 1 ? "" : cache.substring(1);
735    }
736    else if (!rdr.ready())
737      c = '\0';
738    else
739      c = (char)rdr.read();
740    if (c == '\r' || c == '\n') {
741      if (c == '\r' || lastChar != '\r') {
742        line++;
743        col = 0;
744      }  
745      lastChar = c;      
746    }      
747    col++;
748    return c;
749  }
750
751  private String readToTagEnd() throws IOException, FHIRFormatError 
752  {
753    StringBuilder s = new StringBuilder();
754    while (peekChar() != '>' && peekChar() != '\0')
755      s.append(readChar());
756    if (peekChar() != '\0')
757    {
758      readChar();
759      skipWhiteSpace();
760    } else if (mustBeWellFormed)
761      throw new FHIRFormatError("Unexpected termination of html source"+descLoc());
762    return s.toString();
763  }
764
765  private String readToDocTypeEnd() throws IOException, FHIRFormatError 
766  {
767    StringBuilder s = new StringBuilder();
768      
769    boolean done = false;
770    while (!done) {
771      char c = peekChar();
772      if (c == '>') {
773        done = true;
774        readChar();
775      } else if (c != '\0')
776        s.append(readChar());
777      else if (mustBeWellFormed)
778        throw new FHIRFormatError("Unexpected termination of html source"+descLoc());
779    }
780    return s.toString();
781  }
782
783  private String readToCommentEnd() throws IOException, FHIRFormatError 
784  {
785    if (peekChar() == '!')
786      readChar();
787    StringBuilder s = new StringBuilder();
788      
789    boolean simple = true;
790    if (peekChar() == '-') {
791      readChar();
792      simple = peekChar() != '-';
793      if (simple)
794        s.append('-');
795      else
796        readChar();
797    }
798    
799    boolean done = false;
800    while (!done) {
801      char c = peekChar();
802      if (c == '-') {
803        readChar();
804        if (peekChar() == '-') {
805          readChar();
806          if (peekChar() == '>') {
807            done = true;
808          } else
809            s.append("--");
810        } else 
811          s.append('-');
812      } else if (simple && peekChar() == '>') {
813        done = true;
814      } else if (c != '\0')
815        s.append(readChar());
816      else if (mustBeWellFormed)
817        throw new FHIRFormatError("Unexpected termination of html source"+descLoc());
818    }
819    if (peekChar() != '\0')
820    {
821      readChar();
822      skipWhiteSpace();
823    }
824    return s.toString();
825  }
826
827  private boolean isNameChar(char ch)
828  {
829    return Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == ':';
830  }
831
832  private String readName() throws IOException
833  {
834    StringBuilder s = new StringBuilder();
835    while (isNameChar(peekChar()))
836      s.append(readChar());
837    return s.toString();
838  }
839
840  private String readUntil(char ch) throws IOException
841  {
842    StringBuilder s = new StringBuilder();
843    while (peekChar() != 0 && peekChar() != ch)
844      s.append(readChar());
845    readChar();
846    return s.toString();
847  }
848  
849  private void parseLiteral(StringBuilder s) throws IOException, FHIRFormatError {
850    // UInt16 w;
851    readChar();
852    String c = readUntil(';');
853    if (c.equals("apos"))
854      s.append('\'');
855    else if (c.equals("quot"))
856      s.append('"');
857    else if (c.equals("nbsp"))
858      s.append(XhtmlNode.NBSP);
859    else if (c.equals("amp"))
860      s.append('&');
861    else if (c.equals("lsquo"))
862      s.append((char) 8216); // right single quotation, U+2019 ISOnum 
863    else if (c.equals("rsquo"))
864      s.append((char) 8217); // right single quotation, U+2019 ISOnum 
865      //s.append((char)0x60); // right single quote
866          //s.append('’');
867    else if (c.equals("gt"))
868      s.append('>');
869    else if (c.equals("lt"))
870      s.append('<');
871    else if (c.equals("copy"))
872      s.append((char) 169);
873    else if (c.equals("reg"))
874      s.append((char) 174);
875    else if (c.equals("sect"))
876      s.append((char) 0xA7);
877    else if (c.charAt(0) == '#') {
878      if (isInteger(c.substring(1), 10))
879        s.append((char) Integer.parseInt(c.substring(1)));
880      else if (c.charAt(1) == 'x' && isInteger(c.substring(2), 16))
881        s.append((char) Integer.parseInt(c.substring(2), 16));
882    } else if (c.equals("fnof"))
883      s.append((char) 402); // latin small f with hook = function = florin, U+0192 ISOtech -->
884    else if (c.equals("Alpha"))
885      s.append((char) 913); // greek capital letter alpha, U+0391
886    else if (c.equals("Beta"))
887      s.append((char) 914); // greek capital letter beta, U+0392
888    else if (c.equals("Gamma"))
889      s.append((char) 915); // greek capital letter gamma, U+0393 ISOgrk3
890    else if (c.equals("Delta"))
891      s.append((char) 916); // greek capital letter delta, U+0394 ISOgrk3
892    else if (c.equals("Epsilon"))
893      s.append((char) 917); // greek capital letter epsilon, U+0395
894    else if (c.equals("Zeta"))
895      s.append((char) 918); // greek capital letter zeta, U+0396
896    else if (c.equals("Eta"))
897      s.append((char) 919); // greek capital letter eta, U+0397
898    else if (c.equals("Theta"))
899      s.append((char) 920); // greek capital letter theta, U+0398 ISOgrk3
900    else if (c.equals("Iota"))
901      s.append((char) 921); // greek capital letter iota, U+0399
902    else if (c.equals("Kappa"))
903      s.append((char) 922); // greek capital letter kappa, U+039A
904    else if (c.equals("Lambda"))
905      s.append((char) 923); // greek capital letter lambda, U+039B ISOgrk3
906    else if (c.equals("Mu"))
907      s.append((char) 924); // greek capital letter mu, U+039C
908    else if (c.equals("Nu"))
909      s.append((char) 925); // greek capital letter nu, U+039D
910    else if (c.equals("Xi"))
911      s.append((char) 926); // greek capital letter xi, U+039E ISOgrk3
912    else if (c.equals("Omicron"))
913      s.append((char) 927); // greek capital letter omicron, U+039F
914    else if (c.equals("Pi"))
915      s.append((char) 928); // greek capital letter pi, U+03A0 ISOgrk3
916    else if (c.equals("Rho"))
917      s.append((char) 929); // greek capital letter rho, U+03A1
918    else if (c.equals("Sigma"))
919      s.append((char) 931); // greek capital letter sigma, U+03A3 ISOgrk3
920    else if (c.equals("Tau"))
921      s.append((char) 932); // greek capital letter tau, U+03A4
922    else if (c.equals("Upsilon"))
923      s.append((char) 933); // greek capital letter upsilon, U+03A5 ISOgrk3
924    else if (c.equals("Phi"))
925      s.append((char) 934); // greek capital letter phi, U+03A6 ISOgrk3
926    else if (c.equals("Chi"))
927      s.append((char) 935); // greek capital letter chi, U+03A7
928    else if (c.equals("Psi"))
929      s.append((char) 936); // greek capital letter psi, U+03A8 ISOgrk3
930    else if (c.equals("Omega"))
931      s.append((char) 937); // greek capital letter omega, U+03A9 ISOgrk3
932    else if (c.equals("alpha"))
933      s.append((char) 945); // greek small letter alpha, U+03B1 ISOgrk3
934    else if (c.equals("beta"))
935      s.append((char) 946); // greek small letter beta, U+03B2 ISOgrk3
936    else if (c.equals("gamma"))
937      s.append((char) 947); // greek small letter gamma, U+03B3 ISOgrk3
938    else if (c.equals("delta"))
939      s.append((char) 948); // greek small letter delta, U+03B4 ISOgrk3
940    else if (c.equals("epsilon"))
941      s.append((char) 949); // greek small letter epsilon, U+03B5 ISOgrk3
942    else if (c.equals("zeta"))
943      s.append((char) 950); // greek small letter zeta, U+03B6 ISOgrk3
944    else if (c.equals("eta"))
945      s.append((char) 951); // greek small letter eta, U+03B7 ISOgrk3
946    else if (c.equals("theta"))
947      s.append((char) 952); // greek small letter theta, U+03B8 ISOgrk3
948    else if (c.equals("iota"))
949      s.append((char) 953); // greek small letter iota, U+03B9 ISOgrk3
950    else if (c.equals("kappa"))
951      s.append((char) 954); // greek small letter kappa, U+03BA ISOgrk3
952    else if (c.equals("lambda"))
953      s.append((char) 955); // greek small letter lambda, U+03BB ISOgrk3
954    else if (c.equals("mu"))
955      s.append((char) 956); // greek small letter mu, U+03BC ISOgrk3
956    else if (c.equals("nu"))
957      s.append((char) 957); // greek small letter nu, U+03BD ISOgrk3
958    else if (c.equals("xi"))
959      s.append((char) 958); // greek small letter xi, U+03BE ISOgrk3
960    else if (c.equals("omicron"))
961      s.append((char) 959); // greek small letter omicron, U+03BF NEW
962    else if (c.equals("pi"))
963      s.append((char) 960); // greek small letter pi, U+03C0 ISOgrk3
964    else if (c.equals("rho"))
965      s.append((char) 961); // greek small letter rho, U+03C1 ISOgrk3
966    else if (c.equals("sigmaf"))
967      s.append((char) 962); // greek small letter final sigma, U+03C2 ISOgrk3
968    else if (c.equals("sigma"))
969      s.append((char) 963); // greek small letter sigma, U+03C3 ISOgrk3
970    else if (c.equals("tau"))
971      s.append((char) 964); // greek small letter tau, U+03C4 ISOgrk3
972    else if (c.equals("upsilon"))
973      s.append((char) 965); // greek small letter upsilon, U+03C5 ISOgrk3
974    else if (c.equals("phi"))
975      s.append((char) 966); // greek small letter phi, U+03C6 ISOgrk3
976    else if (c.equals("chi"))
977      s.append((char) 967); // greek small letter chi, U+03C7 ISOgrk3
978    else if (c.equals("psi"))
979      s.append((char) 968); // greek small letter psi, U+03C8 ISOgrk3
980    else if (c.equals("omega"))
981      s.append((char) 969); // greek small letter omega, U+03C9 ISOgrk3
982    else if (c.equals("thetasym"))
983      s.append((char) 977); // greek small letter theta symbol, U+03D1 NEW
984    else if (c.equals("upsih"))
985      s.append((char) 978); // greek upsilon with hook symbol, U+03D2 NEW
986    else if (c.equals("piv"))
987      s.append((char) 982); // greek pi symbol, U+03D6 ISOgrk3
988    else if (c.equals("bull"))
989      s.append((char) 8226); // bullet = black small circle, U+2022 ISOpub
990    else if (c.equals("hellip"))
991      s.append((char) 8230); // horizontal ellipsis = three dot leader, U+2026 ISOpub
992    else if (c.equals("prime"))
993      s.append((char) 8242); // prime = minutes = feet, U+2032 ISOtech
994    else if (c.equals("Prime"))
995      s.append((char) 8243); // double prime = seconds = inches, U+2033 ISOtech
996    else if (c.equals("oline"))
997      s.append((char) 8254); // overline = spacing overscore, U+203E NEW
998    else if (c.equals("frasl"))
999      s.append((char) 8260); // fraction slash, U+2044 NEW
1000    else if (c.equals("weierp"))
1001      s.append((char) 8472); // script capital P = power set = Weierstrass p, U+2118 ISOamso
1002    else if (c.equals("image"))
1003      s.append((char) 8465); // blackletter capital I = imaginary part, U+2111 ISOamso
1004    else if (c.equals("real"))
1005      s.append((char) 8476); // blackletter capital R = real part symbol, U+211C ISOamso
1006    else if (c.equals("trade"))
1007      s.append((char) 8482); // trade mark sign, U+2122 ISOnum
1008    else if (c.equals("alefsym"))
1009      s.append((char) 8501); // alef symbol = first transfinite cardinal, U+2135 NEW
1010    else if (c.equals("larr"))
1011      s.append((char) 8592); // leftwards arrow, U+2190 ISOnum
1012    else if (c.equals("uarr"))
1013      s.append((char) 8593); // upwards arrow, U+2191 ISOnum
1014    else if (c.equals("rarr"))
1015      s.append((char) 8594); // rightwards arrow, U+2192 ISOnum
1016    else if (c.equals("darr"))
1017      s.append((char) 8595); // downwards arrow, U+2193 ISOnum
1018    else if (c.equals("harr"))
1019      s.append((char) 8596); // left right arrow, U+2194 ISOamsa
1020    else if (c.equals("crarr"))
1021      s.append((char) 8629); // downwards arrow with corner leftwards = carriage return, U+21B5 NEW
1022    else if (c.equals("lArr"))
1023      s.append((char) 8656); // leftwards double arrow, U+21D0 ISOtech
1024    else if (c.equals("uArr"))
1025      s.append((char) 8657); // upwards double arrow, U+21D1 ISOamsa
1026    else if (c.equals("rArr"))
1027      s.append((char) 8658); // rightwards double arrow, U+21D2 ISOtech
1028    else if (c.equals("dArr"))
1029      s.append((char) 8659); // downwards double arrow, U+21D3 ISOamsa
1030    else if (c.equals("hArr"))
1031      s.append((char) 8660); // left right double arrow, U+21D4 ISOamsa
1032    else if (c.equals("forall"))
1033      s.append((char) 8704); // for all, U+2200 ISOtech
1034    else if (c.equals("part"))
1035      s.append((char) 8706); // partial differential, U+2202 ISOtech
1036    else if (c.equals("exist"))
1037      s.append((char) 8707); // there exists, U+2203 ISOtech
1038    else if (c.equals("empty"))
1039      s.append((char) 8709); // empty set = null set = diameter, U+2205 ISOamso
1040    else if (c.equals("nabla"))
1041      s.append((char) 8711); // nabla = backward difference, U+2207 ISOtech
1042    else if (c.equals("isin"))
1043      s.append((char) 8712); // element of, U+2208 ISOtech
1044    else if (c.equals("notin"))
1045      s.append((char) 8713); // not an element of, U+2209 ISOtech
1046    else if (c.equals("ni"))
1047      s.append((char) 8715); // contains as member, U+220B ISOtech
1048    else if (c.equals("prod"))
1049      s.append((char) 8719); // n-ary product = product sign, U+220F ISOamsb
1050    else if (c.equals("sum"))
1051      s.append((char) 8721); // n-ary sumation, U+2211 ISOamsb
1052    else if (c.equals("minus"))
1053      s.append((char) 8722); // minus sign, U+2212 ISOtech
1054    else if (c.equals("lowast"))
1055      s.append((char) 8727); // asterisk operator, U+2217 ISOtech
1056    else if (c.equals("radic"))
1057      s.append((char) 8730); // square root = radical sign, U+221A ISOtech
1058    else if (c.equals("prop"))
1059      s.append((char) 8733); // proportional to, U+221D ISOtech
1060    else if (c.equals("infin"))
1061      s.append((char) 8734); // infinity, U+221E ISOtech -->
1062    else if (c.equals("ang"))
1063      s.append((char) 8736); // angle, U+2220 ISOamso
1064    else if (c.equals("and"))
1065      s.append((char) 8743); // logical and = wedge, U+2227 ISOtech
1066    else if (c.equals("or"))
1067      s.append((char) 8744); // logical or = vee, U+2228 ISOtech
1068    else if (c.equals("cap"))
1069      s.append((char) 8745); // intersection = cap, U+2229 ISOtech
1070    else if (c.equals("cup"))
1071      s.append((char) 8746); // union = cup, U+222A ISOtech
1072    else if (c.equals("int"))
1073      s.append((char) 8747); // integral, U+222B ISOtech
1074    else if (c.equals("there4"))
1075      s.append((char) 8756); // therefore, U+2234 ISOtech
1076    else if (c.equals("sim"))
1077      s.append((char) 8764); // tilde operator = varies with = similar t U+223C ISOtech
1078    else if (c.equals("cong"))
1079      s.append((char) 8773); // approximately equal to, U+2245 ISOtec
1080    else if (c.equals("asymp"))
1081      s.append((char) 8776); // almost equal to = asymptotic to, U+2248 ISOamsr
1082    else if (c.equals("ne"))
1083      s.append((char) 8800); // not equal to, U+2260 ISOtech
1084    else if (c.equals("equiv"))
1085      s.append((char) 8801); // identical to, U+2261 ISOtech
1086    else if (c.equals("le"))
1087      s.append((char) 8804); // less-than or equal to, U+2264 ISOtech
1088    else if (c.equals("ge"))
1089      s.append((char) 8805); // greater-than or equal to, U+2265 ISOtech
1090    else if (c.equals("sub"))
1091      s.append((char) 8834); // subset of, U+2282 ISOtech
1092    else if (c.equals("sup"))
1093      s.append((char) 8835); // superset of, U+2283 ISOtech
1094    else if (c.equals("nsub"))
1095      s.append((char) 8836); // not a subset of, U+2284 ISOamsn
1096    else if (c.equals("sube"))
1097      s.append((char) 8838); // subset of or equal to, U+2286 ISOtech
1098    else if (c.equals("supe"))
1099      s.append((char) 8839); // superset of or equal to, U+2287 ISOtech
1100    else if (c.equals("oplus"))
1101      s.append((char) 8853); // circled plus = direct sum, U+2295 ISOamsb
1102    else if (c.equals("otimes"))
1103      s.append((char) 8855); // circled times = vector product, U+2297 ISOamsb -->
1104    else if (c.equals("perp"))
1105      s.append((char) 8869); // up tack = orthogonal to = perpendicular, U+22A5 ISOtech
1106    else if (c.equals("sdot"))
1107      s.append((char) 8901); // dot operator, U+22C5 ISOamsb
1108    else if (c.equals("lceil"))
1109      s.append((char) 8968); // left ceiling = apl upstile, U+2308 ISOamsc
1110    else if (c.equals("rceil"))
1111      s.append((char) 8969); // right ceiling, U+2309 ISOamsc
1112    else if (c.equals("lfloor"))
1113      s.append((char) 8970); // left floor = apl downstile, U+230A ISOamsc
1114    else if (c.equals("rfloor"))
1115      s.append((char) 8971); // right floor, U+230B ISOamsc
1116    else if (c.equals("lang"))
1117      s.append((char) 9001); // left-pointing angle bracket = bra, U+2329 ISOtech
1118    else if (c.equals("rang"))
1119      s.append((char) 9002); // right-pointing angle bracket = ket, U+232A ISOtech
1120    else if (c.equals("loz"))
1121      s.append((char) 9674); // lozenge, U+25CA ISOpub
1122    else if (c.equals("spades"))
1123      s.append((char) 9824); // black spade suit, U+2660 ISOpub
1124    else if (c.equals("clubs"))
1125      s.append((char) 9827); // black club suit = shamrock, U+2663 ISOpub
1126    else if (c.equals("hearts"))
1127      s.append((char) 9829); // black heart suit = valentine, U+2665 ISOpub
1128    else if (c.equals("diams"))
1129      s.append((char) 9830); // black diamond suit, U+2666 ISOpub --
1130    else if (c.equals("ndash"))
1131      s.append((char) 8211); 
1132    else if (c.equals("mdash"))
1133      s.append((char) 8212); 
1134    else if (c.equals("ldquo"))
1135      s.append((char) 8221); 
1136    else if (c.equals("rdquo"))
1137      s.append((char) 201D); 
1138    else
1139      throw new FHIRFormatError("unable to parse character reference '" + c + "'' (last text = '" + lastText + "'" + descLoc());
1140  }
1141  
1142  private boolean isInteger(String s, int base) {
1143    try {
1144      Integer.parseInt(s, base);
1145      return true;
1146    } catch (Exception e) {
1147      return false;
1148    }
1149  }
1150
1151  public XhtmlNode parseFragment(String source) throws IOException, FHIRException  {
1152    rdr = new StringReader(source);
1153    return parseFragment();
1154  }
1155  
1156  public XhtmlNode parseFragment(InputStream input) throws IOException, FHIRException  {
1157    rdr = new InputStreamReader(input);
1158    return parseFragment();
1159  }
1160  
1161  private XhtmlNode parseFragment() throws IOException, FHIRException 
1162  {
1163    skipWhiteSpace();
1164    if (peekChar() != '<')
1165      throw new FHIRException("Unable to Parse HTML - does not start with tag. Found "+peekChar()+descLoc());
1166    readChar();
1167    if (peekChar() == '?') {
1168      readToTagEnd();
1169      skipWhiteSpace();
1170      if (peekChar() != '<')
1171        throw new FHIRException("Unable to Parse HTML - does not start with tag after processing instruction. Found "+peekChar()+descLoc());
1172      readChar();
1173    }
1174    String n = readName().toLowerCase();
1175    readToTagEnd();
1176    XhtmlNode result = new XhtmlNode(NodeType.Element);
1177
1178    int colonIndex = n.indexOf(':');
1179    if (colonIndex != -1) {
1180      n = n.substring(colonIndex + 1);
1181    }
1182    
1183    result.setName(n);
1184    unwindPoint = null;
1185    List<XhtmlNode> p = new ArrayList<>();
1186    parseElementInner(result, p, null, true);
1187
1188    return result;
1189  }
1190
1191  
1192}