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