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