0001: /*
0002: JSPWiki - a JSP-based WikiWiki clone.
0003:
0004: Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
0005:
0006: This program is free software; you can redistribute it and/or modify
0007: it under the terms of the GNU Lesser General Public License as published by
0008: the Free Software Foundation; either version 2.1 of the License, or
0009: (at your option) any later version.
0010:
0011: This program is distributed in the hope that it will be useful,
0012: but WITHOUT ANY WARRANTY; without even the implied warranty of
0013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0014: GNU Lesser General Public License for more details.
0015:
0016: You should have received a copy of the GNU Lesser General Public License
0017: along with this program; if not, write to the Free Software
0018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0019: */
0020: package com.ecyrd.jspwiki;
0021:
0022: import java.io.*;
0023: import java.util.*;
0024:
0025: import org.apache.log4j.Logger;
0026: import org.apache.oro.text.*;
0027: import org.apache.oro.text.regex.*;
0028:
0029: import com.ecyrd.jspwiki.plugin.PluginManager;
0030: import com.ecyrd.jspwiki.plugin.PluginException;
0031: import com.ecyrd.jspwiki.attachment.AttachmentManager;
0032: import com.ecyrd.jspwiki.attachment.Attachment;
0033: import com.ecyrd.jspwiki.providers.ProviderException;
0034: import com.ecyrd.jspwiki.acl.AccessControlList;
0035: import com.ecyrd.jspwiki.auth.modules.PageAuthorizer;
0036: import com.ecyrd.jspwiki.auth.WikiSecurityException;
0037: import com.ecyrd.jspwiki.auth.UserManager;
0038:
0039: /**
0040: * Handles conversion from Wiki format into fully featured HTML.
0041: * This is where all the magic happens. It is CRITICAL that this
0042: * class is tested, or all Wikis might die horribly.
0043: * <P>
0044: * The output of the HTML has not yet been validated against
0045: * the HTML DTD. However, it is very simple.
0046: *
0047: * @author Janne Jalkanen
0048: */
0049:
0050: public class TranslatorReader extends Reader {
0051: public static final int READ = 0;
0052: public static final int EDIT = 1;
0053: private static final int EMPTY = 2; // Empty message
0054: private static final int LOCAL = 3;
0055: private static final int LOCALREF = 4;
0056: private static final int IMAGE = 5;
0057: private static final int EXTERNAL = 6;
0058: private static final int INTERWIKI = 7;
0059: private static final int IMAGELINK = 8;
0060: private static final int IMAGEWIKILINK = 9;
0061: public static final int ATTACHMENT = 10;
0062: private static final int ATTACHMENTIMAGE = 11;
0063:
0064: /** Lists all punctuation characters allowed in WikiMarkup. These
0065: will not be cleaned away. */
0066:
0067: private static final String PUNCTUATION_CHARS_ALLOWED = "._:";
0068:
0069: /** Allow this many characters to be pushed back in the stream. In effect,
0070: this limits the size of a single heading line. */
0071: private static final int PUSHBACK_BUFFER_SIZE = 10 * 1024;
0072: private PushbackReader m_in;
0073:
0074: private StringReader m_data = new StringReader("");
0075:
0076: private static Logger log = Logger
0077: .getLogger(TranslatorReader.class);
0078:
0079: //private boolean m_iscode = false;
0080: private boolean m_isbold = false;
0081: private boolean m_isitalic = false;
0082: private boolean m_isTypedText = false;
0083: private boolean m_istable = false;
0084: private boolean m_isPre = false;
0085: private boolean m_isEscaping = false;
0086: private boolean m_isdefinition = false;
0087:
0088: /** Contains style information, in multiple forms. */
0089: private Stack m_styleStack = new Stack();
0090:
0091: // general list handling
0092: private int m_genlistlevel = 0;
0093: private StringBuffer m_genlistBulletBuffer = new StringBuffer(); // stores the # and * pattern
0094: private boolean m_allowPHPWikiStyleLists = true;
0095:
0096: private boolean m_isOpenParagraph = false;
0097:
0098: /** Tag that gets closed at EOL. */
0099: private String m_closeTag = null;
0100:
0101: private WikiEngine m_engine;
0102: private WikiContext m_context;
0103:
0104: /** Optionally stores internal wikilinks */
0105: private ArrayList m_localLinkMutatorChain = new ArrayList();
0106: private ArrayList m_externalLinkMutatorChain = new ArrayList();
0107: private ArrayList m_attachmentLinkMutatorChain = new ArrayList();
0108: private ArrayList m_headingListenerChain = new ArrayList();
0109:
0110: /** Keeps image regexp Patterns */
0111: private ArrayList m_inlineImagePatterns;
0112:
0113: private PatternMatcher m_inlineMatcher = new Perl5Matcher();
0114:
0115: private ArrayList m_linkMutators = new ArrayList();
0116:
0117: /**
0118: * This property defines the inline image pattern. It's current value
0119: * is jspwiki.translatorReader.inlinePattern
0120: */
0121: public static final String PROP_INLINEIMAGEPTRN = "jspwiki.translatorReader.inlinePattern";
0122:
0123: /** If true, consider CamelCase hyperlinks as well. */
0124: public static final String PROP_CAMELCASELINKS = "jspwiki.translatorReader.camelCaseLinks";
0125:
0126: /** If true, all hyperlinks are translated as well, regardless whether they
0127: are surrounded by brackets. */
0128: public static final String PROP_PLAINURIS = "jspwiki.translatorReader.plainUris";
0129:
0130: /** If true, all outward links (external links) have a small link image appended. */
0131: public static final String PROP_USEOUTLINKIMAGE = "jspwiki.translatorReader.useOutlinkImage";
0132:
0133: /** If set to "true", allows using raw HTML within Wiki text. Be warned,
0134: this is a VERY dangerous option to set - never turn this on in a publicly
0135: allowable Wiki, unless you are absolutely certain of what you're doing. */
0136: public static final String PROP_ALLOWHTML = "jspwiki.translatorReader.allowHTML";
0137:
0138: /** If set to "true", all external links are tagged with 'rel="nofollow"' */
0139: public static final String PROP_USERELNOFOLLOW = "jspwiki.translatorReader.useRelNofollow";
0140:
0141: /** If set to "true", enables plugins during parsing */
0142: public static final String PROP_RUNPLUGINS = "jspwiki.translatorReader.runPlugins";
0143:
0144: /** If true, then considers CamelCase links as well. */
0145: private boolean m_camelCaseLinks = false;
0146:
0147: /** If true, consider URIs that have no brackets as well. */
0148: // FIXME: Currently reserved, but not used.
0149: private boolean m_plainUris = false;
0150:
0151: /** If true, all outward links use a small link image. */
0152: private boolean m_useOutlinkImage = true;
0153:
0154: /** If true, allows raw HTML. */
0155: private boolean m_allowHTML = false;
0156:
0157: /** If true, executes plugins; otherwise ignores them. */
0158: private boolean m_enablePlugins = true;
0159:
0160: private boolean m_useRelNofollow = false;
0161:
0162: private boolean m_inlineImages = true;
0163:
0164: private PatternMatcher m_matcher = new Perl5Matcher();
0165: private PatternCompiler m_compiler = new Perl5Compiler();
0166: private Pattern m_camelCasePtrn;
0167:
0168: private TextRenderer m_renderer;
0169:
0170: /**
0171: * The default inlining pattern. Currently "*.png"
0172: */
0173: public static final String DEFAULT_INLINEPATTERN = "*.png";
0174:
0175: /**
0176: * These characters constitute word separators when trying
0177: * to find CamelCase links.
0178: */
0179: private static final String WORD_SEPARATORS = ",.|;+=&()";
0180:
0181: protected static final int BOLD = 0;
0182: protected static final int ITALIC = 1;
0183: protected static final int TYPED = 2;
0184:
0185: /**
0186: * This list contains all IANA registered URI protocol
0187: * types as of September 2004 + a few well-known extra types.
0188: *
0189: * JSPWiki recognises all of them as external links.
0190: */
0191: static final String[] c_externalLinks = { "http:", "ftp:",
0192: "https:", "mailto:", "news:", "file:", "rtsp:", "mms:",
0193: "ldap:", "gopher:", "nntp:", "telnet:", "wais:",
0194: "prospero:", "z39.50s", "z39.50r", "vemmi:", "imap:",
0195: "nfs:", "acap:", "tip:", "pop:", "dav:",
0196: "opaquelocktoken:", "sip:", "sips:", "tel:", "fax:",
0197: "modem:", "soap.beep:", "soap.beeps", "xmlrpc.beep",
0198: "xmlrpc.beeps", "urn:", "go:", "h323:", "ipp:", "tftp:",
0199: "mupdate:", "pres:", "im:", "mtqp", "smb:" };
0200:
0201: /**
0202: * Creates a TranslatorReader using the default HTML renderer.
0203: */
0204: public TranslatorReader(WikiContext context, Reader in) {
0205: initialize(context, in, new HTMLRenderer());
0206: }
0207:
0208: public TranslatorReader(WikiContext context, Reader in,
0209: TextRenderer renderer) {
0210: initialize(context, in, renderer);
0211: }
0212:
0213: /**
0214: * Replaces the current input character stream with a new one.
0215: * @param in New source for input. If null, this method does nothing.
0216: * @return the old stream
0217: */
0218: public Reader setInputReader(Reader in) {
0219: Reader old = m_in;
0220:
0221: if (in != null) {
0222: m_in = new PushbackReader(new BufferedReader(in),
0223: PUSHBACK_BUFFER_SIZE);
0224: }
0225:
0226: return old;
0227: }
0228:
0229: /**
0230: * @param engine The WikiEngine this reader is attached to. Is
0231: * used to figure out of a page exits.
0232: */
0233:
0234: // FIXME: TranslatorReaders should be pooled for better performance.
0235: private void initialize(WikiContext context, Reader in,
0236: TextRenderer renderer) {
0237: PatternCompiler compiler = new GlobCompiler();
0238: ArrayList compiledpatterns = new ArrayList();
0239:
0240: m_engine = context.getEngine();
0241: m_context = context;
0242:
0243: m_renderer = renderer;
0244:
0245: setInputReader(in);
0246:
0247: Collection ptrns = getImagePatterns(m_engine);
0248:
0249: //
0250: // Make them into Regexp Patterns. Unknown patterns
0251: // are ignored.
0252: //
0253: for (Iterator i = ptrns.iterator(); i.hasNext();) {
0254: try {
0255: compiledpatterns.add(compiler
0256: .compile((String) i.next()));
0257: } catch (MalformedPatternException e) {
0258: log.error("Malformed pattern in properties: ", e);
0259: }
0260: }
0261:
0262: m_inlineImagePatterns = compiledpatterns;
0263:
0264: try {
0265: m_camelCasePtrn = m_compiler
0266: .compile("^([[:^alnum:]]*|\\~)([[:upper:]]+[[:lower:]]+[[:upper:]]+[[:alnum:]]*)[[:^alnum:]]*$");
0267: } catch (MalformedPatternException e) {
0268: log.fatal(
0269: "Internal error: Someone put in a faulty pattern.",
0270: e);
0271: throw new InternalWikiException(
0272: "Faulty camelcasepattern in TranslatorReader");
0273: }
0274:
0275: //
0276: // Set the properties.
0277: //
0278: Properties props = m_engine.getWikiProperties();
0279:
0280: String cclinks = (String) m_context.getPage().getAttribute(
0281: PROP_CAMELCASELINKS);
0282:
0283: if (cclinks != null) {
0284: m_camelCaseLinks = TextUtil.isPositive(cclinks);
0285: } else {
0286: m_camelCaseLinks = TextUtil.getBooleanProperty(props,
0287: PROP_CAMELCASELINKS, m_camelCaseLinks);
0288: }
0289:
0290: m_plainUris = TextUtil.getBooleanProperty(props,
0291: PROP_PLAINURIS, m_plainUris);
0292: m_useOutlinkImage = TextUtil.getBooleanProperty(props,
0293: PROP_USEOUTLINKIMAGE, m_useOutlinkImage);
0294: m_allowHTML = TextUtil.getBooleanProperty(props,
0295: PROP_ALLOWHTML, m_allowHTML);
0296:
0297: m_useRelNofollow = TextUtil.getBooleanProperty(props,
0298: PROP_USERELNOFOLLOW, m_useRelNofollow);
0299:
0300: String runplugins = m_engine.getVariable(m_context,
0301: PROP_RUNPLUGINS);
0302: if (runplugins != null)
0303: enablePlugins(TextUtil.isPositive(runplugins));
0304:
0305: if (m_engine.getUserManager() == null
0306: || m_engine.getUserManager().getAuthenticator() == null) {
0307: disableAccessRules();
0308: }
0309:
0310: m_context.getPage().setHasMetadata();
0311: }
0312:
0313: /**
0314: * Sets the currently used renderer. This method is protected because
0315: * we only want to use it internally for now. The renderer interface
0316: * is not yet set to stone, so it's not expected that third parties
0317: * would use this.
0318: */
0319: protected void setRenderer(TextRenderer renderer) {
0320: m_renderer = renderer;
0321: }
0322:
0323: /**
0324: * Adds a hook for processing link texts. This hook is called
0325: * when the link text is written into the output stream, and
0326: * you may use it to modify the text. It does not affect the
0327: * actual link, only the user-visible text.
0328: *
0329: * @param mutator The hook to call. Null is safe.
0330: */
0331: public void addLinkTransmutator(StringTransmutator mutator) {
0332: if (mutator != null) {
0333: m_linkMutators.add(mutator);
0334: }
0335: }
0336:
0337: /**
0338: * Adds a hook for processing local links. The engine
0339: * transforms both non-existing and existing page links.
0340: *
0341: * @param mutator The hook to call. Null is safe.
0342: */
0343: public void addLocalLinkHook(StringTransmutator mutator) {
0344: if (mutator != null) {
0345: m_localLinkMutatorChain.add(mutator);
0346: }
0347: }
0348:
0349: /**
0350: * Adds a hook for processing external links. This includes
0351: * all http:// ftp://, etc. links, including inlined images.
0352: *
0353: * @param mutator The hook to call. Null is safe.
0354: */
0355: public void addExternalLinkHook(StringTransmutator mutator) {
0356: if (mutator != null) {
0357: m_externalLinkMutatorChain.add(mutator);
0358: }
0359: }
0360:
0361: /**
0362: * Adds a hook for processing attachment links.
0363: *
0364: * @param mutator The hook to call. Null is safe.
0365: */
0366: public void addAttachmentLinkHook(StringTransmutator mutator) {
0367: if (mutator != null) {
0368: m_attachmentLinkMutatorChain.add(mutator);
0369: }
0370: }
0371:
0372: public void addHeadingListener(HeadingListener listener) {
0373: if (listener != null) {
0374: m_headingListenerChain.add(listener);
0375: }
0376: }
0377:
0378: private boolean m_parseAccessRules = true;
0379:
0380: public void disableAccessRules() {
0381: m_parseAccessRules = false;
0382: }
0383:
0384: /**
0385: * Can be used to turn on plugin execution on a translator-reader basis
0386: */
0387: public void enablePlugins(boolean toggle) {
0388: m_enablePlugins = toggle;
0389: }
0390:
0391: /**
0392: * Use this to turn on or off image inlining.
0393: * @param toggle If true, images are inlined (as per set in jspwiki.properties)
0394: * If false, then images won't be inlined; instead, they will be
0395: * treated as standard hyperlinks.
0396: * @since 2.2.9
0397: */
0398: public void enableImageInlining(boolean toggle) {
0399: m_inlineImages = toggle;
0400: }
0401:
0402: /**
0403: * Figure out which image suffixes should be inlined.
0404: * @return Collection of Strings with patterns.
0405: */
0406:
0407: protected static Collection getImagePatterns(WikiEngine engine) {
0408: Properties props = engine.getWikiProperties();
0409: ArrayList ptrnlist = new ArrayList();
0410:
0411: for (Enumeration e = props.propertyNames(); e.hasMoreElements();) {
0412: String name = (String) e.nextElement();
0413:
0414: if (name.startsWith(PROP_INLINEIMAGEPTRN)) {
0415: String ptrn = props.getProperty(name);
0416:
0417: ptrnlist.add(ptrn);
0418: }
0419: }
0420:
0421: if (ptrnlist.size() == 0) {
0422: ptrnlist.add(DEFAULT_INLINEPATTERN);
0423: }
0424:
0425: return ptrnlist;
0426: }
0427:
0428: /**
0429: * Returns link name, if it exists; otherwise it returns null.
0430: */
0431: private String linkExists(String page) {
0432: try {
0433: if (page == null || page.length() == 0)
0434: return null;
0435:
0436: return m_engine.getFinalPageName(page);
0437: } catch (ProviderException e) {
0438: log.warn("TranslatorReader got a faulty page name!", e);
0439:
0440: return page; // FIXME: What would be the correct way to go back?
0441: }
0442: }
0443:
0444: /**
0445: * Calls a transmutator chain.
0446: *
0447: * @param list Chain to call
0448: * @param text Text that should be passed to the mutate() method
0449: * of each of the mutators in the chain.
0450: * @return The result of the mutation.
0451: */
0452:
0453: private String callMutatorChain(Collection list, String text) {
0454: if (list != null && list.size() > 0) {
0455: for (Iterator i = list.iterator(); i.hasNext();) {
0456: StringTransmutator m = (StringTransmutator) i.next();
0457:
0458: text = m.mutate(m_context, text);
0459: }
0460: }
0461:
0462: // clean up display of relative vs absolute vs global page names
0463: if (text.indexOf(":") != -1) {
0464: String wikiName = WikiEngine.getWikiName(text);
0465: if (wikiName.length() == 0) {
0466: // global page - display 'Main*'
0467: text = text.substring(1) + "*";
0468: } else
0469: text = WikiEngine.getRelativeToWikiPageName(text);
0470: }
0471: return text;
0472: }
0473:
0474: private void callHeadingListenerChain(Heading param) {
0475: List list = m_headingListenerChain;
0476:
0477: for (Iterator i = list.iterator(); i.hasNext();) {
0478: HeadingListener h = (HeadingListener) i.next();
0479:
0480: h.headingAdded(m_context, param);
0481: }
0482: }
0483:
0484: /**
0485: * Write a HTMLized link depending on its type.
0486: * The link mutator chain is processed.
0487: *
0488: * @param type Type of the link.
0489: * @param link The actual link.
0490: * @param text The user-visible text for the link.
0491: */
0492: public String makeLink(int type, String link, String text) {
0493: if (text == null)
0494: text = link;
0495:
0496: text = callMutatorChain(m_linkMutators, text);
0497:
0498: return m_renderer.makeLink(type, link, text);
0499: }
0500:
0501: /**
0502: * Just like makeLink, but also adds the section reference (#sect...)
0503: */
0504: private String makeLink(int type, String link, String text,
0505: String sectref) {
0506: if (text == null)
0507: text = link;
0508:
0509: text = callMutatorChain(m_linkMutators, text);
0510:
0511: return m_renderer.makeLink(type, link, text, sectref);
0512: }
0513:
0514: /**
0515: * Cleans a Wiki name.
0516: * <P>
0517: * [ This is a link ] -> ThisIsALink
0518: *
0519: * @param link Link to be cleared. Null is safe, and causes this to return null.
0520: * @return A cleaned link.
0521: *
0522: * @since 2.0
0523: */
0524: public static String cleanLink(String link) {
0525: if (link == null)
0526: return null;
0527: String wiki = cleanLinkPart(WikiEngine.getWikiName(link), false);
0528: link = WikiEngine.getRelativePageName(link);
0529: if (link != null) {
0530: String linkbits[] = link.split("/", 2);
0531: link = cleanLinkPart(linkbits[0], true);
0532: if (linkbits.length == 2)
0533: link = link + "/" + linkbits[1];
0534: }
0535: if (wiki != null)
0536: link = wiki + ":" + link;
0537: return link;
0538: }
0539:
0540: // if morph is false, then we only contract spaces and dashes, no character manipulation
0541: public static String cleanLinkPart(String link, boolean morph) {
0542: if (link == null)
0543: return null;
0544:
0545: StringBuffer clean = new StringBuffer();
0546:
0547: //
0548: // Compress away all whitespace and capitalize
0549: // all words in between.
0550: //
0551:
0552: StringTokenizer st = new StringTokenizer(link, " -");
0553:
0554: while (st.hasMoreTokens()) {
0555: StringBuffer component = new StringBuffer(st.nextToken());
0556:
0557: if (morph)
0558: component.setCharAt(0, Character.toUpperCase(component
0559: .charAt(0)));
0560:
0561: //
0562: // We must do this, because otherwise compiling on JDK 1.4 causes
0563: // a downwards incompatibility to JDK 1.3.
0564: //
0565: clean.append(component.toString());
0566: }
0567:
0568: //
0569: // Remove non-alphanumeric characters that should not
0570: // be put inside WikiNames. Note that all valid
0571: // Unicode letters are considered okay for WikiNames.
0572: // It is the problem of the WikiPageProvider to take
0573: // care of actually storing that information.
0574: //
0575:
0576: for (int i = 0; i < clean.length(); i++) {
0577: char ch = clean.charAt(i);
0578:
0579: if (!(Character.isLetterOrDigit(ch) || PUNCTUATION_CHARS_ALLOWED
0580: .indexOf(ch) != -1)) {
0581: clean.deleteCharAt(i);
0582: --i; // We just shortened this buffer.
0583: }
0584: }
0585:
0586: return clean.toString();
0587: }
0588:
0589: /**
0590: * Figures out if a link is an off-site link. This recognizes
0591: * the most common protocols by checking how it starts.
0592: */
0593:
0594: // FIXME: Should really put the external link types to a sorted set,
0595: // then searching for them would be faster.
0596: private boolean isExternalLink(String link) {
0597: for (int i = 0; i < c_externalLinks.length; i++) {
0598: if (link.startsWith(c_externalLinks[i]))
0599: return true;
0600: }
0601:
0602: return false;
0603: }
0604:
0605: /**
0606: * Returns true, if the link in question is an access
0607: * rule.
0608: */
0609: private static boolean isAccessRule(String link) {
0610: return link.startsWith("{ALLOW") || link.startsWith("{DENY");
0611: }
0612:
0613: /**
0614: * Matches the given link to the list of image name patterns
0615: * to determine whether it should be treated as an inline image
0616: * or not.
0617: */
0618: private boolean isImageLink(String link) {
0619: if (m_inlineImages) {
0620: for (Iterator i = m_inlineImagePatterns.iterator(); i
0621: .hasNext();) {
0622: if (m_inlineMatcher.matches(link, (Pattern) i.next()))
0623: return true;
0624: }
0625: }
0626:
0627: return false;
0628: }
0629:
0630: private static boolean isMetadata(String link) {
0631: return link.startsWith("{SET");
0632: }
0633:
0634: /**
0635: * Returns true, if the argument contains a number, otherwise false.
0636: * In a quick test this is roughly the same speed as Integer.parseInt()
0637: * if the argument is a number, and roughly ten times the speed, if
0638: * the argument is NOT a number.
0639: */
0640:
0641: private boolean isNumber(String s) {
0642: if (s == null)
0643: return false;
0644:
0645: if (s.length() > 1 && s.charAt(0) == '-')
0646: s = s.substring(1);
0647:
0648: for (int i = 0; i < s.length(); i++) {
0649: if (!Character.isDigit(s.charAt(i)))
0650: return false;
0651: }
0652:
0653: return true;
0654: }
0655:
0656: /**
0657: * Checks for the existence of a traditional style CamelCase link.
0658: * <P>
0659: * We separate all white-space -separated words, and feed it to this
0660: * routine to find if there are any possible camelcase links.
0661: * For example, if "word" is "__HyperLink__" we return "HyperLink".
0662: *
0663: * @param word A phrase to search in.
0664: * @return The match within the phrase. Returns null, if no CamelCase
0665: * hyperlink exists within this phrase.
0666: */
0667: private String checkForCamelCaseLink(String word) {
0668: PatternMatcherInput input;
0669:
0670: input = new PatternMatcherInput(word);
0671:
0672: if (m_matcher.contains(input, m_camelCasePtrn)) {
0673: MatchResult res = m_matcher.getMatch();
0674:
0675: int start = res.beginOffset(2);
0676: int end = res.endOffset(2);
0677:
0678: String link = res.group(2);
0679:
0680: if (res.group(1) != null) {
0681: if (res.group(1).equals("~")
0682: || res.group(1).indexOf('[') != -1) {
0683: // Delete the (~) from beginning.
0684: // We'll make '~' the generic kill-processing-character from
0685: // now on.
0686: return null;
0687: }
0688: }
0689:
0690: return link;
0691: } // if match
0692:
0693: return null;
0694: }
0695:
0696: /**
0697: * When given a link to a WikiName, we just return
0698: * a proper HTML link for it. The local link mutator
0699: * chain is also called.
0700: */
0701: private String makeCamelCaseLink(String wikiname) {
0702: String matchedLink;
0703: String link;
0704:
0705: callMutatorChain(m_localLinkMutatorChain, wikiname);
0706:
0707: if ((matchedLink = linkExists(wikiname)) != null) {
0708: link = makeLink(READ, matchedLink, wikiname);
0709: } else {
0710: link = makeLink(EDIT, wikiname, wikiname);
0711: }
0712:
0713: return link;
0714: }
0715:
0716: private String makeDirectURILink(String url) {
0717: String last = "";
0718: String result;
0719:
0720: if (url.endsWith(",") || url.endsWith(".")) {
0721: last = url.substring(url.length() - 1);
0722: url = url.substring(0, url.length() - 1);
0723: }
0724:
0725: callMutatorChain(m_externalLinkMutatorChain, url);
0726:
0727: if (isImageLink(url)) {
0728: result = handleImageLink(url, url, false);
0729: } else {
0730: result = makeLink(EXTERNAL, url, url)
0731: + m_renderer.outlinkImage();
0732: }
0733:
0734: result += last;
0735:
0736: return result;
0737: }
0738:
0739: /**
0740: * Image links are handled differently:
0741: * 1. If the text is a WikiName of an existing page,
0742: * it gets linked.
0743: * 2. If the text is an external link, then it is inlined.
0744: * 3. Otherwise it becomes an ALT text.
0745: *
0746: * @param reallink The link to the image.
0747: * @param link Link text portion, may be a link to somewhere else.
0748: * @param hasLinkText If true, then the defined link had a link text available.
0749: * This means that the link text may be a link to a wiki page,
0750: * or an external resource.
0751: */
0752:
0753: private String handleImageLink(String reallink, String link,
0754: boolean hasLinkText) {
0755: String possiblePage = cleanLink(link);
0756: String matchedLink;
0757: String res = "";
0758:
0759: if (isExternalLink(link) && hasLinkText) {
0760: res = makeLink(IMAGELINK, reallink, link);
0761: } else if ((matchedLink = linkExists(possiblePage)) != null
0762: && hasLinkText) {
0763: // System.out.println("Orig="+link+", Matched: "+matchedLink);
0764: callMutatorChain(m_localLinkMutatorChain, possiblePage);
0765:
0766: res = makeLink(IMAGEWIKILINK, reallink, link);
0767: } else {
0768: res = makeLink(IMAGE, reallink, link);
0769: }
0770:
0771: return res;
0772: }
0773:
0774: private String handleAccessRule(String ruleLine) {
0775: if (!m_parseAccessRules)
0776: return "";
0777: AccessControlList acl;
0778: WikiPage page = m_context.getPage();
0779: UserManager mgr = m_context.getEngine().getUserManager();
0780:
0781: if (ruleLine.startsWith("{"))
0782: ruleLine = ruleLine.substring(1);
0783: if (ruleLine.endsWith("}"))
0784: ruleLine = ruleLine.substring(0, ruleLine.length() - 1);
0785:
0786: log.debug("page=" + page.getName() + ", ACL = " + ruleLine);
0787:
0788: try {
0789: acl = PageAuthorizer.parseAcl(page, mgr, ruleLine);
0790:
0791: page.setAcl(acl);
0792:
0793: log.debug(acl.toString());
0794: } catch (WikiSecurityException wse) {
0795: return m_renderer.makeError(wse.getMessage());
0796: }
0797:
0798: return "";
0799: }
0800:
0801: /**
0802: * Handles metadata setting [{SET foo=bar}]
0803: */
0804: private String handleMetadata(String link) {
0805: try {
0806: String args = link.substring(link.indexOf(' '), link
0807: .length() - 1);
0808:
0809: String name = args.substring(0, args.indexOf('='));
0810: String val = args.substring(args.indexOf('=') + 1, args
0811: .length());
0812:
0813: name = name.trim();
0814: val = val.trim();
0815:
0816: if (val.startsWith("'"))
0817: val = val.substring(1);
0818: if (val.endsWith("'"))
0819: val = val.substring(0, val.length() - 1);
0820:
0821: // log.debug("SET name='"+name+"', value='"+val+"'.");
0822:
0823: if (name.length() > 0 && val.length() > 0) {
0824: val = m_engine.getVariableManager().expandVariables(
0825: m_context, val);
0826:
0827: m_context.getPage().setAttribute(name, val);
0828: }
0829: } catch (Exception e) {
0830: m_renderer.makeError(" Invalid SET found: " + link);
0831: }
0832:
0833: return "";
0834: }
0835:
0836: /**
0837: * Gobbles up all hyperlinks that are encased in square brackets.
0838: */
0839: private String handleHyperlinks(String link) {
0840: StringBuffer sb = new StringBuffer();
0841: String reallink;
0842: int cutpoint;
0843:
0844: if (isAccessRule(link)) {
0845: return handleAccessRule(link);
0846: }
0847:
0848: if (isMetadata(link)) {
0849: return handleMetadata(link);
0850: }
0851:
0852: if (PluginManager.isPluginLink(link)) {
0853: String included = "";
0854: try {
0855: if (m_enablePlugins) {
0856: included = m_engine.getPluginManager().execute(
0857: m_context, link);
0858: }
0859: } catch (PluginException e) {
0860: log.info("Failed to insert plugin", e);
0861: log.info("Root cause:", e.getRootThrowable());
0862: included = m_renderer
0863: .makeError("Plugin insertion failed: "
0864: + e.getMessage());
0865: }
0866:
0867: sb.append(included);
0868:
0869: return sb.toString();
0870: }
0871:
0872: link = TextUtil.replaceEntities(link);
0873:
0874: if ((cutpoint = link.indexOf('|')) != -1) {
0875: reallink = link.substring(cutpoint + 1).trim();
0876: link = link.substring(0, cutpoint);
0877: } else {
0878: reallink = link.trim();
0879: }
0880:
0881: int interwikipoint = -1;
0882:
0883: //
0884: // Yes, we now have the components separated.
0885: // link = the text the link should have
0886: // reallink = the url or page name.
0887: //
0888: // In many cases these are the same. [link|reallink].
0889: //
0890: if (VariableManager.isVariableLink(link)) {
0891: String value;
0892:
0893: try {
0894: value = m_engine.getVariableManager().parseAndGetValue(
0895: m_context, link);
0896: } catch (NoSuchVariableException e) {
0897: value = m_renderer.makeError(e.getMessage());
0898: } catch (IllegalArgumentException e) {
0899: value = m_renderer.makeError(e.getMessage());
0900: }
0901:
0902: sb.append(value);
0903: } else if (isExternalLink(reallink)) {
0904: // It's an external link, out of this Wiki
0905:
0906: callMutatorChain(m_externalLinkMutatorChain, reallink);
0907:
0908: if (isImageLink(reallink)) {
0909: sb.append(handleImageLink(reallink, link,
0910: (cutpoint != -1)));
0911: } else {
0912: sb.append(makeLink(EXTERNAL, reallink, link));
0913: sb.append(m_renderer.outlinkImage());
0914: }
0915: } else if ((interwikipoint = reallink.indexOf("::")) != -1) {
0916: // It's an interwiki link
0917: // InterWiki links also get added to external link chain
0918: // after the links have been resolved.
0919:
0920: // FIXME: There is an interesting issue here: We probably should
0921: // URLEncode the wikiPage, but we can't since some of the
0922: // Wikis use slashes (/), which won't survive URLEncoding.
0923: // Besides, we don't know which character set the other Wiki
0924: // is using, so you'll have to write the entire name as it appears
0925: // in the URL. Bugger.
0926:
0927: String extWiki = reallink.substring(0, interwikipoint);
0928: String wikiPage = reallink.substring(interwikipoint + 2);
0929:
0930: String urlReference;
0931: if (extWiki.equals("Edit")) {
0932: urlReference = m_context.getURL(WikiContext.EDIT,
0933: wikiPage);
0934: } else
0935: urlReference = m_engine.getInterWikiURL(extWiki);
0936:
0937: if (urlReference != null) {
0938: urlReference = TextUtil.replaceString(urlReference,
0939: "%s", wikiPage);
0940: callMutatorChain(m_externalLinkMutatorChain,
0941: urlReference);
0942:
0943: sb.append(makeLink(INTERWIKI, urlReference, link));
0944:
0945: if (isExternalLink(urlReference)) {
0946: sb.append(m_renderer.outlinkImage());
0947: }
0948: } else {
0949: sb
0950: .append(link
0951: + " "
0952: + m_renderer
0953: .makeError("No InterWiki reference defined in properties for Wiki called '"
0954: + extWiki + "'!)"));
0955: }
0956: } else if (reallink.startsWith("#")) {
0957: // It defines a local footnote
0958: sb.append(makeLink(LOCAL, reallink, link));
0959: } else if (isNumber(reallink)) {
0960: // It defines a reference to a local footnote
0961: sb.append(makeLink(LOCALREF, reallink, link));
0962: } else {
0963: int hashMark = -1;
0964:
0965: //
0966: // Internal wiki link, but is it an attachment link?
0967: //
0968: String rellink = reallink;
0969: String attachment = findAttachment(rellink); // attachments can have relative names
0970: reallink = WikiEngine.makeAbsolutePageName(reallink);
0971: if (attachment != null) {
0972: callMutatorChain(m_attachmentLinkMutatorChain,
0973: attachment);
0974:
0975: if (isImageLink(reallink)) {
0976: attachment = m_context.getURL(WikiContext.ATTACH,
0977: attachment);
0978: sb.append(handleImageLink(attachment, link,
0979: (cutpoint != -1)));
0980: } else {
0981: sb.append(makeLink(ATTACHMENT, attachment, link));
0982: }
0983: } else if ((hashMark = reallink.indexOf('#')) != -1) {
0984: // It's an internal Wiki link, but to a named section
0985:
0986: String namedSection = reallink.substring(hashMark + 1);
0987: reallink = reallink.substring(0, hashMark);
0988:
0989: reallink = cleanLink(reallink);
0990:
0991: callMutatorChain(m_localLinkMutatorChain, reallink);
0992:
0993: String matchedLink;
0994: if ((matchedLink = linkExists(reallink)) != null) {
0995: String sectref = "section-"
0996: + m_engine.encodeName(matchedLink) + "-"
0997: + namedSection;
0998: sectref = sectref.replace('%', '_');
0999: sb
1000: .append(makeLink(READ, matchedLink, link,
1001: sectref));
1002: } else {
1003: sb.append(makeLink(EDIT, reallink, link));
1004: }
1005: } else {
1006: // It's an internal Wiki link
1007: int debug;
1008: if (reallink.indexOf(":") != -1)
1009: debug = 77;
1010: reallink = cleanLink(reallink);
1011:
1012: callMutatorChain(m_localLinkMutatorChain, reallink);
1013:
1014: String matchedLink;
1015: if (reallink != null && reallink.endsWith(":"))
1016: matchedLink = reallink; // link to another wiki's default page - use READ mode
1017: else
1018: matchedLink = linkExists(reallink);
1019:
1020: if (matchedLink != null) {
1021: sb.append(makeLink(READ, matchedLink, link));
1022: } else {
1023: sb.append(makeLink(EDIT, reallink, link));
1024: }
1025: }
1026: }
1027:
1028: return sb.toString();
1029: }
1030:
1031: private String findAttachment(String link) {
1032: AttachmentManager mgr = m_engine.getAttachmentManager();
1033: WikiPage currentPage = m_context.getPage();
1034: Attachment att = null;
1035:
1036: /*
1037: System.out.println("Finding attachment of page "+currentPage.getName());
1038: System.out.println("With name "+link);
1039: */
1040:
1041: try {
1042: att = mgr.getAttachmentInfo(m_context, link);
1043: } catch (ProviderException e) {
1044: log.warn("Finding attachments failed: ", e);
1045: return null;
1046: }
1047:
1048: if (att != null) {
1049: return att.getName();
1050: } else if (link.indexOf('/') != -1) {
1051: return link;
1052: }
1053:
1054: return null;
1055: }
1056:
1057: /**
1058: * Closes all annoying lists and things that the user might've
1059: * left open.
1060: */
1061: private String closeAll() {
1062: StringBuffer buf = new StringBuffer();
1063:
1064: if (m_isbold) {
1065: buf.append(m_renderer.closeTextEffect(BOLD));
1066: m_isbold = false;
1067: }
1068:
1069: if (m_isitalic) {
1070: buf.append(m_renderer.closeTextEffect(ITALIC));
1071: m_isitalic = false;
1072: }
1073:
1074: if (m_isTypedText) {
1075: buf.append(m_renderer.closeTextEffect(TYPED));
1076: m_isTypedText = false;
1077: }
1078:
1079: /*
1080: for( ; m_listlevel > 0; m_listlevel-- )
1081: {
1082: buf.append( "</ul>\n" );
1083: }
1084:
1085: for( ; m_numlistlevel > 0; m_numlistlevel-- )
1086: {
1087: buf.append( "</ol>\n" );
1088: }
1089: */
1090: // cleanup OL and UL lists
1091: buf.append(unwindGeneralList());
1092:
1093: if (m_isPre) {
1094: buf.append(m_renderer.closePreformatted());
1095: m_isEscaping = false;
1096: m_isPre = false;
1097: }
1098:
1099: if (m_istable) {
1100: buf.append(m_renderer.closeTable());
1101: m_istable = false;
1102: }
1103:
1104: if (m_isOpenParagraph) {
1105: buf.append(m_renderer.closeParagraph());
1106: m_isOpenParagraph = false;
1107: }
1108:
1109: return buf.toString();
1110: }
1111:
1112: // FIXME: should remove countChar() as it is unused
1113: /**
1114: * Counts how many consecutive characters of a certain type exists on the line.
1115: * @param line String of chars to check.
1116: * @param startPos Position to start reading from.
1117: * @param char Character to check for.
1118: */
1119: private int countChar(String line, int startPos, char c) {
1120: int count;
1121:
1122: for (count = 0; (startPos + count < line.length())
1123: && (line.charAt(count + startPos) == c); count++)
1124: ;
1125:
1126: return count;
1127: }
1128:
1129: /**
1130: * Returns a new String that has char c n times.
1131: */
1132: private String repeatChar(char c, int n) {
1133: StringBuffer sb = new StringBuffer();
1134: for (int i = 0; i < n; i++)
1135: sb.append(c);
1136:
1137: return sb.toString();
1138: }
1139:
1140: private int nextToken() throws IOException {
1141: if (m_in == null)
1142: return -1;
1143: return m_in.read();
1144: }
1145:
1146: /**
1147: * Push back any character to the current input. Does not
1148: * push back a read EOF, though.
1149: */
1150: private void pushBack(int c) throws IOException {
1151: if (c != -1 && m_in != null) {
1152: m_in.unread(c);
1153: }
1154: }
1155:
1156: /**
1157: * Pushes back any string that has been read. It will obviously
1158: * be pushed back in a reverse order.
1159: *
1160: * @since 2.1.77
1161: */
1162: private void pushBack(String s) throws IOException {
1163: for (int i = s.length() - 1; i >= 0; i--) {
1164: pushBack(s.charAt(i));
1165: }
1166: }
1167:
1168: private String handleBackslash() throws IOException {
1169: int ch = nextToken();
1170:
1171: if (ch == '\\') {
1172: int ch2 = nextToken();
1173:
1174: if (ch2 == '\\') {
1175: return m_renderer.lineBreak(true);
1176: }
1177:
1178: pushBack(ch2);
1179:
1180: return m_renderer.lineBreak(false);
1181: }
1182:
1183: pushBack(ch);
1184:
1185: return "\\";
1186: }
1187:
1188: private String handleUnderscore() throws IOException {
1189: int ch = nextToken();
1190: String res = "_";
1191:
1192: if (ch == '_') {
1193: res = m_isbold ? m_renderer.closeTextEffect(BOLD)
1194: : m_renderer.openTextEffect(BOLD);
1195: m_isbold = !m_isbold;
1196: } else {
1197: pushBack(ch);
1198: }
1199:
1200: return res;
1201: }
1202:
1203: /**
1204: * For example: italics.
1205: */
1206: private String handleApostrophe() throws IOException {
1207: int ch = nextToken();
1208: String res = "'";
1209:
1210: if (ch == '\'') {
1211: res = m_isitalic ? m_renderer.closeTextEffect(ITALIC)
1212: : m_renderer.openTextEffect(ITALIC);
1213: m_isitalic = !m_isitalic;
1214: } else {
1215: pushBack(ch);
1216: }
1217:
1218: return res;
1219: }
1220:
1221: private String handleOpenbrace(boolean isBlock) throws IOException {
1222: int ch = nextToken();
1223: String res = "{";
1224:
1225: if (ch == '{') {
1226: int ch2 = nextToken();
1227:
1228: if (ch2 == '{') {
1229: res = startBlockLevel()
1230: + m_renderer.openPreformatted(isBlock);
1231: m_isPre = true;
1232: m_isEscaping = true;
1233: } else {
1234: pushBack(ch2);
1235:
1236: res = m_renderer.openTextEffect(TYPED);
1237: m_isTypedText = true;
1238: }
1239: } else {
1240: pushBack(ch);
1241: }
1242:
1243: return res;
1244: }
1245:
1246: /**
1247: * Handles both }} and }}}
1248: */
1249: private String handleClosebrace() throws IOException {
1250: String res = "}";
1251:
1252: int ch2 = nextToken();
1253:
1254: if (ch2 == '}') {
1255: int ch3 = nextToken();
1256:
1257: if (ch3 == '}') {
1258: if (m_isPre) {
1259: m_isPre = false;
1260: m_isEscaping = false;
1261: res = m_renderer.closePreformatted();
1262: } else {
1263: res = "}}}";
1264: }
1265: } else {
1266: pushBack(ch3);
1267:
1268: if (!m_isEscaping) {
1269: res = m_renderer.closeTextEffect(TYPED);
1270: m_isTypedText = false;
1271: } else {
1272: pushBack(ch2);
1273: }
1274: }
1275: } else {
1276: pushBack(ch2);
1277: }
1278:
1279: return res;
1280: }
1281:
1282: private String handleDash() throws IOException {
1283: int ch = nextToken();
1284:
1285: if (ch == '-') {
1286: int ch2 = nextToken();
1287:
1288: if (ch2 == '-') {
1289: int ch3 = nextToken();
1290:
1291: if (ch3 == '-') {
1292: // Empty away all the rest of the dashes.
1293: // Do not forget to return the first non-match back.
1294: while ((ch = nextToken()) == '-')
1295: ;
1296:
1297: pushBack(ch);
1298: return startBlockLevel() + m_renderer.makeRuler();
1299: }
1300:
1301: pushBack(ch3);
1302: }
1303: pushBack(ch2);
1304: }
1305:
1306: pushBack(ch);
1307:
1308: return "-";
1309: }
1310:
1311: /**
1312: * This method peeks ahead in the stream until EOL and returns the result.
1313: * It will keep the buffers untouched.
1314: *
1315: * @return The string from the current position to the end of line.
1316: */
1317:
1318: // FIXME: Always returns an empty line, even if the stream is full.
1319: private String peekAheadLine() throws IOException {
1320: String s = readUntilEOL().toString();
1321: pushBack(s);
1322:
1323: return s;
1324: }
1325:
1326: private String handleHeading() throws IOException {
1327: StringBuffer buf = new StringBuffer();
1328:
1329: int ch = nextToken();
1330:
1331: Heading hd = new Heading();
1332:
1333: if (ch == '!') {
1334: int ch2 = nextToken();
1335:
1336: if (ch2 == '!') {
1337: String title = peekAheadLine();
1338:
1339: buf.append(m_renderer.makeHeading(
1340: Heading.HEADING_LARGE, title, hd));
1341: } else {
1342: pushBack(ch2);
1343: String title = peekAheadLine();
1344: buf.append(m_renderer.makeHeading(
1345: Heading.HEADING_MEDIUM, title, hd));
1346: }
1347: } else {
1348: pushBack(ch);
1349: String title = peekAheadLine();
1350: buf.append(m_renderer.makeHeading(Heading.HEADING_SMALL,
1351: title, hd));
1352: }
1353:
1354: callHeadingListenerChain(hd);
1355:
1356: return buf.toString();
1357: }
1358:
1359: /**
1360: * Reads the stream until the next EOL or EOF. Note that it will also read the
1361: * EOL from the stream.
1362: */
1363: private StringBuffer readUntilEOL() throws IOException {
1364: int ch;
1365: StringBuffer buf = new StringBuffer();
1366:
1367: while (true) {
1368: ch = nextToken();
1369:
1370: if (ch == -1)
1371: break;
1372:
1373: buf.append((char) ch);
1374:
1375: if (ch == '\n')
1376: break;
1377: }
1378:
1379: return buf;
1380: }
1381:
1382: /**
1383: * Starts a block level element, therefore closing the
1384: * a potential open paragraph tag.
1385: */
1386: private String startBlockLevel() {
1387: if (m_isOpenParagraph) {
1388: m_isOpenParagraph = false;
1389: return m_renderer.closeParagraph();
1390: }
1391:
1392: return "";
1393: }
1394:
1395: /**
1396: * Like original handleOrderedList() and handleUnorderedList()
1397: * however handles both ordered ('#') and unordered ('*') mixed together.
1398: */
1399:
1400: // FIXME: Refactor this; it's a bit messy.
1401: private String handleGeneralList() throws IOException {
1402: String cStrShortName = "handleGeneralList()"; //put log messages in some context
1403:
1404: StringBuffer buf = new StringBuffer();
1405:
1406: buf.append(startBlockLevel());
1407:
1408: String strBullets = readWhile("*#");
1409: String strBulletsRaw = strBullets; // to know what was original before phpwiki style substitution
1410: int numBullets = strBullets.length();
1411:
1412: // override the beginning portion of bullet pattern to be like the previous
1413: // to simulate PHPWiki style lists
1414:
1415: if (m_allowPHPWikiStyleLists) {
1416: // only substitute if different
1417: if (!(strBullets.substring(0, Math.min(numBullets,
1418: m_genlistlevel))
1419: .equals(m_genlistBulletBuffer.substring(0, Math
1420: .min(numBullets, m_genlistlevel))))) {
1421: if (numBullets <= m_genlistlevel) {
1422: // Substitute all but the last character (keep the expressed bullet preference)
1423: strBullets = (numBullets > 1 ? m_genlistBulletBuffer
1424: .substring(0, numBullets - 1)
1425: : "")
1426: + strBullets.substring(numBullets - 1,
1427: numBullets);
1428: } else {
1429: strBullets = m_genlistBulletBuffer
1430: + strBullets.substring(m_genlistlevel,
1431: numBullets);
1432: }
1433: }
1434: }
1435:
1436: //
1437: // Check if this is still of the same type
1438: //
1439: if (strBullets.substring(0,
1440: Math.min(numBullets, m_genlistlevel)).equals(
1441: m_genlistBulletBuffer.substring(0, Math.min(numBullets,
1442: m_genlistlevel)))) {
1443: int chBullet;
1444:
1445: if (numBullets > m_genlistlevel) {
1446: buf.append(m_renderer.openList(strBullets
1447: .charAt(m_genlistlevel++)));
1448:
1449: for (; m_genlistlevel < numBullets; m_genlistlevel++) {
1450: // bullets are growing, get from new bullet list
1451: buf.append(m_renderer.openListItem());
1452: buf.append(m_renderer.openList(strBullets
1453: .charAt(m_genlistlevel)));
1454: }
1455: } else if (numBullets < m_genlistlevel) {
1456: // Close the previous list item.
1457: buf.append(m_renderer.closeListItem());
1458:
1459: for (; m_genlistlevel > numBullets; m_genlistlevel--) {
1460: // bullets are shrinking, get from old bullet list
1461: buf.append(m_renderer
1462: .closeList(m_genlistBulletBuffer
1463: .charAt(m_genlistlevel - 1)));
1464: if (m_genlistlevel > 0)
1465: buf.append(m_renderer.closeListItem());
1466:
1467: }
1468: } else {
1469: if (m_genlistlevel > 0)
1470: buf.append(m_renderer.closeListItem());
1471: }
1472: } else {
1473: //
1474: // The pattern has changed, unwind and restart
1475: //
1476: char chBullet;
1477: int numEqualBullets;
1478: int numCheckBullets;
1479:
1480: // find out how much is the same
1481: numEqualBullets = 0;
1482: numCheckBullets = Math.min(numBullets, m_genlistlevel);
1483:
1484: while (numEqualBullets < numCheckBullets) {
1485: // if the bullets are equal so far, keep going
1486: if (strBullets.charAt(numEqualBullets) == m_genlistBulletBuffer
1487: .charAt(numEqualBullets))
1488: numEqualBullets++;
1489: // otherwise giveup, we have found how many are equal
1490: else
1491: break;
1492: }
1493:
1494: //unwind
1495: for (; m_genlistlevel > numEqualBullets; m_genlistlevel--) {
1496: buf.append(m_renderer.closeList(m_genlistBulletBuffer
1497: .charAt(m_genlistlevel - 1)));
1498: if (m_genlistlevel > 0)
1499: buf.append(m_renderer.closeListItem());
1500: }
1501:
1502: //rewind
1503: buf.append(m_renderer.openList(strBullets
1504: .charAt(numEqualBullets++)));
1505: for (int i = numEqualBullets; i < numBullets; i++) {
1506: buf.append(m_renderer.openListItem());
1507: buf.append(m_renderer.openList(strBullets.charAt(i)));
1508: }
1509: m_genlistlevel = numBullets;
1510: }
1511: buf.append(m_renderer.openListItem());
1512:
1513: // work done, remember the new bullet list (in place of old one)
1514: m_genlistBulletBuffer.setLength(0);
1515: m_genlistBulletBuffer.append(strBullets);
1516:
1517: return buf.toString();
1518: }
1519:
1520: private String unwindGeneralList() {
1521: // String cStrShortName = "unwindGeneralList()";
1522:
1523: StringBuffer buf = new StringBuffer();
1524: int bulletCh;
1525:
1526: //unwind
1527: for (; m_genlistlevel > 0; m_genlistlevel--) {
1528: buf.append(m_renderer.closeListItem());
1529: buf.append(m_renderer.closeList(m_genlistBulletBuffer
1530: .charAt(m_genlistlevel - 1)));
1531: }
1532:
1533: m_genlistBulletBuffer.setLength(0);
1534:
1535: return buf.toString();
1536: }
1537:
1538: private String handleDefinitionList() throws IOException {
1539: if (!m_isdefinition) {
1540: m_isdefinition = true;
1541:
1542: m_closeTag = m_renderer.closeDefinitionItem()
1543: + m_renderer.closeDefinitionList();
1544:
1545: return startBlockLevel() + m_renderer.openDefinitionList()
1546: + m_renderer.openDefinitionTitle();
1547: }
1548:
1549: return ";";
1550: }
1551:
1552: private String handleOpenbracket() throws IOException {
1553: StringBuffer sb = new StringBuffer();
1554: int ch;
1555: boolean isPlugin = false;
1556:
1557: while ((ch = nextToken()) == '[') {
1558: sb.append((char) ch);
1559: }
1560:
1561: if (ch == '{') {
1562: isPlugin = true;
1563: }
1564:
1565: pushBack(ch);
1566:
1567: if (sb.length() > 0) {
1568: return sb.toString();
1569: }
1570:
1571: //
1572: // Find end of hyperlink
1573: //
1574:
1575: ch = nextToken();
1576:
1577: while (ch != -1) {
1578: if (ch == ']'
1579: && (!isPlugin || sb.charAt(sb.length() - 1) == '}')) {
1580: break;
1581: }
1582:
1583: sb.append((char) ch);
1584:
1585: ch = nextToken();
1586: }
1587:
1588: if (ch == -1) {
1589: log.debug("Warning: unterminated link detected!");
1590: return sb.toString();
1591: }
1592:
1593: return handleHyperlinks(sb.toString());
1594: }
1595:
1596: /**
1597: * Reads the stream until the current brace is closed or stream end.
1598: */
1599: private String readBraceContent(char opening, char closing)
1600: throws IOException {
1601: StringBuffer sb = new StringBuffer();
1602: int braceLevel = 1;
1603: int ch;
1604: while ((ch = nextToken()) != -1) {
1605: if (ch == '\\') {
1606: continue;
1607: } else if (ch == opening) {
1608: braceLevel++;
1609: } else if (ch == closing) {
1610: braceLevel--;
1611: if (braceLevel == 0) {
1612: break;
1613: }
1614: }
1615: sb.append((char) ch);
1616: }
1617: return sb.toString();
1618: }
1619:
1620: /**
1621: * Reads the stream until it meets one of the specified
1622: * ending characters, or stream end. The ending character will be left
1623: * in the stream.
1624: */
1625: private String readUntil(String endChars) throws IOException {
1626: StringBuffer sb = new StringBuffer();
1627: int ch = nextToken();
1628:
1629: while (ch != -1) {
1630: if (ch == '\\') {
1631: ch = nextToken();
1632: if (ch == -1) {
1633: break;
1634: }
1635: } else {
1636: if (endChars.indexOf((char) ch) != -1) {
1637: pushBack(ch);
1638: break;
1639: }
1640: }
1641: sb.append((char) ch);
1642: ch = nextToken();
1643: }
1644:
1645: return sb.toString();
1646: }
1647:
1648: /**
1649: * Reads the stream while the characters that have been specified are
1650: * in the stream, returning then the result as a String.
1651: */
1652: private String readWhile(String endChars) throws IOException {
1653: StringBuffer sb = new StringBuffer();
1654: int ch = nextToken();
1655:
1656: while (ch != -1) {
1657: if (endChars.indexOf((char) ch) == -1) {
1658: pushBack(ch);
1659: break;
1660: }
1661:
1662: sb.append((char) ch);
1663: ch = nextToken();
1664: }
1665:
1666: return sb.toString();
1667: }
1668:
1669: /**
1670: * Handles constructs of type %%(style) and %%class
1671: * @param newLine
1672: * @return
1673: * @throws IOException
1674: */
1675: private String handleDiv(boolean newLine) throws IOException {
1676: int ch = nextToken();
1677:
1678: if (ch == '%') {
1679: StringBuffer sb = new StringBuffer();
1680:
1681: String style = null;
1682: String clazz = null;
1683:
1684: ch = nextToken();
1685:
1686: boolean isspan = false;
1687:
1688: //
1689: // Style or class?
1690: //
1691: if (ch == '(') {
1692: style = readBraceContent('(', ')');
1693: } else if (Character.isLetter((char) ch)) {
1694: pushBack(ch);
1695: clazz = readUntil(" \t\n\r");
1696: ch = nextToken();
1697:
1698: //
1699: // Pop out only spaces, so that the upcoming EOL check does not check the
1700: // next line.
1701: //
1702: if (ch == '\n' || ch == '\r') {
1703: pushBack(ch);
1704: }
1705: } else {
1706: //
1707: // Anything else stops.
1708: //
1709:
1710: pushBack(ch);
1711:
1712: try {
1713: Boolean isSpan = (Boolean) m_styleStack.pop();
1714:
1715: if (isSpan == null) {
1716: // Fail quietly
1717: } else if (isSpan.booleanValue()) {
1718: sb.append(m_renderer.closeSpan());
1719: } else {
1720: sb.append(m_renderer.closeDiv());
1721: }
1722: } catch (EmptyStackException e) {
1723: log
1724: .debug("Page '"
1725: + m_context.getPage().getName()
1726: + "' closes a %%-block that has not been opened.");
1727: }
1728:
1729: return sb.toString();
1730: }
1731:
1732: //
1733: // Decide if we should open a div or a span?
1734: //
1735: String eol = peekAheadLine();
1736:
1737: if (eol.trim().length() > 0) {
1738: // There is stuff after the class
1739:
1740: sb.append(m_renderer.openSpan(style, clazz));
1741:
1742: m_styleStack.push(Boolean.TRUE);
1743: } else {
1744: sb.append(startBlockLevel());
1745: sb.append(m_renderer.openDiv(style, clazz));
1746: m_styleStack.push(Boolean.FALSE);
1747: }
1748:
1749: return sb.toString();
1750: }
1751:
1752: pushBack(ch);
1753:
1754: return "%";
1755: }
1756:
1757: private String handleBar(boolean newLine) throws IOException {
1758: StringBuffer sb = new StringBuffer();
1759:
1760: if (!m_istable && !newLine) {
1761: return "|";
1762: }
1763:
1764: if (newLine) {
1765: if (!m_istable) {
1766: sb.append(startBlockLevel());
1767: sb.append(m_renderer.openTable());
1768: m_istable = true;
1769: }
1770:
1771: sb.append(m_renderer.openTableRow());
1772: m_closeTag = m_renderer.closeTableItem()
1773: + m_renderer.closeTableRow();
1774: }
1775:
1776: int ch = nextToken();
1777:
1778: if (ch == '|') {
1779: if (!newLine) {
1780: sb.append(m_renderer.closeTableHeading());
1781: }
1782: sb.append(m_renderer.openTableHeading());
1783: m_closeTag = m_renderer.closeTableHeading()
1784: + m_renderer.closeTableRow();
1785: } else {
1786: if (!newLine) {
1787: sb.append(m_renderer.closeTableItem());
1788: }
1789: sb.append(m_renderer.openTableItem());
1790: pushBack(ch);
1791: }
1792:
1793: return sb.toString();
1794: }
1795:
1796: /**
1797: * Generic escape of next character or entity.
1798: */
1799: private String handleTilde() throws IOException {
1800: int ch = nextToken();
1801:
1802: if (ch == '|' || ch == '~' || ch == '\\' || ch == '*'
1803: || ch == '#' || ch == '-' || ch == '!' || ch == '\''
1804: || ch == '_' || ch == '[' || ch == '{' || ch == ']'
1805: || ch == '}') {
1806: StringBuffer sb = new StringBuffer();
1807: sb.append((char) ch);
1808: sb.append(readWhile("" + (char) ch));
1809: return sb.toString();
1810: }
1811:
1812: if (Character.isUpperCase((char) ch)) {
1813: return String.valueOf((char) ch);
1814: }
1815:
1816: // No escape.
1817: pushBack(ch);
1818:
1819: return "~";
1820: }
1821:
1822: private void fillBuffer() throws IOException {
1823: StringBuffer buf = new StringBuffer();
1824: StringBuffer word = null;
1825: int previousCh = -2;
1826: int start = 0;
1827:
1828: boolean quitReading = false;
1829: boolean newLine = true; // FIXME: not true if reading starts in middle of buffer
1830:
1831: while (!quitReading) {
1832: int ch = nextToken();
1833: String s = null;
1834:
1835: //
1836: // Check if we're actually ending the preformatted mode.
1837: // We still must do an entity transformation here.
1838: //
1839: if (m_isEscaping) {
1840: if (ch == '}') {
1841: buf.append(handleClosebrace());
1842: } else if (ch == -1) {
1843: quitReading = true;
1844: } else {
1845: m_renderer.doChar(buf, (char) ch);
1846: }
1847:
1848: continue;
1849: }
1850:
1851: //
1852: // CamelCase detection, a non-trivial endeavour.
1853: // We keep track of all white-space separated entities, which we
1854: // hereby refer to as "words". We then check for an existence
1855: // of a CamelCase format text string inside the "word", and
1856: // if one exists, we replace it with a proper link.
1857: //
1858:
1859: if (m_camelCaseLinks) {
1860: // Quick parse of start of a word boundary.
1861:
1862: if (word == null
1863: && (Character.isWhitespace((char) previousCh)
1864: || WORD_SEPARATORS
1865: .indexOf((char) previousCh) != -1 || newLine)
1866: && !Character.isWhitespace((char) ch)) {
1867: word = new StringBuffer();
1868: }
1869:
1870: // Are we currently tracking a word?
1871: if (word != null) {
1872: //
1873: // Check for the end of the word.
1874: //
1875:
1876: if (Character.isWhitespace((char) ch) || ch == -1
1877: || WORD_SEPARATORS.indexOf((char) ch) != -1) {
1878: String potentialLink = word.toString();
1879:
1880: String camelCase = checkForCamelCaseLink(potentialLink);
1881:
1882: if (camelCase != null) {
1883: // System.out.println("Buffer is "+buf);
1884:
1885: // System.out.println(" Replacing "+camelCase+" with proper link.");
1886: start = buf.toString().lastIndexOf(
1887: camelCase);
1888: buf.replace(start, start
1889: + camelCase.length(),
1890: makeCamelCaseLink(camelCase));
1891:
1892: // System.out.println(" Resulting with "+buf);
1893: } else {
1894: // System.out.println("Checking for potential URI: "+potentialLink);
1895: if (isExternalLink(potentialLink)) {
1896: // System.out.println("buf="+buf);
1897: start = buf.toString().lastIndexOf(
1898: potentialLink);
1899:
1900: if (start >= 0) {
1901: String link = readUntil(" \t()[]{}!\"'\n|");
1902:
1903: link = potentialLink + (char) ch
1904: + link; // Do not forget the start.
1905:
1906: // System.out.println("start="+start+", pl="+potentialLink);
1907:
1908: buf.replace(start, start
1909: + potentialLink.length(),
1910: makeDirectURILink(link));
1911:
1912: // System.out.println("Resulting with "+buf);
1913:
1914: ch = nextToken();
1915: }
1916: }
1917: }
1918:
1919: // We've ended a word boundary, so time to reset.
1920: word = null;
1921: } else {
1922: // This should only be appending letters and digits.
1923: word.append((char) ch);
1924: } // if end of word
1925: } // if word's not null
1926:
1927: // Always set the previous character to test for word starts.
1928: previousCh = ch;
1929:
1930: } // if m_camelCaseLinks
1931:
1932: //
1933: // An empty line stops a list
1934: //
1935: if (newLine && ch != '*' && ch != '#' && ch != ' '
1936: && m_genlistlevel > 0) {
1937: buf.append(unwindGeneralList());
1938: }
1939:
1940: if (newLine && ch != '|' && m_istable) {
1941: buf.append(m_renderer.closeTable());
1942: m_istable = false;
1943: m_closeTag = null;
1944: }
1945:
1946: //
1947: // Now, check the incoming token.
1948: //
1949: switch (ch) {
1950: case '\r':
1951: // DOS linefeeds we forget
1952: s = null;
1953: break;
1954:
1955: case '\n':
1956: //
1957: // Close things like headings, etc.
1958: //
1959: if (m_closeTag != null) {
1960: buf.append(m_closeTag);
1961: m_closeTag = null;
1962: }
1963:
1964: m_isdefinition = false;
1965:
1966: if (newLine) {
1967: // Paragraph change.
1968: buf.append(startBlockLevel());
1969:
1970: //
1971: // Figure out which elements cannot be enclosed inside
1972: // a <p></p> pair according to XHTML rules.
1973: //
1974: String nextLine = peekAheadLine();
1975: if (nextLine.length() == 0
1976: || (nextLine.length() > 0
1977: && !nextLine.startsWith("{{{")
1978: && !nextLine.startsWith("----")
1979: && !nextLine.startsWith("%%") && "*#!;"
1980: .indexOf(nextLine.charAt(0)) == -1)) {
1981: buf.append(m_renderer.openParagraph());
1982: m_isOpenParagraph = true;
1983: }
1984: } else {
1985: buf.append("\n");
1986: newLine = true;
1987: }
1988:
1989: break;
1990:
1991: case '\\':
1992: s = handleBackslash();
1993: break;
1994:
1995: case '_':
1996: s = handleUnderscore();
1997: break;
1998:
1999: case '\'':
2000: s = handleApostrophe();
2001: break;
2002:
2003: case '{':
2004: s = handleOpenbrace(newLine);
2005: break;
2006:
2007: case '}':
2008: s = handleClosebrace();
2009: break;
2010:
2011: case '-':
2012: s = handleDash();
2013: break;
2014:
2015: case '!':
2016: if (newLine) {
2017: s = handleHeading();
2018: } else {
2019: s = "!";
2020: }
2021: break;
2022:
2023: case ';':
2024: if (newLine) {
2025: s = handleDefinitionList();
2026: } else {
2027: s = ";";
2028: }
2029: break;
2030:
2031: case ':':
2032: if (m_isdefinition) {
2033: s = m_renderer.closeDefinitionTitle()
2034: + m_renderer.openDefinitionItem();
2035: m_isdefinition = false;
2036: } else {
2037: s = ":";
2038: }
2039: break;
2040:
2041: case '[':
2042: s = handleOpenbracket();
2043: break;
2044:
2045: case '*':
2046: if (newLine) {
2047: pushBack('*');
2048: s = handleGeneralList();
2049: } else {
2050: s = "*";
2051: }
2052: break;
2053:
2054: case '#':
2055: if (newLine) {
2056: pushBack('#');
2057: s = handleGeneralList();
2058: } else {
2059: s = "#";
2060: }
2061: break;
2062:
2063: case '|':
2064: s = handleBar(newLine);
2065: break;
2066:
2067: case '<':
2068: s = m_allowHTML ? "<" : "<";
2069: break;
2070:
2071: case '>':
2072: s = m_allowHTML ? ">" : ">";
2073: break;
2074:
2075: case '\"':
2076: s = m_allowHTML ? "\"" : """;
2077: break;
2078:
2079: /*
2080: case '&':
2081: s = "&";
2082: break;
2083: */
2084: case '~':
2085: s = handleTilde();
2086: break;
2087:
2088: case '%':
2089: s = handleDiv(newLine);
2090: break;
2091:
2092: case -1:
2093: if (m_closeTag != null) {
2094: buf.append(m_closeTag);
2095: m_closeTag = null;
2096: }
2097: quitReading = true;
2098: break;
2099:
2100: default:
2101: buf.append((char) ch);
2102: newLine = false;
2103: break;
2104: }
2105:
2106: if (s != null) {
2107: buf.append(s);
2108: newLine = false;
2109: }
2110:
2111: }
2112:
2113: m_data = new StringReader(buf.toString());
2114: }
2115:
2116: public int read() throws IOException {
2117: int val = m_data.read();
2118:
2119: if (val == -1) {
2120: fillBuffer();
2121: val = m_data.read();
2122:
2123: if (val == -1) {
2124: m_data = new StringReader(closeAll());
2125:
2126: val = m_data.read();
2127: }
2128: }
2129:
2130: return val;
2131: }
2132:
2133: public int read(char[] buf, int off, int len) throws IOException {
2134: return m_data.read(buf, off, len);
2135: }
2136:
2137: public boolean ready() throws IOException {
2138: log.debug("ready ? " + m_data.ready());
2139: if (!m_data.ready()) {
2140: fillBuffer();
2141: }
2142:
2143: return m_data.ready();
2144: }
2145:
2146: public void close() {
2147: }
2148:
2149: /**
2150: * All HTML output stuff is here. This class is a helper class, and will
2151: * be spawned later on with a proper API of its own so that we can have
2152: * different kinds of renderers.
2153: */
2154:
2155: // FIXME: Not everything is yet, and in the future this class will be spawned
2156: // out to be its own class.
2157: private class HTMLRenderer extends TextRenderer {
2158: private boolean m_isPreBlock = false;
2159: private TranslatorReader m_cleanTranslator;
2160:
2161: /*
2162: FIXME: It's relatively slow to create two TranslatorReaders each time.
2163: */
2164: public HTMLRenderer() {
2165: }
2166:
2167: /**
2168: * Does a lazy init. Otherwise, we would get into a situation
2169: * where HTMLRenderer would try and boot a TranslatorReader before
2170: * the TranslatorReader it is contained by is up.
2171: */
2172: private TranslatorReader getCleanTranslator() {
2173: if (m_cleanTranslator == null) {
2174: WikiContext dummyContext = new WikiContext(m_engine,
2175: m_context.getPage());
2176: m_cleanTranslator = new TranslatorReader(dummyContext,
2177: null, new TextRenderer());
2178: m_cleanTranslator.m_allowHTML = true;
2179: }
2180:
2181: return m_cleanTranslator;
2182: }
2183:
2184: public void doChar(StringBuffer buf, char ch) {
2185: if (ch == '<') {
2186: buf.append("<");
2187: } else if (ch == '>') {
2188: buf.append(">");
2189: } else if (ch == '&') {
2190: buf.append("&");
2191: } else {
2192: buf.append(ch);
2193: }
2194: }
2195:
2196: public String openDiv(String style, String clazz) {
2197: StringBuffer sb = new StringBuffer();
2198:
2199: sb.append("<div");
2200: sb.append(style != null ? " style=\"" + style + "\"" : "");
2201: sb.append(clazz != null ? " class=\"" + clazz + "\"" : "");
2202: sb.append(">");
2203:
2204: return sb.toString();
2205: }
2206:
2207: public String openSpan(String style, String clazz) {
2208: StringBuffer sb = new StringBuffer();
2209:
2210: sb.append("<span");
2211: sb.append(style != null ? " style=\"" + style + "\"" : "");
2212: sb.append(clazz != null ? " class=\"" + clazz + "\"" : "");
2213: sb.append(">");
2214:
2215: return sb.toString();
2216: }
2217:
2218: public String closeDiv() {
2219: return "</div>";
2220: }
2221:
2222: public String closeSpan() {
2223: return "</span>";
2224: }
2225:
2226: public String openParagraph() {
2227: return "<p>";
2228: }
2229:
2230: public String closeParagraph() {
2231: return "</p>\n";
2232: }
2233:
2234: /**
2235: * Writes out a text effect
2236: */
2237: public String openTextEffect(int effect) {
2238: switch (effect) {
2239: case BOLD:
2240: return "<b>";
2241: case ITALIC:
2242: return "<i>";
2243: case TYPED:
2244: return "<tt>";
2245: }
2246:
2247: return "";
2248: }
2249:
2250: public String closeTextEffect(int effect) {
2251: switch (effect) {
2252: case BOLD:
2253: return "</b>";
2254: case ITALIC:
2255: return "</i>";
2256: case TYPED:
2257: return "</tt>";
2258: }
2259:
2260: return "";
2261: }
2262:
2263: public String openDefinitionItem() {
2264: return "<dd>";
2265: }
2266:
2267: public String closeDefinitionItem() {
2268: return "</dd>\n";
2269: }
2270:
2271: public String openDefinitionTitle() {
2272: return "<dt>";
2273: }
2274:
2275: public String closeDefinitionTitle() {
2276: return "</dt>";
2277: }
2278:
2279: public String openDefinitionList() {
2280: return "<dl>\n";
2281: }
2282:
2283: public String closeDefinitionList() {
2284: return "</dl>";
2285: }
2286:
2287: /**
2288: * Write a HTMLized link depending on its type.
2289: *
2290: * <p>This jsut calls makeLink() with "section" set to null.
2291: */
2292: public String makeLink(int type, String link, String text) {
2293: return makeLink(type, link, text, null);
2294: }
2295:
2296: private final String getURL(String context, String link) {
2297: return m_context.getURL(context, link, null);
2298: }
2299:
2300: /**
2301: * Write a HTMLized link depending on its type.
2302: *
2303: * @param type Type of the link.
2304: * @param link The actual link.
2305: * @param text The user-visible text for the link.
2306: * @param section Which named anchor to point to. This may not have any
2307: * effect on certain link types. If null, will ignore it.
2308: */
2309: public String makeLink(int type, String link, String text,
2310: String section) {
2311: String result;
2312:
2313: if (text == null)
2314: text = link;
2315:
2316: section = (section != null) ? ("#" + section) : "";
2317:
2318: // Make sure we make a link name that can be accepted
2319: // as a valid URL.
2320:
2321: String encodedlink = m_engine.encodeName(link);
2322:
2323: if (encodedlink.length() == 0) {
2324: type = EMPTY;
2325: }
2326:
2327: switch (type) {
2328: case READ:
2329: result = "<a class=\"wikipage\" href=\""
2330: + getURL(WikiContext.VIEW, link) + section
2331: + "\">" + text + "</a>";
2332: break;
2333:
2334: case EDIT:
2335: result = "<a class=\"editpage\" title=\"Create '"
2336: + link + "'\" href=\""
2337: + getURL(WikiContext.EDIT, link) + "\">" + text
2338: + "</a>";
2339: break;
2340:
2341: case EMPTY:
2342: result = "<u>" + text + "</u>";
2343: break;
2344:
2345: //
2346: // These two are for local references - footnotes and
2347: // references to footnotes.
2348: // We embed the page name (or whatever WikiContext gives us)
2349: // to make sure the links are unique across Wiki.
2350: //
2351: case LOCALREF:
2352: result = "<a class=\"footnoteref\" href=\"#ref-"
2353: + m_context.getPage().getName() + "-" + link
2354: + "\">[" + text + "]</a>";
2355: break;
2356:
2357: case LOCAL:
2358: result = "<a class=\"footnote\" name=\"ref-"
2359: + m_context.getPage().getName() + "-"
2360: + link.substring(1) + "\">[" + text + "]</a>";
2361: break;
2362:
2363: //
2364: // With the image, external and interwiki types we need to
2365: // make sure nobody can put in Javascript or something else
2366: // annoying into the links themselves. We do this by preventing
2367: // a haxor from stopping the link name short with quotes in
2368: // fillBuffer().
2369: //
2370: case IMAGE:
2371: result = "<img class=\"inline\" src=\"" + link
2372: + "\" alt=\"" + text + "\" />";
2373: break;
2374:
2375: case IMAGELINK:
2376: result = "<a href=\"" + text
2377: + "\"><img class=\"inline\" src=\"" + link
2378: + "\" alt=\"" + text + "\"/></a>";
2379: break;
2380:
2381: case IMAGEWIKILINK:
2382: String pagelink = getURL(WikiContext.VIEW, text);
2383: result = "<a class=\"wikipage\" href=\"" + pagelink
2384: + "\"><img class=\"inline\" src=\"" + link
2385: + "\" alt=\"" + text + "\" /></a>";
2386: break;
2387:
2388: case EXTERNAL:
2389: result = "<a class=\"external\" "
2390: + (m_useRelNofollow ? "rel=\"nofollow\" " : "")
2391: + "href=\"" + link + section + "\">" + text
2392: + "</a>";
2393: break;
2394:
2395: case INTERWIKI:
2396: result = "<a class=\"interwiki\" href=\"" + link
2397: + section + "\">" + text + "</a>";
2398: break;
2399:
2400: case ATTACHMENT:
2401: String attlink = getURL(WikiContext.ATTACH, link);
2402:
2403: String infolink = getURL(WikiContext.INFO, link);
2404:
2405: String imglink = getURL(WikiContext.NONE,
2406: "images/attachment_small.png");
2407:
2408: result = "<a class=\"attachment\" href=\"" + attlink
2409: + "\">" + text + "</a>" + "<a href=\""
2410: + infolink + "\"><img src=\"" + imglink
2411: + "\" border=\"0\" alt=\"(info)\"/></a>";
2412: break;
2413:
2414: default:
2415: result = "";
2416: break;
2417: }
2418:
2419: return result;
2420: }
2421:
2422: /**
2423: * Writes HTML for error message.
2424: */
2425:
2426: public String makeError(String error) {
2427: return "<span class=\"error\">" + error + "</span>";
2428: }
2429:
2430: /**
2431: * Emits a vertical line.
2432: */
2433:
2434: public String makeRuler() {
2435: return "<hr />";
2436: }
2437:
2438: /**
2439: * Modifies the "hd" parameter to contain proper values. Because
2440: * an "id" tag may only contain [a-zA-Z0-9:_-], we'll replace the
2441: * % after url encoding with '_'.
2442: */
2443: private String makeHeadingAnchor(String baseName, String title,
2444: Heading hd) {
2445: hd.m_titleText = title;
2446: title = cleanLink(title);
2447: hd.m_titleSection = m_engine.encodeName(title);
2448: hd.m_titleAnchor = "section-"
2449: + m_engine.encodeName(baseName) + "-"
2450: + hd.m_titleSection;
2451:
2452: hd.m_titleAnchor = hd.m_titleAnchor.replace('%', '_');
2453: return hd.m_titleAnchor;
2454: }
2455:
2456: private String makeSectionTitle(String title) {
2457: title = title.trim();
2458:
2459: StringWriter outTitle = new StringWriter();
2460:
2461: try {
2462: TranslatorReader read = getCleanTranslator();
2463: read.setInputReader(new StringReader(title));
2464: FileUtil.copyContents(read, outTitle);
2465: } catch (IOException e) {
2466: log.fatal("CleanTranslator not working", e);
2467: throw new InternalWikiException(
2468: "CleanTranslator not working as expected, when cleaning title"
2469: + e.getMessage());
2470: }
2471:
2472: return outTitle.toString();
2473: }
2474:
2475: /**
2476: * Returns XHTML for the start of the heading. Also sets the
2477: * line-end emitter.
2478: * @param level
2479: * @param headings A List to which heading should be added.
2480: */
2481: public String makeHeading(int level, String title, Heading hd) {
2482: String res = "";
2483:
2484: String pageName = m_context.getPage().getName();
2485:
2486: String outTitle = makeSectionTitle(title);
2487:
2488: hd.m_level = level;
2489:
2490: switch (level) {
2491: case Heading.HEADING_SMALL:
2492: res = "<h4 id='"
2493: + makeHeadingAnchor(pageName, outTitle, hd)
2494: + "'>";
2495: m_closeTag = "</h4>";
2496: break;
2497:
2498: case Heading.HEADING_MEDIUM:
2499: res = "<h3 id='"
2500: + makeHeadingAnchor(pageName, outTitle, hd)
2501: + "'>";
2502: m_closeTag = "</h3>";
2503: break;
2504:
2505: case Heading.HEADING_LARGE:
2506: res = "<h2 id='"
2507: + makeHeadingAnchor(pageName, outTitle, hd)
2508: + "'>";
2509: m_closeTag = "</h2>";
2510: break;
2511: }
2512:
2513: return res;
2514: }
2515:
2516: /**
2517: * @param bullet A character detailing which kind of a list
2518: * we are dealing with here. Options are '#' and '*'.
2519: */
2520: public String openList(char bullet) {
2521: String res = "";
2522:
2523: if (bullet == '#')
2524: res = "<ol>\n";
2525: else if (bullet == '*')
2526: res = "<ul>\n";
2527: else
2528: log.info("Warning: unknown bullet character '" + bullet
2529: + "' at (+)");
2530:
2531: return res;
2532: }
2533:
2534: public String openListItem() {
2535: return "<li>";
2536: }
2537:
2538: public String closeListItem() {
2539: return "</li>\n";
2540: }
2541:
2542: /**
2543: * @param bullet A character detailing which kind of a list
2544: * we are dealing with here. Options are '#' and '*'.
2545: */
2546: public String closeList(char bullet) {
2547: String res = "";
2548:
2549: if (bullet == '#') {
2550: res = "</ol>\n";
2551: } else if (bullet == '*') {
2552: res = "</ul>\n";
2553: } else {
2554: //FIXME unknown character -> error
2555: log.info("Warning: unknown character in unwind '"
2556: + bullet + "'");
2557: }
2558:
2559: return res;
2560: }
2561:
2562: public String openTable() {
2563: return "<table class=\"wikitable\" border=\"1\">\n";
2564: }
2565:
2566: public String closeTable() {
2567: return "</table>\n";
2568: }
2569:
2570: public String openTableRow() {
2571: return "<tr>";
2572: }
2573:
2574: public String closeTableRow() {
2575: return "</tr>";
2576: }
2577:
2578: public String openTableItem() {
2579: return "<td>";
2580: }
2581:
2582: public String closeTableItem() {
2583: return "</td>";
2584: }
2585:
2586: public String openTableHeading() {
2587: return "<th>";
2588: }
2589:
2590: public String closeTableHeading() {
2591: return "</th>";
2592: }
2593:
2594: public String openPreformatted(boolean isBlock) {
2595: m_isPreBlock = isBlock;
2596:
2597: if (isBlock) {
2598: return "<pre>";
2599: }
2600:
2601: return "<span style=\"font-family:monospace; whitespace:pre;\">";
2602: }
2603:
2604: public String closePreformatted() {
2605: if (m_isPreBlock)
2606: return "</pre>\n";
2607:
2608: return "</span>";
2609: }
2610:
2611: /**
2612: * If outlink images are turned on, returns a link to the outward
2613: * linking image.
2614: */
2615: public String outlinkImage() {
2616: if (m_useOutlinkImage) {
2617: return "<img class=\"outlink\" src=\""
2618: + getURL(WikiContext.NONE, "images/out.png")
2619: + "\" alt=\"\" />";
2620: }
2621:
2622: return "";
2623: }
2624:
2625: /**
2626: * @param clear If true, then flushes all thingies.
2627: */
2628: public String lineBreak(boolean clear) {
2629: if (clear)
2630: return "<br clear=\"all\" />";
2631:
2632: return "<br />";
2633: }
2634:
2635: } // HTMLRenderer
2636:
2637: /**
2638: * A very simple class for outputting plain text with no
2639: * formatting.
2640: */
2641: private class TextRenderer {
2642: public void doChar(StringBuffer buf, char ch) {
2643: buf.append(ch);
2644: }
2645:
2646: public String openDiv(String style, String clazz) {
2647: return "";
2648: }
2649:
2650: public String closeDiv() {
2651: return "";
2652: }
2653:
2654: public String openSpan(String style, String clazz) {
2655: return "";
2656: }
2657:
2658: public String closeSpan() {
2659: return "";
2660: }
2661:
2662: public String openParagraph() {
2663: return "";
2664: }
2665:
2666: public String closeParagraph() {
2667: return "\n\n";
2668: }
2669:
2670: /**
2671: * Writes out a text effect
2672: */
2673: public String openTextEffect(int effect) {
2674: return "";
2675: }
2676:
2677: public String closeTextEffect(int effect) {
2678: return "";
2679: }
2680:
2681: public String openDefinitionItem() {
2682: return " : ";
2683: }
2684:
2685: public String closeDefinitionItem() {
2686: return "\n";
2687: }
2688:
2689: public String openDefinitionTitle() {
2690: return "";
2691: }
2692:
2693: public String closeDefinitionTitle() {
2694: return "";
2695: }
2696:
2697: public String openDefinitionList() {
2698: return "";
2699: }
2700:
2701: public String closeDefinitionList() {
2702: return "\n";
2703: }
2704:
2705: /**
2706: * Write a HTMLized link depending on its type.
2707: *
2708: * <p>This jsut calls makeLink() with "section" set to null.
2709: */
2710: public String makeLink(int type, String link, String text) {
2711: return text;
2712: }
2713:
2714: public String makeLink(int type, String link, String text,
2715: String section) {
2716: return text;
2717: }
2718:
2719: /**
2720: * Writes HTML for error message.
2721: */
2722:
2723: public String makeError(String error) {
2724: return "ERROR: " + error;
2725: }
2726:
2727: /**
2728: * Emits a vertical line.
2729: */
2730:
2731: public String makeRuler() {
2732: return "----------------------------------";
2733: }
2734:
2735: /**
2736: * Returns XHTML for the start of the heading. Also sets the
2737: * line-end emitter.
2738: * @param level
2739: */
2740: public String makeHeading(int level, String title, Heading hd) {
2741: String res = "";
2742:
2743: String pageName = m_context.getPage().getName();
2744:
2745: title = title.trim();
2746:
2747: hd.m_level = level;
2748: hd.m_titleText = title;
2749: hd.m_titleSection = "";
2750: hd.m_titleAnchor = "";
2751:
2752: switch (level) {
2753: case Heading.HEADING_SMALL:
2754: res = title;
2755: m_closeTag = "\n\n";
2756: break;
2757:
2758: case Heading.HEADING_MEDIUM:
2759: res = title;
2760: m_closeTag = "\n"
2761: + TextUtil.repeatString("-", title.length())
2762: + "\n\n";
2763: break;
2764:
2765: case Heading.HEADING_LARGE:
2766: res = title.toUpperCase();
2767: m_closeTag = "\n"
2768: + TextUtil.repeatString("=", title.length())
2769: + "\n\n";
2770: break;
2771: }
2772:
2773: return res;
2774: }
2775:
2776: /**
2777: * @param bullet A character detailing which kind of a list
2778: * we are dealing with here. Options are '#' and '*'.
2779: */
2780: // FIXME: Should really start a different kind of list depending
2781: // on the bullet type
2782: public String openList(char bullet) {
2783: return "\n";
2784: }
2785:
2786: public String openListItem() {
2787: return "- ";
2788: }
2789:
2790: public String closeListItem() {
2791: return "\n";
2792: }
2793:
2794: /**
2795: * @param bullet A character detailing which kind of a list
2796: * we are dealing with here. Options are '#' and '*'.
2797: */
2798: public String closeList(char bullet) {
2799: return "\n\n";
2800: }
2801:
2802: public String openTable() {
2803: return "\n";
2804: }
2805:
2806: public String closeTable() {
2807: return "\n";
2808: }
2809:
2810: public String openTableRow() {
2811: return "";
2812: }
2813:
2814: public String closeTableRow() {
2815: return "\n";
2816: }
2817:
2818: public String openTableItem() {
2819: return "\t";
2820: }
2821:
2822: public String closeTableItem() {
2823: return "";
2824: }
2825:
2826: public String openTableHeading() {
2827: return "\t";
2828: }
2829:
2830: public String closeTableHeading() {
2831: return "";
2832: }
2833:
2834: public String openPreformatted(boolean isBlock) {
2835: return "";
2836: }
2837:
2838: public String closePreformatted() {
2839: return "\n";
2840: }
2841:
2842: /**
2843: * If outlink images are turned on, returns a link to the outward
2844: * linking image.
2845: */
2846: public String outlinkImage() {
2847: return "";
2848: }
2849:
2850: /**
2851: * @param clear If true, then flushes all thingies.
2852: */
2853: public String lineBreak(boolean clear) {
2854: return "\n";
2855: }
2856:
2857: } // TextRenderer
2858:
2859: /**
2860: * This class is used to store the headings in a manner which
2861: * allow the building of a Table Of Contents.
2862: */
2863: public class Heading {
2864: public static final int HEADING_SMALL = 1;
2865: public static final int HEADING_MEDIUM = 2;
2866: public static final int HEADING_LARGE = 3;
2867:
2868: public int m_level;
2869: public String m_titleText;
2870: public String m_titleAnchor;
2871: public String m_titleSection;
2872: }
2873: }
|