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