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