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