]> gitweb.fperrin.net Git - DictionaryPC.git/blob - src/com/hughes/android/dictionary/parser/WikiTokenizer.java
Baseline HTML parsing done, goldens updated!
[DictionaryPC.git] / src / com / hughes / android / dictionary / parser / WikiTokenizer.java
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package com.hughes.android.dictionary.parser;
16
17 import java.util.ArrayList;
18 import java.util.LinkedHashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
23
24 public final class WikiTokenizer {
25   
26   public static interface Callback {
27     void onPlainText(final String text);
28     void onMarkup(WikiTokenizer wikiTokenizer);
29     void onWikiLink(WikiTokenizer wikiTokenizer);
30     void onNewline(WikiTokenizer wikiTokenizer);
31     void onFunction(final WikiTokenizer tokenizer, String functionName, List<String> functionPositionArgs,
32         Map<String, String> functionNamedArgs);
33     void onHeading(WikiTokenizer wikiTokenizer);
34     void onListItem(WikiTokenizer wikiTokenizer);
35     void onComment(WikiTokenizer wikiTokenizer);
36     void onHtml(WikiTokenizer wikiTokenizer);
37   }
38   
39   public static class DoNothingCallback implements Callback {
40
41     @Override
42     public void onPlainText(String text) {
43     }
44
45     @Override
46     public void onMarkup(WikiTokenizer wikiTokenizer) {
47     }
48
49     @Override
50     public void onWikiLink(WikiTokenizer wikiTokenizer) {
51     }
52
53     @Override
54     public void onNewline(WikiTokenizer wikiTokenizer) {
55     }
56
57     @Override
58     public void onFunction(WikiTokenizer tokenizer, String functionName,
59         List<String> functionPositionArgs, Map<String, String> functionNamedArgs) {
60     }
61
62     @Override
63     public void onHeading(WikiTokenizer wikiTokenizer) {
64     }
65
66     @Override
67     public void onListItem(WikiTokenizer wikiTokenizer) {
68     }
69
70     @Override
71     public void onComment(WikiTokenizer wikiTokenizer) {
72     }
73
74     @Override
75     public void onHtml(WikiTokenizer wikiTokenizer) {
76     }
77   }
78   
79   //private static final Pattern wikiTokenEvent = Pattern.compile("($)", Pattern.MULTILINE);
80   private static final Pattern wikiTokenEvent = Pattern.compile("(" +
81                 "\\{\\{|\\}\\}|" +
82                 "\\[\\[|\\]\\]|" +
83                 "\\||" +  // Need the | because we might have to find unescaped pipes
84         "=|" +  // Need the = because we might have to find unescaped =
85                 "<!--|" +
86                 "''|" +
87                 "$)", Pattern.MULTILINE);
88   private static final String listChars = "*#:;";
89   
90     
91   final String wikiText;
92   final Matcher matcher;
93
94   boolean justReturnedNewline = true;
95   int lastLineStart = 0;
96   int end = 0;
97   int start = -1;
98
99   final List<String> errors = new ArrayList<String>();
100   final List<String> tokenStack = new ArrayList<String>();
101   
102
103   private String headingWikiText;
104   private int headingDepth;
105   private int listPrefixEnd;
106   private boolean isPlainText;
107   private boolean isMarkup;
108   private boolean isComment;
109   private boolean isFunction;
110   private boolean isWikiLink;
111   private boolean isHtml;
112   private int firstUnescapedPipePos;
113   
114   private int lastUnescapedPipePos;
115   private int lastUnescapedEqualsPos;
116   private final List<String> positionArgs = new ArrayList<String>();
117   private final Map<String,String> namedArgs = new LinkedHashMap<String,String>();
118   
119
120   public WikiTokenizer(final String wikiText) {
121     this(wikiText, true);
122   }
123
124   public WikiTokenizer(final String wikiText, final boolean isNewline) {
125     this.wikiText = wikiText.replaceAll("\u2028", "\n");
126     this.matcher = wikiTokenEvent.matcher(wikiText);
127     justReturnedNewline = isNewline;
128   }
129
130   private void clear() {
131     errors.clear();
132     tokenStack.clear();
133
134     headingWikiText = null;
135     headingDepth = -1;
136     listPrefixEnd = -1;
137     isPlainText = false;
138     isMarkup = false;
139     isComment = false;
140     isFunction = false;
141     isWikiLink = false;
142     isHtml = false;
143     
144     firstUnescapedPipePos = -1;
145     lastUnescapedPipePos = -1;
146     lastUnescapedEqualsPos = -1;
147     positionArgs.clear();
148     namedArgs.clear();
149   }
150
151   private static final Pattern POSSIBLE_WIKI_TEXT = Pattern.compile(
152       "\\{\\{|" +
153       "\\[\\[|" +
154       "<!--|" +
155       "''|" +
156       "[\n]"
157       );
158
159   public static void dispatch(final String wikiText, final boolean isNewline, final Callback callback) {
160     // Optimization...
161     if (!POSSIBLE_WIKI_TEXT.matcher(wikiText).find()) {
162       callback.onPlainText(wikiText);
163     } else {
164       final WikiTokenizer tokenizer = new WikiTokenizer(wikiText, isNewline);
165       while (tokenizer.nextToken() != null) {
166         if (tokenizer.isPlainText()) {
167           callback.onPlainText(tokenizer.token());
168         } else if (tokenizer.isMarkup()) {
169           callback.onMarkup(tokenizer);
170         } else if (tokenizer.isWikiLink) {
171           callback.onWikiLink(tokenizer);
172         } else if (tokenizer.isNewline()) {
173           callback.onNewline(tokenizer);
174         } else if (tokenizer.isFunction()) {
175           callback.onFunction(tokenizer, tokenizer.functionName(), tokenizer.functionPositionArgs(), tokenizer.functionNamedArgs());
176         } else if (tokenizer.isHeading()) {
177           callback.onHeading(tokenizer);
178         } else if (tokenizer.isListItem()) {
179           callback.onListItem(tokenizer);
180         } else if (tokenizer.isComment()) {
181           callback.onComment(tokenizer);
182         } else if (tokenizer.isHtml()) {
183           callback.onHtml(tokenizer);
184         } else if (!tokenizer.errors.isEmpty()) {
185           // Log was already printed....
186         } else {
187           throw new IllegalStateException("Unknown wiki state: " + tokenizer.token());
188         }
189       }
190     }
191   }
192   
193   public List<String> errors() {
194     return errors;
195   }
196   
197   public boolean isNewline() {
198     return justReturnedNewline;
199   }
200   
201   public void returnToLineStart() {
202     end = start = lastLineStart;
203     justReturnedNewline = true;
204   }
205   
206   public boolean isHeading() {
207     return headingWikiText != null;
208   }
209   
210   public String headingWikiText() {
211     assert isHeading();
212     return headingWikiText;
213   }
214   
215   public int headingDepth() {
216     assert isHeading();
217     return headingDepth;
218   }
219   
220   public boolean isMarkup() {
221     return isMarkup;
222   }
223
224   public boolean isComment() {
225     return isComment;
226   }
227
228   public boolean isListItem() {
229     return listPrefixEnd != -1;
230   }
231   
232   public String listItemPrefix() {
233     assert isListItem();
234     return wikiText.substring(start, listPrefixEnd);
235   }
236   
237   public static String getListTag(char c) {
238     if (c == '#') {
239       return "ol";
240     }
241     return "ul";
242   }
243
244   public String listItemWikiText() {
245     assert isListItem();
246     return wikiText.substring(listPrefixEnd, end);
247   }
248   
249   public boolean isFunction() {
250     return isFunction;
251   }
252
253   public String functionName() {
254     assert isFunction();
255     // "{{.."
256     if (firstUnescapedPipePos != -1) {
257       return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos).trim());
258     }
259     return trimNewlines(wikiText.substring(start + 2, end - 2).trim());
260   }
261   
262   public List<String> functionPositionArgs() {
263     return positionArgs;
264   }
265
266   public Map<String, String> functionNamedArgs() {
267     return namedArgs;
268   }
269
270   public boolean isPlainText() {
271     return isPlainText;
272   }
273
274   public boolean isWikiLink() {
275     return isWikiLink;
276   }
277
278   public String wikiLinkText() {
279     assert isWikiLink();
280     // "[[.."
281     if (lastUnescapedPipePos != -1) {
282       return trimNewlines(wikiText.substring(lastUnescapedPipePos + 1, end - 2));
283     }
284     assert start + 2 < wikiText.length() && end >= 2: wikiText;
285     return trimNewlines(wikiText.substring(start + 2, end - 2));
286   }
287
288   public String wikiLinkDest() {
289     assert isWikiLink();
290     // "[[.."
291     if (firstUnescapedPipePos != -1) {
292       return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos));
293     }
294     return null;
295   }
296   
297   public boolean isHtml() {
298     return isHtml;
299   }
300
301   public boolean remainderStartsWith(final String prefix) {
302     return wikiText.startsWith(prefix, start);
303   }
304   
305   public void nextLine() {
306     final int oldStart = start;
307     while(nextToken() != null && !isNewline()) {}
308     if (isNewline()) {
309       --end;
310     }
311     start = oldStart;
312   }
313
314   
315   public WikiTokenizer nextToken() {
316     this.clear();
317     
318     start = end;
319     
320     if (justReturnedNewline) {
321       lastLineStart = start;
322     }
323     
324     try {
325     
326     final int len = wikiText.length();
327     if (start >= len) {
328       return null;
329     }
330     
331     // Eat a newline if we're looking at one:
332     final boolean atNewline = wikiText.charAt(end) == '\n' || wikiText.charAt(end) == '\u2028';
333     if (atNewline) {
334       justReturnedNewline = true;
335       ++end;
336       return this;
337     }
338     
339     if (justReturnedNewline) {    
340       justReturnedNewline = false;
341
342       final char firstChar = wikiText.charAt(end);
343       if (firstChar == '=') {
344         final int headerStart = end;
345         // Skip ===...
346         while (++end < len && wikiText.charAt(end) == '=') {}
347         final int headerTitleStart = end;
348         headingDepth = headerTitleStart - headerStart;
349         // Skip non-=...
350         if (end < len) {
351           final int nextNewline = safeIndexOf(wikiText, end, "\n", "\n");
352           final int closingEquals = escapedFindEnd(end, "=");
353           if (wikiText.charAt(closingEquals - 1) == '=') {
354             end = closingEquals - 1;
355           } else {
356             end = nextNewline;
357           }
358         }
359         final int headerTitleEnd = end;
360         headingWikiText = wikiText.substring(headerTitleStart, headerTitleEnd);
361         // Skip ===...
362         while (end < len && ++end < len && wikiText.charAt(end) == '=') {}
363         final int headerEnd = end;
364         if (headerEnd - headerTitleEnd != headingDepth) {
365           errors.add("Mismatched header depth: " + token());
366         }
367         return this;
368       }
369       if (listChars.indexOf(firstChar) != -1) {
370         while (++end < len && listChars.indexOf(wikiText.charAt(end)) != -1) {}
371         listPrefixEnd = end;
372         end = escapedFindEnd(start, "\n");
373         return this;
374       }
375     }
376
377     if (wikiText.startsWith("'''", start)) {
378       isMarkup = true;
379       end = start + 3;
380       return this;
381     }
382     
383     if (wikiText.startsWith("''", start)) {
384       isMarkup = true;
385       end = start + 2;
386       return this;
387     }
388
389     if (wikiText.startsWith("[[", start)) {
390       end = escapedFindEnd(start + 2, "]]");
391       isWikiLink = errors.isEmpty();
392       return this;
393     }
394
395     if (wikiText.startsWith("{{", start)) {      
396       end = escapedFindEnd(start + 2, "}}");
397       isFunction = errors.isEmpty();
398       return this;
399     }
400
401     if (wikiText.startsWith("<pre>", start)) {
402       end = safeIndexOf(wikiText, start, "</pre>", "\n");
403       isHtml = true;
404       return this;
405     }
406
407     if (wikiText.startsWith("<math>", start)) {
408       end = safeIndexOf(wikiText, start, "</math>", "\n");
409       isHtml = true;
410       return this;
411     }
412
413     if (wikiText.startsWith("<!--", start)) {
414       isComment = true;
415       end = safeIndexOf(wikiText, start, "-->", "\n");
416       return this;
417     }
418
419     if (wikiText.startsWith("}}", start) || wikiText.startsWith("]]", start)) {
420       errors.add("Close without open!");
421       end += 2;
422       return this;
423     }
424
425     if (wikiText.charAt(start) == '|' || wikiText.charAt(start) == '=') {
426       isPlainText = true;
427       ++end;
428       return this;
429     }
430
431     
432     if (this.matcher.find(start)) {
433       end = this.matcher.start(1);
434       isPlainText = true;
435       if (end == start) {
436         errors.add("Empty group: " + this.matcher.group());
437         assert false;
438       }
439       return this;
440     }
441     
442     end = wikiText.length();
443     return this;
444     
445     } finally {
446       if (!errors.isEmpty()) {
447         System.err.println("Errors: " + errors + ", token=" + token());
448       }
449     }
450     
451   }
452   
453   public String token() {
454     final String token = wikiText.substring(start, end);
455     assert token.equals("\n") || !token.endsWith("\n") : "token='" + token + "'";
456     return token;
457   }
458   
459   private int escapedFindEnd(final int start, final String toFind) {
460     assert tokenStack.isEmpty();
461     
462     final boolean insideFunction = toFind.equals("}}");
463     
464     int end = start;
465     int firstNewline = -1;
466     while (end < wikiText.length()) {
467       if (matcher.find(end)) {
468         final String matchText = matcher.group();
469         final int matchStart = matcher.start();
470         
471         assert matcher.end() > end || matchText.length() == 0: "Group=" + matcher.group();
472         if (matchText.length() == 0) {
473           assert matchStart == wikiText.length() || wikiText.charAt(matchStart) == '\n' : wikiText + ", " + matchStart;
474           if (firstNewline == -1) {
475             firstNewline = matcher.end();
476           }
477           if (tokenStack.isEmpty() && toFind.equals("\n")) {
478             return matchStart;
479           }
480           ++end;
481         } else if (tokenStack.isEmpty() && matchText.equals(toFind)) {
482           // The normal return....
483           if (insideFunction) {
484             addFunctionArg(insideFunction, matchStart);
485           }
486           return matcher.end();
487         } else if (matchText.equals("[[") || matchText.equals("{{")) {
488           tokenStack.add(matchText);
489         } else if (matchText.equals("]]") || matchText.equals("}}")) {
490           if (tokenStack.size() > 0) {
491             final String removed = tokenStack.remove(tokenStack.size() - 1);
492             if (removed.equals("{{") && !matcher.group().equals("}}")) {
493               errors.add("Unmatched {{ error: " + wikiText.substring(start));
494               return safeIndexOf(wikiText, start, "\n", "\n");
495             } else if (removed.equals("[[") && !matcher.group().equals("]]")) {
496               errors.add("Unmatched [[ error: " + wikiText.substring(start));
497               return safeIndexOf(wikiText, start, "\n", "\n");
498             }
499           } else {
500             errors.add("Pop too many error: " + wikiText.substring(start).replaceAll("\n", "\\\\n"));
501             // If we were looking for a newline
502             return safeIndexOf(wikiText, start, "\n", "\n");
503           }
504         } else if (matchText.equals("|")) { 
505           if (tokenStack.isEmpty()) {
506             addFunctionArg(insideFunction, matchStart);
507           }
508         } else if (matchText.equals("=")) {
509           if (tokenStack.isEmpty()) {
510             lastUnescapedEqualsPos = matchStart;
511           }
512           // Do nothing.  These can match spuriously, and if it's not the thing
513           // we're looking for, keep on going.
514         } else if (matchText.equals("<!--")) {
515           end = wikiText.indexOf("-->");
516           if (end == -1) {
517             errors.add("Unmatched <!-- error: " + wikiText.substring(start));
518             return safeIndexOf(wikiText, start, "\n", "\n");
519           }
520         } else if (matchText.equals("''")) {
521           // Don't care.
522         } else {
523           assert false : "Match text='" + matchText + "'";
524           throw new IllegalStateException();
525         }
526       } else {
527         // Hmmm, we didn't find the closing symbol we were looking for...
528         errors.add("Couldn't find: " + toFind + ", "+ wikiText.substring(start));
529         return safeIndexOf(wikiText, start, "\n", "\n");
530       }
531       
532       // Inside the while loop.  Just go forward.
533       end = Math.max(end, matcher.end());
534     }
535     if (toFind.equals("\n") && tokenStack.isEmpty()) {
536       // We were looking for the end, we got it.
537       return end;
538     }
539     if (firstNewline != -1) {
540       errors.add("Couldn't find: " + toFind + ", "+ wikiText.substring(start));
541       return firstNewline;
542     }
543     return end;
544   }
545
546   private void addFunctionArg(final boolean insideFunction, final int matchStart) {
547     if (firstUnescapedPipePos == -1) {
548       firstUnescapedPipePos = lastUnescapedPipePos = matchStart;
549     } else if (insideFunction) {
550       if (lastUnescapedEqualsPos > lastUnescapedPipePos) {
551         final String key = wikiText.substring(lastUnescapedPipePos + 1, lastUnescapedEqualsPos);
552         final String value = wikiText.substring(lastUnescapedEqualsPos + 1, matchStart);
553         namedArgs.put(trimNewlines(key), trimNewlines(value));
554       } else {
555         final String value = wikiText.substring(lastUnescapedPipePos + 1, matchStart);
556         positionArgs.add(trimNewlines(value));
557       }
558     }
559     lastUnescapedPipePos = matchStart;
560   }
561   
562   static final String trimNewlines(String s) {
563     while (s.startsWith("\n")) {
564       s = s.substring(1);
565     }
566     while (s.endsWith("\n")) {
567       s = s.substring(0, s.length() - 1);
568     }
569     return s.replaceAll("\n", " ");
570   }
571
572   static int safeIndexOf(final String s, final int start, final String target, final String backup) {
573     int close = s.indexOf(target, start);
574     if (close != -1) {
575       // Don't step over a \n.
576       return close + (target.equals("\n") ? 0 : target.length());
577     }
578     close = s.indexOf(backup, start);
579     if (close != -1) {
580       return close + (backup.equals("\n") ? 0 : backup.length());
581     }
582     return s.length();
583   }
584
585   public static String toPlainText(final String wikiText) {
586     final WikiTokenizer wikiTokenizer = new WikiTokenizer(wikiText);
587     final StringBuilder builder = new StringBuilder();
588     while (wikiTokenizer.nextToken() != null) {
589       if (wikiTokenizer.isPlainText()) {
590         builder.append(wikiTokenizer.token());
591       } else if (wikiTokenizer.isWikiLink()) {
592         builder.append(wikiTokenizer.wikiLinkText());
593       } else if (wikiTokenizer.isNewline()) {
594         builder.append("\n");
595       } else if (wikiTokenizer.isFunction()) {
596         builder.append(wikiTokenizer.token());
597       }
598     }
599     return builder.toString();
600   }
601
602   public static StringBuilder appendFunction(final StringBuilder builder, final String name, List<String> args,
603       final Map<String, String> namedArgs) {
604     builder.append(name);
605     for (final String arg : args) {
606       builder.append("|").append(arg);
607     }
608     for (final Map.Entry<String, String> entry : namedArgs.entrySet()) {
609       builder.append("|").append(entry.getKey()).append("=").append(entry.getValue());
610     }
611     return builder;
612   }
613
614 }