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