b04b9d1a67b7b8a86e7ca65923b9b2b3f6f7dcd0
[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         // scan element name
84         while(Character.isSpace(cs.peek())) cs.get();
85         String elementName = parseElementName(cs);
86
87         h.put("$name", elementName);
88         if (elementName.equals("!--")) {
89             h.put("0", parseComment(cs));
90             h.put("$numchildren", new Integer(0));
91             return null;
92         }
93
94         // scan attributes
95         while (cs.peek() != '>') {
96             String name = parseAttributeName(cs);
97             if (name.equals("")) break;
98             String value = expandEntities(parseAttributeValue(cs));
99             h.put(name, value);
100         } 
101
102         // eat the close-angle bracket
103         cs.get();
104
105         // bodyless tags return here
106         for(int i=0; i<bodylessTags.length; i++)
107             if (bodylessTags[i].equals(elementName))
108                 return null;
109
110         // scan body
111         return parseBody(cs, h, elementName);
112     }
113
114     /**
115      *  Parses the body of an element. The CharStream should be
116      *  positioned at the character immediately after the right
117      *  bracket closing the start-tag
118      */
119     private static String parseBody(CharStream cs, JSObject h, String elementName) throws IOException {
120         String cdata = "";
121         int length = h.get("$numchildren") == null ? 0 : Integer.parseInt(h.get("$numchildren").toString());
122         while(true) {
123             String closetag = null;
124
125             try {
126                 char c = cs.get();
127                 if (c != '<') { cdata += c; continue; }
128                 String expanded = removeRedundantWhitespace(expandEntities(cdata));
129                 if (expanded.length() > 0) {
130                     h.put(String.valueOf(length), expanded);
131                     h.put("$numchildren", new Integer(++length));
132                 }
133                 cdata = "";
134
135             } catch (EOFException e) {
136                 String expanded = removeRedundantWhitespace(expandEntities(cdata));
137                 if (expanded.length() > 0) {
138                     h.put(String.valueOf(length), expanded);
139                     h.put("$numchildren", new Integer(++length));
140                 }
141                 throw e;
142             }
143                 
144             try {
145                 // scan subelement
146                 if (cs.peek() != '/') {
147                     JSObject kid = new JSObject();
148                     closetag = parseElement(cs, kid);
149                     h.put(String.valueOf(length), kid); 
150                     h.put("$numchildren", new Integer(++length));
151                     
152                 // scan close-tag
153                 } else {
154                     cs.get(); // drop the slash
155                     closetag = parseElementName(cs);
156                     while(cs.get() != '>');
157                 }
158             } catch (EOFException e) {
159                 throw e;
160
161             }
162             
163             if (closetag != null)
164                 return closetag.equals(elementName) ? null : closetag;
165         }
166     }
167
168     /** Parses an element name and returns it. The CharStream should
169      *  be positioned at the first character of the name.
170      */
171     private static String parseElementName(CharStream cs) throws IOException {
172         String ret = "";
173         while (cs.peek() != '>' && !Character.isSpace(cs.peek())) ret += cs.get();
174         return ret.toLowerCase();
175     }
176
177     /** Parses an attribute name and returns it. The CharStream should
178      *  be positioned at the first character of the name, possibly
179      *  with intervening whitespace.
180      */
181     private static String parseAttributeName(CharStream cs) throws IOException {
182         while(Character.isSpace(cs.peek())) cs.get();
183         String ret = "";
184         while(!Character.isSpace(cs.peek()) && cs.peek() != '=' && cs.peek() != '>') ret += cs.get();
185         return ret.toLowerCase();
186     }
187
188     /** Parses an attribute value and returns it. The CharStream
189      *  should be positioned at the equals sign, possibly with
190      *  intervening whitespace.
191      */
192     private static String parseAttributeValue(CharStream cs) throws IOException {
193
194         // eat whitespace and equals sign
195         while(Character.isSpace(cs.peek())) cs.get();
196         if (cs.peek() != '=') return "";
197         cs.get();
198         while(Character.isSpace(cs.peek())) cs.get();
199
200         boolean doublequoted = false;
201         boolean singlequoted = false;
202         String ret = "";
203
204         if (cs.peek() == '\"') { doublequoted = true; cs.get(); }
205         else if (cs.peek() == '\'') { singlequoted = true; cs.get(); }
206
207         while(true) {
208             char c = cs.peek();
209             if (!doublequoted && !singlequoted && (Character.isSpace(c) || c == '>')) break;
210             if (singlequoted && c == '\'') { cs.get(); break; }
211             if (doublequoted && c == '\"') { cs.get(); break; }
212             ret += cs.get();
213         }
214         return ret;
215     }
216
217     /** Parses a comment and returns its body. The CharStream should
218      *  be positioned immediately after the &lt;!--
219      */
220     private static String parseComment(CharStream cs) throws IOException {
221         int dashes = 0;
222         String ret = "";
223         while(true) {
224             char c = cs.get();
225             if (c == '>' && dashes == 2) return ret.substring(0, ret.length() - 2);
226             if (c == '-') dashes++;
227             else dashes = 0;
228             ret += c;
229         }
230     }
231
232     /** Expands all SGML entities in string <tt>s</tt> */
233     public static String expandEntities(String s) throws IOException {
234         if (s.indexOf('&') == -1) return s;
235         StringBuffer sb = new StringBuffer();
236         int i=0;
237         int nextamp = 0;
238         while(nextamp != -1) {
239             nextamp = s.indexOf('&', i);
240             sb.append(nextamp == -1 ? s.substring(i) : s.substring(i, nextamp));
241             if (nextamp == -1) break;
242             if (s.regionMatches(nextamp, "&amp;", 0, 5)) {
243                 sb.append("&");
244                 i = nextamp + 5;
245             } else if (s.regionMatches(nextamp, "&gt;", 0, 4)) {
246                 sb.append(">");
247                 i = nextamp + 4;
248             } else if (s.regionMatches(nextamp, "&lt;", 0, 4)) {
249                 sb.append("<");
250                 i = nextamp + 4;
251             } else if (s.regionMatches(nextamp, "&quot;", 0, 6)) {
252                 sb.append("\"");
253                 i = nextamp + 6;
254             } else if (s.regionMatches(nextamp, "&nbsp;", 0, 6)) {
255                 // FIXME: should have a way to indicate this...
256                 sb.append(" ");
257                 i = nextamp + 6;
258             } else {
259                 sb.append("&");
260                 i = nextamp + 1;
261             }
262         }
263         return sb.toString();
264     }
265
266     // FIXME double check this
267     /** removes all redundant whitespace */
268     private static String removeRedundantWhitespace(String s) {
269
270         if (s.indexOf(' ') == -1 && s.indexOf('\n') == -1 && s.indexOf('\t') == -1 && s.indexOf('\r') == -1) return s;
271
272         int len = s.length();
273         if (cbuf == null || cbuf.length < len) {
274             cbuf = new char[len * 2];
275             sbuf = new StringBuffer(len * 2);
276         }
277         sbuf.setLength(0);
278         s.getChars(0, len, cbuf, 0);
279
280         int last = 0;
281         boolean lastWasWhitespace = false;
282         for(int i=0; i<len; i++) {
283             boolean lastlast = lastWasWhitespace;
284             switch(cbuf[i]) {
285             case '\n': case '\r': case '\t':
286                 cbuf[i] = ' ';
287             case ' ':
288                 lastWasWhitespace = true;
289                 break;
290             default:
291                 lastWasWhitespace = false;
292                 break;
293             }
294             if (lastWasWhitespace && lastlast) {
295                 if (last != i) sbuf.append(cbuf, last, i - last);
296                 last = i+1;
297             }
298         }
299             
300         if (last != len) sbuf.append(cbuf, last, len - last);
301         return sbuf.toString().trim();
302     }
303
304     // CharStream /////////////////////////////////////////////////////////////////////
305
306     private static class CharStream extends PushbackReader {
307         public CharStream(Reader r) { super(r); }
308
309         public char peek() throws IOException {
310             char c = get();
311             unread(c);
312             return c;
313         }
314
315         public char get() throws IOException {
316             int i = read();
317             if (i == -1) throw new EOFException();
318             return (char)i;
319         }
320     }
321
322 }
323