c8bcc6aaf68d9a90df81afe8b5bb0628060e8477
[org.ibex.core.git] / src / org / xwt / HTML.java
1 // Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt;
3
4 import org.xwt.util.*;
5 import java.util.*;
6 import java.net.*;
7 import java.io.*;
8
9 /* 
10    While entities are limited to a subset of Unicode characters ,
11    numeric character references can specify any character. Numeric
12    character references may be given in decimal or hexadecimal, though
13    browser support is stronger for decimal references. Decimal
14    references are of the form &#number; while hexadecimal references
15    take the case-insensitive form &#xnumber;. Examples of numeric
16    character references include © or © for the copyright
17    symbol, Α or Α for the Greek capital letter alpha, and
18    ا or ا for the Arabic letter ALEF.
19
20    http://www.htmlhelp.com/reference/html40/entities/special.html
21    http://www.htmlhelp.com/reference/html40/entities/symbols.html
22    http://www.htmlhelp.com/reference/html40/entities/latin1.html
23
24    FIXME FIXME FIXME: <li> tags close enclosing <li> tags
25 */
26
27 /**
28  *   This class parses an InputStream containing HTML and returns it
29  *   as an XWT DOM tree. Each HTML Element is returned as a struct,
30  *   with the following members:
31  *
32  *   Since HTML may have multiple top level elements (unlike XML),
33  *   this class will search all top level elements for one with a tag
34  *   name 'html'. If such a node is found, only it is returned. If no
35  *   top-level element has the tag name 'html', such a node is
36  *   fabricated, and all top level elements become the children of
37  *   that node, which is then returned.
38  */
39 public class HTML {
40
41     // FIXME: fill in
42     private final static String[] bodylessTags = new String[] { "br", "hr", "input", "img", "isindex" };
43
44     /** we keep a char[] around for use by removeRedundantWhitespace() */
45     private static char[] cbuf = null;
46
47     /** we keep a StringBuffer around for use by removeRedundantWhitespace() */
48     private static StringBuffer sbuf = null;
49
50     public static synchronized JSObject parseReader(Reader r) throws IOException {
51         CharStream cs = new CharStream(r);
52         JSObject h = new JSObject();
53
54         h.put("$name", "html");
55
56         try {
57             while (true) parseBody(cs, h, null);
58         } catch (EOFException e) {
59             // continue until we get an EOFException
60         }
61         
62         Object[] ids = h.getIds();
63         for(int i=0; i<ids.length; i++) {
64             Object el = h.get((String)ids[i]);
65             if (el instanceof JSObject && "html".equals(((JSObject)el).get("$name")))
66                 return (JSObject)el;
67         }
68         
69         return h;
70     }
71
72     /**
73      *  Parses a single element and stores it in <tt>h</tt>. The
74      *  CharStream should be positioned immediately <i>after</i> the
75      *  open bracket.
76      *
77      *  If a close tag not matching this open tag is found, the
78      *  tagname on the close tag will be returned in order to
79      *  facilitate correcting broken HTML. Otherwise, this returns
80      *  null.
81      */
82     private static String parseElement(CharStream cs, JSObject h) throws IOException {
83         
84         // scan element name
85         while(Character.isSpace(cs.peek())) cs.get();
86         String elementName = parseElementName(cs);
87
88         h.put("$name", elementName);
89         if (elementName.equals("!--")) {
90             h.put("0", parseComment(cs));
91             h.put("$numchildren", new Integer(0));
92             return null;
93         }
94
95         // scan attributes
96         while (cs.peek() != '>') {
97             String name = parseAttributeName(cs);
98             if (name.equals("")) break;
99             String value = expandEntities(parseAttributeValue(cs));
100             h.put(name, value);
101         } 
102
103         // eat the close-angle bracket
104         cs.get();
105
106         // bodyless tags return here
107         for(int i=0; i<bodylessTags.length; i++)
108             if (bodylessTags[i].equals(elementName))
109                 return null;
110
111         // scan body
112         return parseBody(cs, h, elementName);
113     }
114
115     /**
116      *  Parses the body of an element. The CharStream should be
117      *  positioned at the character immediately after the right
118      *  bracket closing the start-tag
119      */
120     private static String parseBody(CharStream cs, JSObject h, String elementName) throws IOException {
121         String cdata = "";
122         int length = h.get("$numchildren") == null ? 0 : Integer.parseInt(h.get("$numchildren").toString());
123         while(true) {
124             String closetag = null;
125
126             try {
127                 char c = cs.get();
128                 if (c != '<') { cdata += c; continue; }
129                 String expanded = removeRedundantWhitespace(expandEntities(cdata));
130                 if (expanded.length() > 0) {
131                     h.put(String.valueOf(length), expanded);
132                     h.put("$numchildren", new Integer(++length));
133                 }
134                 cdata = "";
135
136             } catch (EOFException e) {
137                 String expanded = removeRedundantWhitespace(expandEntities(cdata));
138                 if (expanded.length() > 0) {
139                     h.put(String.valueOf(length), expanded);
140                     h.put("$numchildren", new Integer(++length));
141                 }
142                 throw e;
143             }
144                 
145             try {
146                 // scan subelement
147                 if (cs.peek() != '/') {
148                     JSObject kid = new JSObject();
149                     closetag = parseElement(cs, kid);
150                     h.put(String.valueOf(length), kid); 
151                     h.put("$numchildren", new Integer(++length));
152                     
153                 // scan close-tag
154                 } else {
155                     cs.get(); // drop the slash
156                     closetag = parseElementName(cs);
157                     while(cs.get() != '>');
158                 }
159             } catch (EOFException e) {
160                 throw e;
161
162             }
163             
164             if (closetag != null)
165                 return closetag.equals(elementName) ? null : closetag;
166         }
167     }
168
169     /** Parses an element name and returns it. The CharStream should
170      *  be positioned at the first character of the name.
171      */
172     private static String parseElementName(CharStream cs) throws IOException {
173         String ret = "";
174         while (cs.peek() != '>' && !Character.isSpace(cs.peek())) ret += cs.get();
175         return ret.toLowerCase();
176     }
177
178     /** Parses an attribute name and returns it. The CharStream should
179      *  be positioned at the first character of the name, possibly
180      *  with intervening whitespace.
181      */
182     private static String parseAttributeName(CharStream cs) throws IOException {
183         while(Character.isSpace(cs.peek())) cs.get();
184         String ret = "";
185         while(!Character.isSpace(cs.peek()) && cs.peek() != '=' && cs.peek() != '>') ret += cs.get();
186         return ret.toLowerCase();
187     }
188
189     /** Parses an attribute value and returns it. The CharStream
190      *  should be positioned at the equals sign, possibly with
191      *  intervening whitespace.
192      */
193     private static String parseAttributeValue(CharStream cs) throws IOException {
194
195         // eat whitespace and equals sign
196         while(Character.isSpace(cs.peek())) cs.get();
197         if (cs.peek() != '=') return "";
198         cs.get();
199         while(Character.isSpace(cs.peek())) cs.get();
200
201         boolean doublequoted = false;
202         boolean singlequoted = false;
203         String ret = "";
204
205         if (cs.peek() == '\"') { doublequoted = true; cs.get(); }
206         else if (cs.peek() == '\'') { singlequoted = true; cs.get(); }
207
208         while(true) {
209             char c = cs.peek();
210             if (!doublequoted && !singlequoted && (Character.isSpace(c) || c == '>')) break;
211             if (singlequoted && c == '\'') { cs.get(); break; }
212             if (doublequoted && c == '\"') { cs.get(); break; }
213             ret += cs.get();
214         }
215         return ret;
216     }
217
218     /** Parses a comment and returns its body. The CharStream should
219      *  be positioned immediately after the &lt;!--
220      */
221     private static String parseComment(CharStream cs) throws IOException {
222         int dashes = 0;
223         String ret = "";
224         while(true) {
225             char c = cs.get();
226             if (c == '>' && dashes == 2) return ret.substring(0, ret.length() - 2);
227             if (c == '-') dashes++;
228             else dashes = 0;
229             ret += c;
230         }
231     }
232
233     /** Expands all SGML entities in string <tt>s</tt> */
234     public static String expandEntities(String s) throws IOException {
235         if (s.indexOf('&') == -1) return s;
236         StringBuffer sb = new StringBuffer();
237         int i=0;
238         int nextamp = 0;
239         while(nextamp != -1) {
240             nextamp = s.indexOf('&', i);
241             sb.append(nextamp == -1 ? s.substring(i) : s.substring(i, nextamp));
242             if (nextamp == -1) break;
243             if (s.regionMatches(nextamp, "&amp;", 0, 5)) {
244                 sb.append("&");
245                 i = nextamp + 5;
246             } else if (s.regionMatches(nextamp, "&gt;", 0, 4)) {
247                 sb.append(">");
248                 i = nextamp + 4;
249             } else if (s.regionMatches(nextamp, "&lt;", 0, 4)) {
250                 sb.append("<");
251                 i = nextamp + 4;
252             } else if (s.regionMatches(nextamp, "&quot;", 0, 6)) {
253                 sb.append("\"");
254                 i = nextamp + 6;
255             } else if (s.regionMatches(nextamp, "&nbsp;", 0, 6)) {
256                 // FIXME: should have a way to indicate this...
257                 sb.append(" ");
258                 i = nextamp + 6;
259             } else {
260                 sb.append("&");
261                 i = nextamp + 1;
262             }
263         }
264         return sb.toString();
265     }
266
267     // FIXME double check this
268     /** removes all redundant whitespace */
269     private static String removeRedundantWhitespace(String s) {
270
271         if (s.indexOf(' ') == -1 && s.indexOf('\n') == -1 && s.indexOf('\t') == -1 && s.indexOf('\r') == -1) return s;
272
273         int len = s.length();
274         if (cbuf == null || cbuf.length < len) {
275             cbuf = new char[len * 2];
276             sbuf = new StringBuffer(len * 2);
277         }
278         sbuf.setLength(0);
279         s.getChars(0, len, cbuf, 0);
280
281         int last = 0;
282         boolean lastWasWhitespace = false;
283         for(int i=0; i<len; i++) {
284             boolean lastlast = lastWasWhitespace;
285             switch(cbuf[i]) {
286             case '\n': case '\r': case '\t':
287                 cbuf[i] = ' ';
288             case ' ':
289                 lastWasWhitespace = true;
290                 break;
291             default:
292                 lastWasWhitespace = false;
293                 break;
294             }
295             if (lastWasWhitespace && lastlast) {
296                 if (last != i) sbuf.append(cbuf, last, i - last);
297                 last = i+1;
298             }
299         }
300             
301         if (last != len) sbuf.append(cbuf, last, len - last);
302         return sbuf.toString().trim();
303     }
304
305     // CharStream /////////////////////////////////////////////////////////////////////
306
307     private static class CharStream extends PushbackReader {
308         public CharStream(Reader r) { super(r); }
309
310         public char peek() throws IOException {
311             char c = get();
312             unread(c);
313             return c;
314         }
315
316         public char get() throws IOException {
317             int i = read();
318             if (i == -1) throw new EOFException();
319             return (char)i;
320         }
321     }
322
323 }
324