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