001: package com.technoetic.xplanner.wiki;
002:
003: import com.technoetic.xplanner.XPlannerProperties;
004: import org.apache.log4j.Logger;
005: import org.apache.oro.text.perl.MalformedPerl5PatternException;
006: import org.apache.oro.text.perl.Perl5Util;
007: import org.apache.oro.text.regex.PatternMatcherInput;
008:
009: import java.io.BufferedReader;
010: import java.io.StringReader;
011: import java.util.ArrayList;
012: import java.util.HashMap;
013: import java.util.Map;
014: import java.util.Properties;
015:
016: /**
017: * WackoWiki formatter
018: *
019: * www.wackowiki.com
020: *
021: * @author Valery Kholodkov
022: */
023:
024: //DEBT: Completely duplicated from TwikiFormat
025: public class WackoWikiFormat implements WikiFormat {
026: private final Logger log = Logger.getLogger(getClass());
027: private Perl5Util perl = new Perl5Util();
028: private ArrayList codeStack = new ArrayList();
029: private static final String mailSubstitution = "s/([\\s\\(])(?:mailto\\:)*([a-zA-Z0-9\\-\\_\\.\\+]+)\\@"
030: + "([a-zA-Z0-9\\-\\_\\.]+)\\.([a-zA-Z0-9\\-\\_]+)(?=[\\s\\.\\,\\;\\:\\!\\?\\)])/"
031: + "$1<a href=\"mailto:$2@$3.$4\">$2@$3.$4<\\/a>/go";
032: private static final String fancyHr = "s@^([a-zA-Z0-9]+)----*@<table width=\"100%\"><tr><td valign=\"bottom\"><h2>$1</h2></td>"
033: + "<td width=\"98%\" valign=\"middle\"><hr /></td></tr></table>@o";
034: private static final String escapeRegexp = "s@([\\*\\?\\.\\[\\](\\)])@\\\\$1@g";
035: private static final String urlPattern = "m@(^|[-*\\W])((\\w+):([\\w\\$\\-_\\@\\.&\\+\\?/:#%~=]+))(\\[([^\\]]+)\\]|)@";
036: private static final String headerPattern = "^\\s*=(=+)([^=]+)=+\\s*$"; // '==Header=='
037: private static final String wikiWordPattern = "(^|[^\\w:/])(\\w+\\.|)([A-Z][a-z]\\w*[A-Z][a-z]\\w*)(\\b|$)";
038: private static final String wikiWordMatch = "m/" + wikiWordPattern
039: + "/";
040: private static Map schemeHandlers;
041: private ExternalWikiAdapter externalWikiAdapter = null;
042: private MalformedPerl5PatternException malformedPattern = null;
043: private Properties properties = XPlannerProperties.getProperties();
044:
045: public WackoWikiFormat() {
046: schemeHandlers = new HashMap();
047: if (properties.getProperty("wackowiki.wikiadapter") != null) {
048: try {
049: externalWikiAdapter = (ExternalWikiAdapter) Class
050: .forName(
051: properties
052: .getProperty("wackowiki.wikiadapter"))
053: .newInstance();
054: } catch (Exception e) {
055: log.error("Cannot instaintiate wiki adapter", e);
056: throw new RuntimeException(e);
057: }
058: } else {
059: externalWikiAdapter = new GenericWikiAdapter();
060: }
061: }
062:
063: public void setSchemeHandlers(Map schemeHandlers) {
064: WackoWikiFormat.schemeHandlers = schemeHandlers;
065: }
066:
067: public String format(String text) {
068: boolean inPreformattedSection = false;
069: boolean inList = false;
070: boolean inTable = false;
071: PatternMatcherInput patternMatcherInput = new PatternMatcherInput(
072: "");
073: BufferedReader reader = new BufferedReader(new StringReader(
074: text));
075: StringBuffer outputText = new StringBuffer();
076: try {
077: String line = reader.readLine();
078: while (line != null) {
079: try {
080: if (perl.match("m|%%|i", line)) {
081: if (!inPreformattedSection) {
082: line = perl.substitute("s|%%|<pre>|go",
083: line);
084: } else {
085: line = perl.substitute("s|%%|</pre>|go",
086: line);
087: }
088: inPreformattedSection = !inPreformattedSection;
089: }
090: boolean escapeBrackets = (new Boolean(properties
091: .getProperty(
092: WikiFormat.ESCAPE_BRACKETS_KEY,
093: "true"))).booleanValue();
094: if (inPreformattedSection) {
095: line = perl.substitute("s/&/&/go", line);
096: if (escapeBrackets) {
097: line = perl.substitute("s/</</go", line);
098: line = perl.substitute("s/>/>/go", line);
099: }
100: line = perl.substitute(
101: "s/<pre>/<pre>/go", line);
102: } else {
103: // Blockquote
104: line = perl.substitute(
105: "s|^>(.*?)$|> <cite> $1 </cite>|go",
106: line);
107:
108: // Embedded HTML - \263 is a special translation token
109: // -- Allow standalone "<!--"
110: line = perl.substitute("s/<(!--)/\\\\263$1/go",
111: line);
112: // -- Allow standalone "-->"
113: line = perl.substitute("s/(--)>/$1\\\\263/go",
114: line);
115: line = perl.substitute(
116: "s/<(\\S.*?)>/\\\\263$1$\\\\263/g",
117: line);
118: if (escapeBrackets) {
119: line = perl.substitute("s/</</go", line);
120: line = perl.substitute("s/>/>/go", line);
121: }
122: line = perl
123: .substitute(
124: "s/\\\\263(\\S.*?)\\\\263/<$1>/g",
125: line);
126: line = perl.substitute("s/(--)\\\\263/$1>/go",
127: line);
128: line = perl.substitute("s/\\\\263(!--)/<$1/go",
129: line);
130:
131: // Entities
132: line = perl.substitute(
133: "s/&(\\w+?);/\\\\236$1;/g", line); // "&abc;"
134: line = perl.substitute(
135: "s/&(#[0-9]+);/\\\\236$1;/g", line); // "{"
136: line = perl.substitute("s/&/&/go", line); // escape standalone "&"
137: line = perl.substitute("s/\\\\236/&/go", line);
138:
139: // Headings
140: // ==Header== rule
141: patternMatcherInput.setInput(line);
142: while (perl.match("m|" + headerPattern + "|",
143: patternMatcherInput)) {
144: line = perl.substitute("s@"
145: + headerPattern
146: + "@"
147: + makeAnchorHeading(perl.group(2),
148: perl.group(1).length())
149: + "@goi", line);
150: }
151:
152: // Lists etc.
153: // Grabbed from TWiki formatter.
154: while (perl.match("m/^(\t*) /", line)) {
155: line = perl.substitute(
156: "s/^(\t*) /$1\t/o", line);
157: }
158: if (perl.match("m/^\\s*$/", line)) {
159: line = perl.substitute("s/^\\s*$/<p\\/>/o",
160: line);
161: inList = false;
162: }
163: if (perl.match("m/^(\\S+?)/", line)) {
164: inList = false;
165: }
166: if (perl.match("m/^(\\t+)(\\S+?):\\s/", line)) {
167: line = perl
168: .substitute(
169: "s/^(\\t+)(\\S+?):\\s/<dt> $2<dd> /o",
170: line);
171: emitCode(outputText, "dl", perl.group(1)
172: .length());
173: inList = true;
174: }
175: if (perl.match("m/^(\\t+)\\* /", line)) {
176: line = perl.substitute(
177: "s/^(\\t+)\\* /<li> /o", line);
178: emitCode(outputText, "ul", perl.group(1)
179: .length());
180: inList = true;
181: }
182: if (perl.match("m/^(\\t+)\\d+\\.?/", line)) {
183: line = perl.substitute(
184: "s/^(\\t+)\\d+\\.? /<li> /o", line);
185: emitCode(outputText, "ol", perl.group(1)
186: .length());
187: inList = true;
188: }
189:
190: if (inList == false) {
191: emitCode(outputText, "", 0);
192: }
193:
194: // Table
195: if (perl.match("m/^(\\s*)\\|(.*)/", line)) {
196: line = perl.substitute("s/^(\\s*)\\|(.*)/"
197: + emitTableRow("", perl.group(2),
198: inTable) + "/", line);
199: inTable = true;
200: } else if (inTable) {
201: outputText.append("</table>");
202: inTable = false;
203: }
204:
205: // Italic
206: if (perl
207: .match(
208: "m/([\\s\\(]*)\\/\\/([^\\s]+?|[^\\s].*?[^\\s])\\/\\/([\\s,.;:!?<)]|$)/",
209: line)) {
210: line = perl
211: .substitute(
212: "s/([\\s\\(]*)\\/\\/([^\\s]+?|[^\\s].*?[^\\s])\\/\\/([\\s,.;:!?)<]|$)/"
213: + "$1<i>$2<\\/i>$3/g",
214: line);
215: }
216:
217: // Underline
218: if (perl
219: .match(
220: "m/([\\s\\(]*)__([^\\s]+?|[^\\s].*?[^\\s])__([\\s,.;:!?)<]|$)/",
221: line)) {
222: line = perl
223: .substitute(
224: "s/([\\s\\(]*)__([^\\s]+?|[^\\s].*?[^\\s])__([\\s,.;:!?)<]|$)/"
225: + "$1<u>$2<\\/u>$3/g",
226: line);
227: }
228:
229: // Monospace
230: if (perl
231: .match(
232: "m/([\\s\\(]*)##([^\\s]+?|[^\\s].*?[^\\s])##([\\s,.;:!?)<]|$)/",
233: line)) {
234: line = perl.substitute(
235: "s/([\\s\\(]*)##([^\\s]+?|[^\\s].*?[^\\s])##([\\s,.;:!?)<]|$)/"
236: + "$1<tt>$2<\\/tt>$3/g",
237: line);
238: }
239:
240: // Small
241: if (perl
242: .match(
243: "m/([\\s\\(]*)\\+\\+([^\\s]+?|[^\\s].*?[^\\s])\\+\\+([\\s,.;:!?)<]|$)/",
244: line)) {
245: line = perl
246: .substitute(
247: "s/([\\s\\(]*)\\+\\+([^\\s]+?|[^\\s].*?[^\\s])\\+\\+([\\s,.;:!?)<]|$)/"
248: + "$1<small>$2<\\/small>$3/g",
249: line);
250: }
251:
252: // Strightthrough
253: if (perl
254: .match(
255: "m/([\\s\\(]*)\\-\\-([^\\s]+?|[^\\s].*?[^\\s])\\-\\-([\\s,.;:!?)<]|$)/",
256: line)) {
257: line = perl
258: .substitute(
259: "s/([\\s\\(]*)\\-\\-([^\\s]+?|[^\\s].*?[^\\s])\\-\\-([\\s,.;:!?)<]|$)/"
260: + "$1<s>$2<\\/s>$3/g",
261: line);
262: }
263:
264: // Strong
265: if (perl
266: .match(
267: "m/([\\s\\(]*)\\*\\*([^\\s]+?|[^\\s].*?[^\\s])\\*\\*([\\s,.;:!?)<]|$)/",
268: line)) {
269: line = perl
270: .substitute(
271: "s/([\\s\\(]*)\\*\\*([^\\s]+?|[^\\s].*?[^\\s])\\*\\*([\\s,.;:!?)<]|$)/"
272: + "$1<strong>$2<\\/strong>$3/g",
273: line);
274: }
275:
276: // Warning
277: if (perl
278: .match(
279: "m/([\\s\\(]*)!!([^\\s]+?|[^\\s].*?[^\\s])!!([\\s,.;:!?)<]|$)/",
280: line)) {
281: line = perl
282: .substitute(
283: "s/([\\s\\(]*)!!([^\\s]+?|[^\\s].*?[^\\s])!!([\\s,.;:!?)<]|$)/"
284: + "$1<font color=\"#FF0000\">$2<\\/font>$3/g",
285: line);
286: }
287:
288: // Question
289: if (perl
290: .match(
291: "m/([\\s\\(]*)\\?\\?([^\\s]+?|[^\\s].*?[^\\s])\\?\\?([\\s,.;:!?)<]|$)/",
292: line)) {
293: line = perl
294: .substitute(
295: "s/([\\s\\(]*)\\?\\?([^\\s]+?|[^\\s].*?[^\\s])\\?\\?([\\s,.;:!?)<]|$)/"
296: + "$1<font color=\"#00FF00\">$2<\\/font>$3/g",
297: line);
298: }
299:
300: // Handle embedded URLs
301: patternMatcherInput.setInput(line);
302: while (perl.match(urlPattern,
303: patternMatcherInput)) {
304: String link = perl.group(0);
305: String previousText = perl.group(1);
306: String scheme = perl.group(3);
307: String location = perl.group(4);
308: String linkText = perl.group(6);
309: String formattedLink = formatLink(
310: previousText, scheme, location,
311: linkText);
312: if (formattedLink != null) {
313: link = perl.substitute(escapeRegexp,
314: link);
315: line = perl.substitute("s@" + link
316: + "@" + formattedLink + "@go",
317: line);
318: }
319: }
320:
321: // Mailto
322: line = perl.substitute(mailSubstitution, line);
323:
324: //# Horizontal rule
325: line = perl.substitute("s/^---+/<hr\\/>/o",
326: line);
327: line = perl.substitute(fancyHr, line);
328:
329: // WikiWord
330: if (externalWikiAdapter != null) {
331: patternMatcherInput.setInput(line);
332: while (perl.match(wikiWordMatch,
333: patternMatcherInput)) {
334: String wikiWord = perl.group(2)
335: + perl.group(3);
336: line = perl
337: .substitute(
338: "s\0"
339: + wikiWord
340: + "\0"
341: +
342: /*perl.group(1)+*/
343: externalWikiAdapter
344: .formatWikiWord(wikiWord)
345: + perl.group(4)
346: + "\0", line);
347: }
348: }
349: }
350: } catch (MalformedPerl5PatternException ex) {
351: // just continue, set flag for testing purposes
352: malformedPattern = ex;
353: }
354: outputText.append(line);
355: outputText.append("<br>");
356: line = reader.readLine();
357: }
358: emitCode(outputText, "", 0);
359: if (inTable) {
360: outputText.append("</table>");
361: }
362: if (inPreformattedSection) {
363: outputText.append("</pre>");
364: }
365: } catch (Exception ex) {
366: log.error("error during formatting", ex);
367: outputText.setLength(0);
368: outputText.append("[Error during formatting]");
369: }
370: return outputText.toString();
371: }
372:
373: public void setProperties(Properties properties) {
374: this .properties = properties;
375: }
376:
377: private String formatLink(String previousText, String scheme,
378: String location, String linkText) {
379: if (scheme.equals("mailto")) {
380: return null;
381: }
382:
383: SchemeHandler handler = (SchemeHandler) schemeHandlers
384: .get(scheme);
385: if (handler != null) {
386: return previousText
387: + handler.translate(properties, scheme, location,
388: linkText);
389: }
390: String url = scheme + ":" + location;
391: if (perl.match("m/http|ftp|gopher|news|file|https/", scheme)) {
392: if (linkText == null) {
393: linkText = url;
394: }
395: if (perl.match("m|\\.(gif|jpg|jpeg|png)(#|$)|i", url)) {
396: return previousText + "<img border=\"0\" src=\"" + url
397: + "\"/>";
398: } else {
399: return previousText + "<a href=\"" + url
400: + "\" target=\"_top\">" + linkText + "</a>";
401: }
402: }
403: return previousText + url + "[" + linkText + "]";
404: }
405:
406: private String makeAnchorName(String text) {
407: text = perl.substitute("s/^[\\s\\#\\_]*//o", text); //no leading space nor '#', '_'
408: text = perl.substitute("s/[\\s\\_]*$//o", text); // no trailing space, nor '_'
409: text = perl.substitute("s/<\\w[^>]*>//goi", text); //remove HTML tags
410: text = perl.substitute("s/[^a-zA-Z0-9]/_/go", text); // only allowed chars
411: text = perl.substitute("s/__+/_/go", text); // remove excessive '_'
412: text = perl.substitute("s/^(.{32})(.*)$/$1/o", text); // limit to 32 chars
413: return text;
414: }
415:
416: private String makeAnchorHeading(String text, int level) {
417: // - Need to build '<nop><h1><a name="atext"> text </a></h1>'
418: // type markup.
419: // - Initial '<nop>' is needed to prevent subsequent matches.
420: // - Need to make sure that <a> tags are not nested, i.e. in
421: // case heading has a WikiName that gets linked
422: String anchorName = makeAnchorName(text);
423: boolean hasAnchor = perl.match("m/<a /i", text)
424: || perl.match("m/\\[\\[/", text)
425: || perl.match("m/(^|[\\*\\s][\\-\\*\\s]*)([A-Z]{3,})/",
426: text)
427: || perl
428: .match(
429: "m/(^|[\\*\\s][\\(\\-\\*\\s]*)([A-Z]+[a-z0-9]*)\\.([A-Z]+[a-z]+[A-Z]+[a-zA-Z0-9]*)/",
430: text)
431: || perl
432: .match(
433: "m/(^|[\\*\\s][\\(\\-\\*\\s]*)([A-Z]+[a-z]+[A-Z]+[a-zA-Z0-9]*)/",
434: text);
435: if (hasAnchor) {
436: text = "<nop><h" + level + "><a name=\"" + anchorName
437: + "\"> </a> " + text + "</h" + level + ">";
438: } else {
439: text = "<nop><h" + level + "><a name=\"" + anchorName
440: + "\"> " + text + " </a></h" + level + ">";
441: }
442: return text;
443: }
444:
445: public void emitCode(StringBuffer result, String code, int depth) {
446: while (codeStack.size() > depth) {
447: String c = (String) codeStack.remove(codeStack.size() - 1);
448: result.append("</").append(c).append(">\n");
449: }
450: while (codeStack.size() < depth) {
451: codeStack.add(code);
452: result.append("<").append(code).append(">\n");
453: }
454:
455: //if( ( $#code > -1 ) && ( $code[$#code] ne $code ) ) {
456: if (!codeStack.isEmpty()
457: && !codeStack.get(codeStack.size() - 1).equals(code)) {
458: result.append("</").append(
459: codeStack.get(codeStack.size() - 1)).append("><")
460: .append(code).append(">\n");
461: codeStack.set(codeStack.size() - 1, code);
462: }
463: }
464:
465: public String emitTableRow(String previousText, String row,
466: boolean inTable) {
467: StringBuffer result = new StringBuffer();
468: if (inTable) {
469: result.append(previousText).append("<tr class=\"twiki\">");
470: } else {
471: result.append(previousText);
472: result
473: .append("<table class=\"twiki\" border=\"1\" cellspacing=\"0\" cellpadding=\"1\">");
474: result.append("<tr class=\"twiki\">");
475: }
476: row = perl.substitute("s/\\t/ /go", row); // change tab to spaces
477: row = perl.substitute("s/\\s*$//o", row); // remove trailing white space
478: while (perl.match("m/(\\|\\|+)/", row)) {
479: // calc COLSPAN
480: row = perl.substitute("s/(\\|\\|+)/\\\\236"
481: + perl.group(1).length() + "\\|/go", row);
482: }
483:
484: ArrayList cells = new ArrayList();
485: perl.split(cells, "/\\|/", row);
486: for (int i = 0, n = cells.size() - 1; i < n; i++) {
487: String cell = (String) cells.get(i);
488: String attribute = "";
489: if (perl.match("m/\\\\236([0-9]+)/", cell)) {
490: cell = perl.substitute("s/\\\\236([0-9]+)//", cell);
491: attribute = " colspan=\""
492: + Integer.parseInt(perl.group(1)) + "\"";
493: }
494: cell = perl.substitute("s/^\\s+$/ /o", cell);
495: perl.match("m/^(\\s*).*?(\\s*)$/", cell);
496: String left = perl.group(1);
497: String right = perl.group(2);
498: if (left.length() > right.length()) {
499: if (right.length() <= 1) {
500: attribute += " align=\"right\"";
501: } else {
502: attribute += " align=\"center\"";
503: }
504: }
505: if (perl.match("m/^\\s*(\\*.*\\*)\\s*$/", cell)) {
506: result.append("<th").append(attribute).append(
507: " class=\"twiki\" bgcolor=\"#99CCCC\">")
508: .append(perl.group(1)).append("<\\/th>");
509: } else {
510: result.append("<td").append(attribute).append(
511: " class=\"twiki\">").append(cell).append(
512: "<\\/td>");
513: }
514: }
515: result.append("<\\/tr>");
516: return result.toString();
517: }
518:
519: public void setExternalWikiAdapter(
520: ExternalWikiAdapter wikiWordFormatter) {
521: this .externalWikiAdapter = wikiWordFormatter;
522: }
523:
524: public MalformedPerl5PatternException getMalformedPatternException() {
525: return malformedPattern;
526: }
527: }
|