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