]> gitweb.fperrin.net Git - DictionaryPC.git/blob - src/com/hughes/android/dictionary/parser/WikiTokenizer.java
111f131be2308cb40ab8e8d5739314fc0a6dde89
[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 String listItemWikiText() {
238     assert isListItem();
239     return wikiText.substring(listPrefixEnd, end);
240   }
241   
242   public boolean isFunction() {
243     return isFunction;
244   }
245
246   public String functionName() {
247     assert isFunction();
248     // "{{.."
249     if (firstUnescapedPipePos != -1) {
250       return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos).trim());
251     }
252     return trimNewlines(wikiText.substring(start + 2, end - 2).trim());
253   }
254   
255   public List<String> functionPositionArgs() {
256     return positionArgs;
257   }
258
259   public Map<String, String> functionNamedArgs() {
260     return namedArgs;
261   }
262
263   public boolean isPlainText() {
264     return isPlainText;
265   }
266
267   public boolean isWikiLink() {
268     return isWikiLink;
269   }
270
271   public String wikiLinkText() {
272     assert isWikiLink();
273     // "[[.."
274     if (lastUnescapedPipePos != -1) {
275       return trimNewlines(wikiText.substring(lastUnescapedPipePos + 1, end - 2));
276     }
277     assert start + 2 < wikiText.length() && end >= 2: wikiText;
278     return trimNewlines(wikiText.substring(start + 2, end - 2));
279   }
280
281   public String wikiLinkDest() {
282     assert isWikiLink();
283     // "[[.."
284     if (firstUnescapedPipePos != -1) {
285       return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos));
286     }
287     return null;
288   }
289   
290   public boolean isHtml() {
291     return isHtml;
292   }
293
294   public boolean remainderStartsWith(final String prefix) {
295     return wikiText.startsWith(prefix, start);
296   }
297   
298   public void nextLine() {
299     final int oldStart = start;
300     while(nextToken() != null && !isNewline()) {}
301     if (isNewline()) {
302       --end;
303     }
304     start = oldStart;
305   }
306
307   
308   public WikiTokenizer nextToken() {
309     this.clear();
310     
311     start = end;
312     
313     if (justReturnedNewline) {
314       lastLineStart = start;
315     }
316     
317     try {
318     
319     final int len = wikiText.length();
320     if (start >= len) {
321       return null;
322     }
323     
324     // Eat a newline if we're looking at one:
325     final boolean atNewline = wikiText.charAt(end) == '\n' || wikiText.charAt(end) == '\u2028';
326     if (atNewline) {
327       justReturnedNewline = true;
328       ++end;
329       return this;
330     }
331     
332     if (justReturnedNewline) {    
333       justReturnedNewline = false;
334
335       final char firstChar = wikiText.charAt(end);
336       if (firstChar == '=') {
337         final int headerStart = end;
338         // Skip ===...
339         while (++end < len && wikiText.charAt(end) == '=') {}
340         final int headerTitleStart = end;
341         headingDepth = headerTitleStart - headerStart;
342         // Skip non-=...
343         if (end < len) {
344           final int nextNewline = safeIndexOf(wikiText, end, "\n", "\n");
345           final int closingEquals = escapedFindEnd(end, "=");
346           if (wikiText.charAt(closingEquals - 1) == '=') {
347             end = closingEquals - 1;
348           } else {
349             end = nextNewline;
350           }
351         }
352         final int headerTitleEnd = end;
353         headingWikiText = wikiText.substring(headerTitleStart, headerTitleEnd);
354         // Skip ===...
355         while (end < len && ++end < len && wikiText.charAt(end) == '=') {}
356         final int headerEnd = end;
357         if (headerEnd - headerTitleEnd != headingDepth) {
358           errors.add("Mismatched header depth: " + token());
359         }
360         return this;
361       }
362       if (listChars.indexOf(firstChar) != -1) {
363         while (++end < len && listChars.indexOf(wikiText.charAt(end)) != -1) {}
364         listPrefixEnd = end;
365         end = escapedFindEnd(start, "\n");
366         return this;
367       }
368     }
369
370     if (wikiText.startsWith("'''", start)) {
371       isMarkup = true;
372       end = start + 3;
373       return this;
374     }
375     
376     if (wikiText.startsWith("''", start)) {
377       isMarkup = true;
378       end = start + 2;
379       return this;
380     }
381
382     if (wikiText.startsWith("[[", start)) {
383       end = escapedFindEnd(start + 2, "]]");
384       isWikiLink = errors.isEmpty();
385       return this;
386     }
387
388     if (wikiText.startsWith("{{", start)) {      
389       end = escapedFindEnd(start + 2, "}}");
390       isFunction = errors.isEmpty();
391       return this;
392     }
393
394     if (wikiText.startsWith("<pre>", start)) {
395       end = safeIndexOf(wikiText, start, "</pre>", "\n");
396       isHtml = true;
397       return this;
398     }
399
400     if (wikiText.startsWith("<math>", start)) {
401       end = safeIndexOf(wikiText, start, "</math>", "\n");
402       isHtml = true;
403       return this;
404     }
405
406     if (wikiText.startsWith("<!--", start)) {
407       isComment = true;
408       end = safeIndexOf(wikiText, start, "-->", "\n");
409       return this;
410     }
411
412     if (wikiText.startsWith("}}", start) || wikiText.startsWith("]]", start)) {
413       errors.add("Close without open!");
414       end += 2;
415       return this;
416     }
417
418     if (wikiText.charAt(start) == '|' || wikiText.charAt(start) == '=') {
419       isPlainText = true;
420       ++end;
421       return this;
422     }
423
424     
425     if (this.matcher.find(start)) {
426       end = this.matcher.start(1);
427       isPlainText = true;
428       if (end == start) {
429         errors.add("Empty group: " + this.matcher.group());
430         assert false;
431       }
432       return this;
433     }
434     
435     end = wikiText.length();
436     return this;
437     
438     } finally {
439       if (!errors.isEmpty()) {
440         System.err.println("Errors: " + errors + ", token=" + token());
441       }
442     }
443     
444   }
445   
446   public String token() {
447     final String token = wikiText.substring(start, end);
448     assert token.equals("\n") || !token.endsWith("\n") : "token='" + token + "'";
449     return token;
450   }
451   
452   private int escapedFindEnd(final int start, final String toFind) {
453     assert tokenStack.isEmpty();
454     
455     final boolean insideFunction = toFind.equals("}}");
456     
457     int end = start;
458     int firstNewline = -1;
459     while (end < wikiText.length()) {
460       if (matcher.find(end)) {
461         final String matchText = matcher.group();
462         final int matchStart = matcher.start();
463         
464         assert matcher.end() > end || matchText.length() == 0: "Group=" + matcher.group();
465         if (matchText.length() == 0) {
466           assert matchStart == wikiText.length() || wikiText.charAt(matchStart) == '\n' : wikiText + ", " + matchStart;
467           if (firstNewline == -1) {
468             firstNewline = matcher.end();
469           }
470           if (tokenStack.isEmpty() && toFind.equals("\n")) {
471             return matchStart;
472           }
473           ++end;
474         } else if (tokenStack.isEmpty() && matchText.equals(toFind)) {
475           // The normal return....
476           if (insideFunction) {
477             addFunctionArg(insideFunction, matchStart);
478           }
479           return matcher.end();
480         } else if (matchText.equals("[[") || matchText.equals("{{")) {
481           tokenStack.add(matchText);
482         } else if (matchText.equals("]]") || matchText.equals("}}")) {
483           if (tokenStack.size() > 0) {
484             final String removed = tokenStack.remove(tokenStack.size() - 1);
485             if (removed.equals("{{") && !matcher.group().equals("}}")) {
486               errors.add("Unmatched {{ error: " + wikiText.substring(start));
487               return safeIndexOf(wikiText, start, "\n", "\n");
488             } else if (removed.equals("[[") && !matcher.group().equals("]]")) {
489               errors.add("Unmatched [[ error: " + wikiText.substring(start));
490               return safeIndexOf(wikiText, start, "\n", "\n");
491             }
492           } else {
493             errors.add("Pop too many error: " + wikiText.substring(start).replaceAll("\n", "\\\\n"));
494             // If we were looking for a newline
495             return safeIndexOf(wikiText, start, "\n", "\n");
496           }
497         } else if (matchText.equals("|")) { 
498           if (tokenStack.isEmpty()) {
499             addFunctionArg(insideFunction, matchStart);
500           }
501         } else if (matchText.equals("=")) {
502           if (tokenStack.isEmpty()) {
503             lastUnescapedEqualsPos = matchStart;
504           }
505           // Do nothing.  These can match spuriously, and if it's not the thing
506           // we're looking for, keep on going.
507         } else if (matchText.equals("<!--")) {
508           end = wikiText.indexOf("-->");
509           if (end == -1) {
510             errors.add("Unmatched <!-- error: " + wikiText.substring(start));
511             return safeIndexOf(wikiText, start, "\n", "\n");
512           }
513         } else if (matchText.equals("''")) {
514           // Don't care.
515         } else {
516           assert false : "Match text='" + matchText + "'";
517           throw new IllegalStateException();
518         }
519       } else {
520         // Hmmm, we didn't find the closing symbol we were looking for...
521         errors.add("Couldn't find: " + toFind + ", "+ wikiText.substring(start));
522         return safeIndexOf(wikiText, start, "\n", "\n");
523       }
524       
525       // Inside the while loop.  Just go forward.
526       end = Math.max(end, matcher.end());
527     }
528     if (toFind.equals("\n") && tokenStack.isEmpty()) {
529       // We were looking for the end, we got it.
530       return end;
531     }
532     if (firstNewline != -1) {
533       errors.add("Couldn't find: " + toFind + ", "+ wikiText.substring(start));
534       return firstNewline;
535     }
536     return end;
537   }
538
539   private void addFunctionArg(final boolean insideFunction, final int matchStart) {
540     if (firstUnescapedPipePos == -1) {
541       firstUnescapedPipePos = lastUnescapedPipePos = matchStart;
542     } else if (insideFunction) {
543       if (lastUnescapedEqualsPos > lastUnescapedPipePos) {
544         final String key = wikiText.substring(lastUnescapedPipePos + 1, lastUnescapedEqualsPos);
545         final String value = wikiText.substring(lastUnescapedEqualsPos + 1, matchStart);
546         namedArgs.put(trimNewlines(key), trimNewlines(value));
547       } else {
548         final String value = wikiText.substring(lastUnescapedPipePos + 1, matchStart);
549         positionArgs.add(trimNewlines(value));
550       }
551     }
552     lastUnescapedPipePos = matchStart;
553   }
554   
555   static final String trimNewlines(String s) {
556     while (s.startsWith("\n")) {
557       s = s.substring(1);
558     }
559     while (s.endsWith("\n")) {
560       s = s.substring(0, s.length() - 1);
561     }
562     return s.replaceAll("\n", " ");
563   }
564
565   static int safeIndexOf(final String s, final int start, final String target, final String backup) {
566     int close = s.indexOf(target, start);
567     if (close != -1) {
568       // Don't step over a \n.
569       return close + (target.equals("\n") ? 0 : target.length());
570     }
571     close = s.indexOf(backup, start);
572     if (close != -1) {
573       return close + (backup.equals("\n") ? 0 : backup.length());
574     }
575     return s.length();
576   }
577
578   public static String toPlainText(final String wikiText) {
579     final WikiTokenizer wikiTokenizer = new WikiTokenizer(wikiText);
580     final StringBuilder builder = new StringBuilder();
581     while (wikiTokenizer.nextToken() != null) {
582       if (wikiTokenizer.isPlainText()) {
583         builder.append(wikiTokenizer.token());
584       } else if (wikiTokenizer.isWikiLink()) {
585         builder.append(wikiTokenizer.wikiLinkText());
586       } else if (wikiTokenizer.isNewline()) {
587         builder.append("\n");
588       } else if (wikiTokenizer.isFunction()) {
589         builder.append(wikiTokenizer.token());
590       }
591     }
592     return builder.toString();
593   }
594
595   public static StringBuilder appendFunction(final StringBuilder builder, final String name, List<String> args,
596       final Map<String, String> namedArgs) {
597     builder.append(name);
598     for (final String arg : args) {
599       builder.append("|").append(arg);
600     }
601     for (final Map.Entry<String, String> entry : namedArgs.entrySet()) {
602       builder.append("|").append(entry.getKey()).append("=").append(entry.getValue());
603     }
604     return builder;
605   }
606
607 }