/******************************************************************************* * Copyright (c) 2008 Scott Stanchfield * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package com.javadude.doclet.wikitext; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashSet; import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; import com.sun.javadoc.Doc; import com.sun.javadoc.DocErrorReporter; import com.sun.javadoc.ParamTag; import com.sun.javadoc.RootDoc; import com.sun.javadoc.SeeTag; import com.sun.javadoc.SerialFieldTag; import com.sun.javadoc.Tag; import com.sun.javadoc.ThrowsTag; import com.sun.tools.doclets.standard.Standard; /** * This is a _simple_ wikitext *doclet*. So __there__. * Copyright (c) 2009 Scott Stanchfield. All Rights Reserved. * Copyright (c)2009 Scott Stanchfield. All Rights Reserved. * * I hope this -*works*... * * Stuff * - this *might* *be* useful... * - this might be useful again... * * More stuff * # hello * # there * # plah * # fnord * # crinkle * * More content * * {{{ * int x = 42; * * void foo() { * int y = 22; * } * }}} * * _notes_: * _notes:_ * (_notes:_) * *notes*: * (*notes*): * *notes:* * =notes:= * =notes=: * (=notes=): * * Nested paragraph * # how * * || A || B || C || * || xxx || yyy || zzz || * || qqq || rrr || sss || * * @author scott * */ public class WikitextDoclet { public static boolean start(RootDoc rootDoc) { new WikitextDoclet(); return Standard.start(WikitextDoclet.wrap(rootDoc)); } public static boolean validOptions(String[][] options, DocErrorReporter errorReporter) { return Standard.validOptions(options, errorReporter); } public static int optionLength(String option) { return Standard.optionLength(option); } public static interface Unwrapable { public Object unwrap(); } private static final Method unwrapMethod; static { try { unwrapMethod = Unwrapable.class.getDeclaredMethod("unwrap"); System.out.println(unwrapMethod); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } private static final Set<Method> methodsToConvert = new HashSet<Method>(); static { // add methods try { methodsToConvert.add(Doc.class.getDeclaredMethod("commentText")); methodsToConvert.add(Doc.class.getDeclaredMethod("getRawCommentText")); methodsToConvert.add(ParamTag.class.getDeclaredMethod("parameterComment")); methodsToConvert.add(SeeTag.class.getDeclaredMethod("label")); methodsToConvert.add(SerialFieldTag.class.getDeclaredMethod("description")); methodsToConvert.add(Tag.class.getDeclaredMethod("text")); methodsToConvert.add(ThrowsTag.class.getDeclaredMethod("exceptionComment")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static class WrappingInvocationHandler implements InvocationHandler { private Stack<Nesting> nestingStack = new Stack<Nesting>(); private Object target; public WrappingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // unwrap any unwrappable args (there are no methods that take one of our wrappers that need to keep them that way if (args != null) { for (int i = 0; i < args.length; i++) { if (args[i] != null && args[i] instanceof Unwrapable) { args[i] = ((Unwrapable) args[i]).unwrap(); } } } if (method.equals(unwrapMethod)) { return target; } if (methodsToConvert.contains(method)) { return createHtml((String) method.invoke(target, args)); } // if an array, wrap all the elements of the array if they're interface type if (method.getReturnType().isArray()) { Object result = method.invoke(target, args); Class<?> componentType = method.getReturnType().getComponentType(); if (!componentType.isInterface()) { return result; } int length = Array.getLength(result); Object newArray = Array.newInstance(componentType, length); for (int i = 0; i < length; i++) { Array.set(newArray, i, wrap(Array.get(result, i))); } return newArray; } // if primitive return type or not an interface, just return it if (method.getReturnType().isPrimitive() || !method.getReturnType().isInterface()) { return method.invoke(target, args); } // TODO wrap anything else (not positive here - is there anything we should omit?) Object result = method.invoke(target, args); return wrap(result); } private String createHtml(String result) { // simple rules: // _xxxx_ -> <i>xxxx</i> (all on same line) // *xxxx* -> <b>xxxx</b> (all on same line) // __xxxx__ -> <b><i>xxxx</i></b> (all on same line) // =xxxx= -> <code>xxxx</code> (can contain spaces but must not have spaces next to =) // blank lines separate paragraphs // {{{ // xxx // }}} -> <pre>xxx</pre> (must be on separate lines; newlines at either end removed) // # at start of line -> <li> inside <ol> (nesting allowed - multiple #) // - at start of line -> <li> inside <ul> (nesting allowed - multiple -) // | xx | xx | xx | -> table (line starts and ends with |) result = emphasisStrong.matcher(result).replaceAll("<i><b>$1</b></i>"); result = emphasis.matcher(result).replaceAll("<i>$1</i>"); result = strong.matcher(result).replaceAll("<b>$1</b>"); result = code.matcher(result).replaceAll("<code>$1</code>"); result = copyright.matcher(result).replaceAll("©"); StringBuilder html = new StringBuilder(); String[] lines = result.split("(\r\n?|\n)"); // normalize all lines to determine where paragraphs and indented content are // determine the minimum number of leading spaces on non-empty lines and // trim that much off each of the lines int minLeading = Integer.MAX_VALUE; for (String line : lines) { if ("".equals(line.trim())) { continue; } minLeading = Math.min(leadingSpaces(line), minLeading); } for(int i = 0; i < lines.length; i++) { if (lines[i].length() > minLeading) { lines[i] = lines[i].substring(minLeading); } } nestingStack.push(new Nesting(-1, NestingType.PARAGRAPHS)); for (String line : lines) { String trimmedLine = line.trim(); // check for line items (lines that start with - and #) boolean poppedOut = false; int n = leadingSpaces(line); NestingType nestingType = null; Nesting nesting = nestingStack.peek(); if ("{{{".equals(trimmedLine)) { nestingType = NestingType.PREFORMATTED; poppedOut = pushOrPop(nesting, n, html, nestingType); nesting = nestingStack.peek(); continue; } if ("}}}".equals(trimmedLine)) { if (n != nesting.leadingSpaces) { throw new RuntimeException("matching }}} must be at same indentation as {{{"); } // pop nesting popNesting(html); continue; } if (nesting.nestingType == NestingType.PREFORMATTED) { if ("".equals(trimmedLine)) { line = ""; } else if (n < nesting.leadingSpaces) { throw new RuntimeException("Preformatted text must be at least the same indentation as {{{ ... }}}"); } else { line = line.substring(nesting.leadingSpaces); } html.append(line); html.append('\n'); continue; } // TODO allow {{{ or }}} to be on same line as other stuff if (line.contains("{{{") || line.contains("}}}")) { throw new RuntimeException("{{{ or }}} must be a on a line by itself"); } if (!"".equals(trimmedLine)) { switch(line.charAt(n)) { case '-': nestingType = NestingType.UL; line = line.substring(n + 1).trim(); break; case '#': nestingType = NestingType.OL; line = line.substring(n + 1).trim(); break; default: nestingType = n == 0 ? NestingType.PARAGRAPHS : NestingType.TEXT; if (n == 0) { n = -1; // special level for paragraphs } break; } poppedOut = pushOrPop(nesting, n, html, nestingType); nesting = nestingStack.peek(); } // if this is a blank line, or an explicit new item end the previous item if (poppedOut || "".equals(trimmedLine) || nestingType == NestingType.UL || nestingType == NestingType.OL) { finishItemInProgress(html, nesting); } if (!"".equals(trimmedLine)) { if (!nesting.itemInProgress) { nesting.itemInProgress = true; html.append(nesting.nestingType.itemStart()); } html.append(line); html.append('\n'); } } while (!nestingStack.isEmpty()) { popNesting(html); } return html.toString(); } private boolean pushOrPop(Nesting nesting, int n, StringBuilder html, NestingType nestingType) { boolean poppedOut = false; // pop off levels that are deeper than we currently are while (nesting.leadingSpaces > n) { popNesting(html); poppedOut = true; nesting = nestingStack.peek(); } // if at same level as current nesting if (nesting.leadingSpaces == n) { // if it's a different type of nesting, pop off current and push on new below if (nesting.nestingType != nestingType) { popNesting(html); nesting = nestingStack.peek(); } } // if want deeper nesting, push new nesting if (nesting.leadingSpaces < n) { pushNesting(html, n, nestingType); nesting = nestingStack.peek(); } return poppedOut; } private void finishItemInProgress(StringBuilder html, Nesting nesting) { if (nesting.itemInProgress) { html.append(nesting.nestingType.itemEnd()); nesting.itemInProgress = false; html.append('\n'); } } private void pushNesting(StringBuilder html, int n, NestingType nestingType) { // if we have a paragraph or nested paragraph in progress, finish it before going more nested Nesting nesting = nestingStack.peek(); if (nesting.nestingType == NestingType.PARAGRAPHS || nesting.nestingType == NestingType.TEXT || nesting.nestingType == NestingType.PREFORMATTED) { finishItemInProgress(html, nesting); } Nesting newNesting = new Nesting(n, nestingType); nestingStack.push(newNesting); html.append(newNesting.nestingType.start()); html.append('\n'); } private void popNesting(StringBuilder html) { Nesting oldNesting = nestingStack.pop(); finishItemInProgress(html, oldNesting); html.append(oldNesting.nestingType.end()); html.append('\n'); } private int leadingSpaces(String line) { int n = 0; int len = line.length(); while (n < len && Character.isWhitespace(line.charAt(n))) { n++; } if (n == len) { return 0; } return n; } } private static final Pattern copyright = Pattern.compile("(?<=\\A|\\s)(\\(c\\))(?=\\z|\\s|\\d{4})"); private static final Pattern emphasis = Pattern.compile("\\b_(?=[^_\\s])(.*?)(?<=[^_\\s])_\\b"); private static final Pattern emphasisStrong = Pattern.compile("\\b__(?=[^_\\s])(.*?)(?<=[^_\\s])__\\b"); private static final Pattern strong = Pattern.compile("\\B\\*(?=[^\\*\\s])(.*?)(?<=[^\\*\\s])\\*\\B"); private static final Pattern code = Pattern.compile("\\B=(?=[^\\*\\s])(.*?)(?<=[^\\*\\s])=\\B"); private static enum NestingType { PARAGRAPHS { @Override public String start() { return ""; } @Override public String end() { return ""; } @Override public String itemStart() { return "<p>"; } @Override public String itemEnd() { return "</p>"; } }, PREFORMATTED { @Override public String start() { return "<pre>"; } @Override public String end() { return "</pre>"; } @Override public String itemStart() { return ""; } @Override public String itemEnd() { return ""; } }, OL { @Override public String start() { return "<ol>"; } @Override public String end() { return "</ol>"; } @Override public String itemStart() { return "<li>"; } @Override public String itemEnd() { return "</li>"; } }, UL { @Override public String start() { return "<ul>"; } @Override public String end() { return "</ul>"; } @Override public String itemStart() { return "<li>"; } @Override public String itemEnd() { return "</li>"; } }, TEXT { @Override public String start() { return "<dl>"; } @Override public String end() { return "</dl>"; } @Override public String itemStart() { return "<dd>"; } @Override public String itemEnd() { return "</dd>"; } }; public abstract String start(); public abstract String end(); public abstract String itemStart(); public abstract String itemEnd(); }; private static class Nesting { public int leadingSpaces; public NestingType nestingType; public boolean itemInProgress; public Nesting(int leadingSpaces, NestingType nestingType) { this.leadingSpaces = leadingSpaces; this.nestingType = nestingType; } } public static void main(String[] args) { String testData = " This is a _simple_ wikitext *doclet*. So __there__.\n" + " continue this paragraph\n" + "\n" + " I hope this -*works*...\n" + "\n" + " Stuff\n" + " - this *might* *be* useful...\n" + " - this might be useful again...\n" + " - nested...\n" + "\n" + " Uh oh... nested paragraph\n" + " Another nested paragraph\n" + " More nested paragraph\n" + " Back out paragraph\n" + "\n" + "_notes_:\n" + "_notes:_\n" + "(_notes:_)\n" + "*notes*:\n" + "(*notes*):\n" + "*notes:*\n" + "=notes:=\n" + "=notes=:\n" + "(=notes=):\n" + "\n" + "\n" + " More stuff\n" + " # hello\n" + " # there\n" + " # plah\n" + " # fnord\n" + " # crinkle\n" + "\n" + " # how\n" + "\n" + " || A || B || C ||\n" + " || xxx || yyy || zzz ||\n" + " || qqq || rrr || sss ||\n" + ""; System.out.println(new WikitextDoclet().createHtml(testData)); } // pass through for testing private String createHtml(String testData) { return new WrappingInvocationHandler(null).createHtml(testData); } public static <T> T wrap(T o) { if (o == null) { return null; } Class<?>[] interfaces = o.getClass().getInterfaces(); Class<?>[] targetInterfaces = new Class<?>[interfaces.length + 1]; System.arraycopy(interfaces, 0, targetInterfaces, 0, interfaces.length); targetInterfaces[interfaces.length] = Unwrapable.class; @SuppressWarnings("unchecked") T t = (T) Proxy.newProxyInstance(WikitextDoclet.class.getClassLoader(), targetInterfaces, new WrappingInvocationHandler(o)); return t; } }