You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC
svn commit: r627255 [4/41] - in
/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/
com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/
com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki/...
Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TranslatorReader.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TranslatorReader.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TranslatorReader.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/TranslatorReader.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,3152 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.ecyrd.jspwiki;
+
+import java.io.*;
+import java.text.MessageFormat;
+import java.util.*;
+
+import org.apache.log4j.Logger;
+import org.apache.oro.text.*;
+import org.apache.oro.text.regex.*;
+
+import com.ecyrd.jspwiki.i18n.InternationalizationManager;
+import com.ecyrd.jspwiki.parser.Heading;
+import com.ecyrd.jspwiki.parser.HeadingListener;
+import com.ecyrd.jspwiki.plugin.PluginManager;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+import com.ecyrd.jspwiki.action.*;
+import com.ecyrd.jspwiki.attachment.AttachmentManager;
+import com.ecyrd.jspwiki.attachment.Attachment;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.auth.acl.Acl;
+import com.ecyrd.jspwiki.auth.WikiSecurityException;
+
+/**
+ * Handles conversion from Wiki format into fully featured HTML.
+ * This is where all the magic happens. It is CRITICAL that this
+ * class is tested, or all Wikis might die horribly.
+ * <P>
+ * The output of the HTML has not yet been validated against
+ * the HTML DTD. However, it is very simple.
+ * <p>
+ * This class is officially deprecated in 2.3, and will be replaced
+ * with a dummy class later on. Please see MarkupParser.
+ * @author Janne Jalkanen
+ * @deprecated
+ */
+//FIXME2.6: Make use JSPWikiMarkupParser
+//FIXME3.0: Remove
+public class TranslatorReader extends Reader
+{
+ public static final int READ = 0;
+ public static final int EDIT = 1;
+ private static final int EMPTY = 2; // Empty message
+ private static final int LOCAL = 3;
+ private static final int LOCALREF = 4;
+ private static final int IMAGE = 5;
+ private static final int EXTERNAL = 6;
+ private static final int INTERWIKI = 7;
+ private static final int IMAGELINK = 8;
+ private static final int IMAGEWIKILINK = 9;
+ public static final int ATTACHMENT = 10;
+ // private static final int ATTACHMENTIMAGE = 11;
+
+ /** Lists all punctuation characters allowed in WikiMarkup. These
+ will not be cleaned away. */
+
+ private static final String PUNCTUATION_CHARS_ALLOWED = "._";
+
+ /** Allow this many characters to be pushed back in the stream. In effect,
+ this limits the size of a single heading line. */
+ private static final int PUSHBACK_BUFFER_SIZE = 10*1024;
+ private PushbackReader m_in;
+
+ private StringReader m_data = new StringReader("");
+
+ private static Logger log = Logger.getLogger( TranslatorReader.class );
+
+ //private boolean m_iscode = false;
+ private boolean m_isbold = false;
+ private boolean m_isitalic = false;
+ private boolean m_isTypedText = false;
+ private boolean m_istable = false;
+ private boolean m_isPre = false;
+ private boolean m_isEscaping = false;
+ private boolean m_isdefinition = false;
+
+ /** Contains style information, in multiple forms. */
+ private Stack<Boolean> m_styleStack = new Stack<Boolean>();
+
+ // general list handling
+ private int m_genlistlevel = 0;
+ private StringBuffer m_genlistBulletBuffer = new StringBuffer(); // stores the # and * pattern
+ private boolean m_allowPHPWikiStyleLists = true;
+
+
+ private boolean m_isOpenParagraph = false;
+
+ /** Tag that gets closed at EOL. */
+ private String m_closeTag = null;
+
+ private WikiEngine m_engine;
+ private WikiContext m_context;
+
+ /** Optionally stores internal wikilinks */
+ private ArrayList<StringTransmutator> m_localLinkMutatorChain = new ArrayList<StringTransmutator>();
+ private ArrayList<StringTransmutator> m_externalLinkMutatorChain = new ArrayList<StringTransmutator>();
+ private ArrayList<StringTransmutator> m_attachmentLinkMutatorChain = new ArrayList<StringTransmutator>();
+ private ArrayList<HeadingListener> m_headingListenerChain = new ArrayList<HeadingListener>();
+
+ /** Keeps image regexp Patterns */
+ private ArrayList m_inlineImagePatterns;
+
+ private PatternMatcher m_inlineMatcher = new Perl5Matcher();
+
+ private ArrayList<StringTransmutator> m_linkMutators = new ArrayList<StringTransmutator>();
+
+ /**
+ * This property defines the inline image pattern. It's current value
+ * is jspwiki.translatorReader.inlinePattern
+ */
+ public static final String PROP_INLINEIMAGEPTRN = "jspwiki.translatorReader.inlinePattern";
+
+ /** If true, consider CamelCase hyperlinks as well. */
+ public static final String PROP_CAMELCASELINKS = "jspwiki.translatorReader.camelCaseLinks";
+
+ /** If true, all hyperlinks are translated as well, regardless whether they
+ are surrounded by brackets. */
+ public static final String PROP_PLAINURIS = "jspwiki.translatorReader.plainUris";
+
+ /** If true, all outward links (external links) have a small link image appended. */
+ public static final String PROP_USEOUTLINKIMAGE = "jspwiki.translatorReader.useOutlinkImage";
+
+ /** If set to "true", allows using raw HTML within Wiki text. Be warned,
+ this is a VERY dangerous option to set - never turn this on in a publicly
+ allowable Wiki, unless you are absolutely certain of what you're doing. */
+ public static final String PROP_ALLOWHTML = "jspwiki.translatorReader.allowHTML";
+
+ /** If set to "true", all external links are tagged with 'rel="nofollow"' */
+ public static final String PROP_USERELNOFOLLOW = "jspwiki.translatorReader.useRelNofollow";
+
+ /** If set to "true", enables plugins during parsing */
+ public static final String PROP_RUNPLUGINS = "jspwiki.translatorReader.runPlugins";
+
+ /** If true, then considers CamelCase links as well. */
+ private boolean m_camelCaseLinks = false;
+
+ /** If true, consider URIs that have no brackets as well. */
+ // FIXME: Currently reserved, but not used.
+ private boolean m_plainUris = false;
+
+ /** If true, all outward links use a small link image. */
+ private boolean m_useOutlinkImage = true;
+
+ /** If true, allows raw HTML. */
+ private boolean m_allowHTML = false;
+
+ /** If true, executes plugins; otherwise ignores them. */
+ private boolean m_enablePlugins = true;
+
+ private boolean m_useRelNofollow = false;
+
+ private boolean m_inlineImages = true;
+
+ private PatternMatcher m_matcher = new Perl5Matcher();
+ private PatternCompiler m_compiler = new Perl5Compiler();
+ private Pattern m_camelCasePtrn;
+
+ private TextRenderer m_renderer;
+
+ /**
+ * The default inlining pattern. Currently "*.png"
+ */
+ public static final String DEFAULT_INLINEPATTERN = "*.png";
+
+ /**
+ * These characters constitute word separators when trying
+ * to find CamelCase links.
+ */
+ private static final String WORD_SEPARATORS = ",.|;+=&()";
+
+ protected static final int BOLD = 0;
+ protected static final int ITALIC = 1;
+ protected static final int TYPED = 2;
+
+ /**
+ * This list contains all IANA registered URI protocol
+ * types as of September 2004 + a few well-known extra types.
+ *
+ * JSPWiki recognises all of them as external links.
+ */
+ static final String[] c_externalLinks = {
+ "http:", "ftp:", "https:", "mailto:",
+ "news:", "file:", "rtsp:", "mms:", "ldap:",
+ "gopher:", "nntp:", "telnet:", "wais:",
+ "prospero:", "z39.50s", "z39.50r", "vemmi:",
+ "imap:", "nfs:", "acap:", "tip:", "pop:",
+ "dav:", "opaquelocktoken:", "sip:", "sips:",
+ "tel:", "fax:", "modem:", "soap.beep:", "soap.beeps",
+ "xmlrpc.beep", "xmlrpc.beeps", "urn:", "go:",
+ "h323:", "ipp:", "tftp:", "mupdate:", "pres:",
+ "im:", "mtqp", "smb:" };
+
+
+ /**
+ * Creates a TranslatorReader using the default HTML renderer.
+ */
+ public TranslatorReader( WikiContext context, Reader in )
+ {
+ initialize( context, in, new HTMLRenderer() );
+ }
+
+ public TranslatorReader( WikiContext context, Reader in, TextRenderer renderer )
+ {
+ initialize( context, in, renderer );
+ }
+
+ /**
+ * Replaces the current input character stream with a new one.
+ * @param in New source for input. If null, this method does nothing.
+ * @return the old stream
+ */
+ public Reader setInputReader( Reader in )
+ {
+ Reader old = m_in;
+
+ if( in != null )
+ {
+ m_in = new PushbackReader( new BufferedReader( in ),
+ PUSHBACK_BUFFER_SIZE );
+ }
+
+ return old;
+ }
+
+ /**
+ * @param m_engine The WikiEngine this reader is attached to. Is
+ * used to figure out of a page exits.
+ */
+
+ // FIXME: TranslatorReaders should be pooled for better performance.
+ private void initialize( WikiContext context,
+ Reader in,
+ TextRenderer renderer )
+ {
+ PatternCompiler compiler = new GlobCompiler();
+ ArrayList compiledpatterns = new ArrayList();
+
+ m_engine = context.getEngine();
+ m_context = context;
+
+ m_renderer = renderer;
+
+ setInputReader( in );
+
+ Collection ptrns = getImagePatterns( m_engine );
+
+ //
+ // Make them into Regexp Patterns. Unknown patterns
+ // are ignored.
+ //
+ for( Iterator i = ptrns.iterator(); i.hasNext(); )
+ {
+ try
+ {
+ compiledpatterns.add( compiler.compile( (String)i.next() ) );
+ }
+ catch( MalformedPatternException e )
+ {
+ log.error("Malformed pattern in properties: ", e );
+ }
+ }
+
+ m_inlineImagePatterns = compiledpatterns;
+
+ try
+ {
+ m_camelCasePtrn = m_compiler.compile( "^([[:^alnum:]]*)([[:upper:]]+[[:lower:]]+[[:upper:]]+[[:alnum:]]*)[[:^alnum:]]*$" );
+ }
+ catch( MalformedPatternException e )
+ {
+ log.fatal("Internal error: Someone put in a faulty pattern.",e);
+ throw new InternalWikiException("Faulty camelcasepattern in TranslatorReader");
+ }
+
+ //
+ // Set the properties.
+ //
+ Properties props = m_engine.getWikiProperties();
+
+ String cclinks = (String)m_context.getPage().getAttribute( PROP_CAMELCASELINKS );
+
+ if( cclinks != null )
+ {
+ m_camelCaseLinks = TextUtil.isPositive( cclinks );
+ }
+ else
+ {
+ m_camelCaseLinks = TextUtil.getBooleanProperty( props,
+ PROP_CAMELCASELINKS,
+ m_camelCaseLinks );
+ }
+
+ m_plainUris = TextUtil.getBooleanProperty( props,
+ PROP_PLAINURIS,
+ m_plainUris );
+ m_useOutlinkImage = TextUtil.getBooleanProperty( props,
+ PROP_USEOUTLINKIMAGE,
+ m_useOutlinkImage );
+ m_allowHTML = TextUtil.getBooleanProperty( props,
+ PROP_ALLOWHTML,
+ m_allowHTML );
+
+ m_useRelNofollow = TextUtil.getBooleanProperty( props,
+ PROP_USERELNOFOLLOW,
+ m_useRelNofollow );
+
+ String runplugins = m_engine.getVariable( m_context, PROP_RUNPLUGINS );
+ if( runplugins != null ) enablePlugins( TextUtil.isPositive(runplugins));
+
+ if( m_engine.getUserManager().getUserDatabase() == null || m_engine.getAuthorizationManager() == null )
+ {
+ disableAccessRules();
+ }
+
+ m_context.getPage().setHasMetadata();
+ }
+
+ /**
+ * Sets the currently used renderer. This method is protected because
+ * we only want to use it internally for now. The renderer interface
+ * is not yet set to stone, so it's not expected that third parties
+ * would use this.
+ */
+ protected void setRenderer( TextRenderer renderer )
+ {
+ m_renderer = renderer;
+ }
+
+ /**
+ * Adds a hook for processing link texts. This hook is called
+ * when the link text is written into the output stream, and
+ * you may use it to modify the text. It does not affect the
+ * actual link, only the user-visible text.
+ *
+ * @param mutator The hook to call. Null is safe.
+ */
+ public void addLinkTransmutator( StringTransmutator mutator )
+ {
+ if( mutator != null )
+ {
+ m_linkMutators.add( mutator );
+ }
+ }
+
+ /**
+ * Adds a hook for processing local links. The engine
+ * transforms both non-existing and existing page links.
+ *
+ * @param mutator The hook to call. Null is safe.
+ */
+ public void addLocalLinkHook( StringTransmutator mutator )
+ {
+ if( mutator != null )
+ {
+ m_localLinkMutatorChain.add( mutator );
+ }
+ }
+
+ /**
+ * Adds a hook for processing external links. This includes
+ * all http:// ftp://, etc. links, including inlined images.
+ *
+ * @param mutator The hook to call. Null is safe.
+ */
+ public void addExternalLinkHook( StringTransmutator mutator )
+ {
+ if( mutator != null )
+ {
+ m_externalLinkMutatorChain.add( mutator );
+ }
+ }
+
+ /**
+ * Adds a hook for processing attachment links.
+ *
+ * @param mutator The hook to call. Null is safe.
+ */
+ public void addAttachmentLinkHook( StringTransmutator mutator )
+ {
+ if( mutator != null )
+ {
+ m_attachmentLinkMutatorChain.add( mutator );
+ }
+ }
+
+ public void addHeadingListener( HeadingListener listener )
+ {
+ if( listener != null )
+ {
+ m_headingListenerChain.add( listener );
+ }
+ }
+
+ private boolean m_parseAccessRules = true;
+
+ public void disableAccessRules()
+ {
+ m_parseAccessRules = false;
+ }
+
+ /**
+ * Can be used to turn on plugin execution on a translator-reader basis
+ */
+ public void enablePlugins( boolean toggle )
+ {
+ m_enablePlugins = toggle;
+ }
+
+ /**
+ * Use this to turn on or off image inlining.
+ * @param toggle If true, images are inlined (as per set in jspwiki.properties)
+ * If false, then images won't be inlined; instead, they will be
+ * treated as standard hyperlinks.
+ * @since 2.2.9
+ */
+ public void enableImageInlining( boolean toggle )
+ {
+ m_inlineImages = toggle;
+ }
+
+ /**
+ * Figure out which image suffixes should be inlined.
+ * @return Collection of Strings with patterns.
+ */
+
+ protected static Collection getImagePatterns( WikiEngine engine )
+ {
+ Properties props = engine.getWikiProperties();
+ ArrayList<String> ptrnlist = new ArrayList<String>();
+
+ for( Enumeration e = props.propertyNames(); e.hasMoreElements(); )
+ {
+ String name = (String) e.nextElement();
+
+ if( name.startsWith( PROP_INLINEIMAGEPTRN ) )
+ {
+ String ptrn = props.getProperty( name );
+
+ ptrnlist.add( ptrn );
+ }
+ }
+
+ if( ptrnlist.size() == 0 )
+ {
+ ptrnlist.add( DEFAULT_INLINEPATTERN );
+ }
+
+ return ptrnlist;
+ }
+
+ /**
+ * Returns link name, if it exists; otherwise it returns null.
+ */
+ private String linkExists( String page )
+ {
+ try
+ {
+ if( page == null || page.length() == 0 ) return null;
+
+ return m_engine.getFinalPageName( page );
+ }
+ catch( ProviderException e )
+ {
+ log.warn("TranslatorReader got a faulty page name!",e);
+
+ return page; // FIXME: What would be the correct way to go back?
+ }
+ }
+
+ /**
+ * Calls a transmutator chain.
+ *
+ * @param list Chain to call
+ * @param text Text that should be passed to the mutate() method
+ * of each of the mutators in the chain.
+ * @return The result of the mutation.
+ */
+
+ private String callMutatorChain( Collection list, String text )
+ {
+ if( list == null || list.size() == 0 )
+ {
+ return text;
+ }
+
+ for( Iterator i = list.iterator(); i.hasNext(); )
+ {
+ StringTransmutator m = (StringTransmutator) i.next();
+
+ text = m.mutate( m_context, text );
+ }
+
+ return text;
+ }
+
+ private void callHeadingListenerChain( Heading param )
+ {
+ List list = m_headingListenerChain;
+
+ for( Iterator i = list.iterator(); i.hasNext(); )
+ {
+ HeadingListener h = (HeadingListener) i.next();
+
+ h.headingAdded( m_context, param );
+ }
+ }
+
+ /**
+ * Write a HTMLized link depending on its type.
+ * The link mutator chain is processed.
+ *
+ * @param type Type of the link.
+ * @param link The actual link.
+ * @param text The user-visible text for the link.
+ */
+ public String makeLink( int type, String link, String text )
+ {
+ if( text == null ) text = link;
+
+ text = callMutatorChain( m_linkMutators, text );
+
+ return m_renderer.makeLink( type, link, text );
+ }
+
+ /**
+ * Just like makeLink, but also adds the section reference (#sect...)
+ */
+ private String makeLink( int type, String link, String text, String sectref )
+ {
+ if( text == null ) text = link;
+
+ text = callMutatorChain( m_linkMutators, text );
+
+ return m_renderer.makeLink( type, link, text, sectref );
+ }
+
+
+ /**
+ * Cleans a Wiki name.
+ * <P>
+ * [ This is a link ] -> ThisIsALink
+ *
+ * @param link Link to be cleared. Null is safe, and causes this to return null.
+ * @return A cleaned link.
+ *
+ * @since 2.0
+ */
+ public static String cleanLink( String link )
+ {
+ StringBuffer clean = new StringBuffer();
+
+ if( link == null ) return null;
+
+ //
+ // Compress away all whitespace and capitalize
+ // all words in between.
+ //
+
+ StringTokenizer st = new StringTokenizer( link, " -" );
+
+ while( st.hasMoreTokens() )
+ {
+ StringBuffer component = new StringBuffer(st.nextToken());
+
+ component.setCharAt(0, Character.toUpperCase( component.charAt(0) ) );
+
+ //
+ // We must do this, because otherwise compiling on JDK 1.4 causes
+ // a downwards incompatibility to JDK 1.3.
+ //
+ clean.append( component.toString() );
+ }
+
+ //
+ // Remove non-alphanumeric characters that should not
+ // be put inside WikiNames. Note that all valid
+ // Unicode letters are considered okay for WikiNames.
+ // It is the problem of the WikiPageProvider to take
+ // care of actually storing that information.
+ //
+
+ for( int i = 0; i < clean.length(); i++ )
+ {
+ char ch = clean.charAt(i);
+
+ if( !(Character.isLetterOrDigit(ch) ||
+ PUNCTUATION_CHARS_ALLOWED.indexOf(ch) != -1 ))
+ {
+ clean.deleteCharAt(i);
+ --i; // We just shortened this buffer.
+ }
+ }
+
+ return clean.toString();
+ }
+
+ /**
+ * Figures out if a link is an off-site link. This recognizes
+ * the most common protocols by checking how it starts.
+ */
+
+ // FIXME: Should really put the external link types to a sorted set,
+ // then searching for them would be faster.
+ private boolean isExternalLink( String link )
+ {
+ for( int i = 0; i < c_externalLinks.length; i++ )
+ {
+ if( link.startsWith( c_externalLinks[i] ) ) return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true, if the link in question is an access
+ * rule.
+ */
+ private static boolean isAccessRule( String link )
+ {
+ return link.startsWith("{ALLOW") || link.startsWith("{DENY");
+ }
+
+ /**
+ * Matches the given link to the list of image name patterns
+ * to determine whether it should be treated as an inline image
+ * or not.
+ */
+ private boolean isImageLink( String link )
+ {
+ if( m_inlineImages )
+ {
+ for( Iterator i = m_inlineImagePatterns.iterator(); i.hasNext(); )
+ {
+ if( m_inlineMatcher.matches( link, (Pattern) i.next() ) )
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean isMetadata( String link )
+ {
+ return link.startsWith("{SET");
+ }
+
+ /**
+ * Returns true, if the argument contains a number, otherwise false.
+ * In a quick test this is roughly the same speed as Integer.parseInt()
+ * if the argument is a number, and roughly ten times the speed, if
+ * the argument is NOT a number.
+ */
+
+ private boolean isNumber( String s )
+ {
+ if( s == null ) return false;
+
+ if( s.length() > 1 && s.charAt(0) == '-' )
+ s = s.substring(1);
+
+ for( int i = 0; i < s.length(); i++ )
+ {
+ if( !Character.isDigit(s.charAt(i)) )
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks for the existence of a traditional style CamelCase link.
+ * <P>
+ * We separate all white-space -separated words, and feed it to this
+ * routine to find if there are any possible camelcase links.
+ * For example, if "word" is "__HyperLink__" we return "HyperLink".
+ *
+ * @param word A phrase to search in.
+ * @return The match within the phrase. Returns null, if no CamelCase
+ * hyperlink exists within this phrase.
+ */
+ private String checkForCamelCaseLink( String word )
+ {
+ PatternMatcherInput input;
+
+ input = new PatternMatcherInput( word );
+
+ if( m_matcher.contains( input, m_camelCasePtrn ) )
+ {
+ MatchResult res = m_matcher.getMatch();
+
+ String link = res.group(2);
+
+ if( res.group(1) != null )
+ {
+ if( res.group(1).endsWith("~") ||
+ res.group(1).indexOf('[') != -1 )
+ {
+ // Delete the (~) from beginning.
+ // We'll make '~' the generic kill-processing-character from
+ // now on.
+ return null;
+ }
+ }
+
+ return link;
+ } // if match
+
+ return null;
+ }
+
+ /**
+ * When given a link to a WikiName, we just return
+ * a proper HTML link for it. The local link mutator
+ * chain is also called.
+ */
+ private String makeCamelCaseLink( String wikiname )
+ {
+ String matchedLink;
+ String link;
+
+ callMutatorChain( m_localLinkMutatorChain, wikiname );
+
+ if( (matchedLink = linkExists( wikiname )) != null )
+ {
+ link = makeLink( READ, matchedLink, wikiname );
+ }
+ else
+ {
+ link = makeLink( EDIT, wikiname, wikiname );
+ }
+
+ return link;
+ }
+
+ private String makeDirectURILink( String url )
+ {
+ String last = "";
+ String result;
+
+ if( url.endsWith(",") || url.endsWith(".") )
+ {
+ last = url.substring( url.length()-1 );
+ url = url.substring( 0, url.length()-1 );
+ }
+
+ callMutatorChain( m_externalLinkMutatorChain, url );
+
+ if( isImageLink( url ) )
+ {
+ result = handleImageLink( url, url, false );
+ }
+ else
+ {
+ result = makeLink( EXTERNAL, url, url ) + m_renderer.outlinkImage();
+ }
+
+ result += last;
+
+ return result;
+ }
+
+ /**
+ * Image links are handled differently:
+ * 1. If the text is a WikiName of an existing page,
+ * it gets linked.
+ * 2. If the text is an external link, then it is inlined.
+ * 3. Otherwise it becomes an ALT text.
+ *
+ * @param reallink The link to the image.
+ * @param link Link text portion, may be a link to somewhere else.
+ * @param hasLinkText If true, then the defined link had a link text available.
+ * This means that the link text may be a link to a wiki page,
+ * or an external resource.
+ */
+
+ private String handleImageLink( String reallink, String link, boolean hasLinkText )
+ {
+ String possiblePage = cleanLink( link );
+ String res = "";
+
+ if( isExternalLink( link ) && hasLinkText )
+ {
+ res = makeLink( IMAGELINK, reallink, link );
+ }
+ else if( ( linkExists( possiblePage ) ) != null &&
+ hasLinkText )
+ {
+ // System.out.println("Orig="+link+", Matched: "+matchedLink);
+ callMutatorChain( m_localLinkMutatorChain, possiblePage );
+
+ res = makeLink( IMAGEWIKILINK, reallink, link );
+ }
+ else
+ {
+ res = makeLink( IMAGE, reallink, link );
+ }
+
+ return res;
+ }
+
+ private String handleAccessRule( String ruleLine )
+ {
+ if( !m_parseAccessRules ) return "";
+ Acl acl;
+ WikiPage page = m_context.getPage();
+ // UserDatabase db = m_context.getEngine().getUserDatabase();
+
+ if( ruleLine.startsWith( "{" ) )
+ ruleLine = ruleLine.substring( 1 );
+ if( ruleLine.endsWith( "}" ) )
+ ruleLine = ruleLine.substring( 0, ruleLine.length() - 1 );
+
+ log.debug("page="+page.getName()+", ACL = "+ruleLine);
+
+ try
+ {
+ acl = m_engine.getAclManager().parseAcl( page, ruleLine );
+
+ page.setAcl( acl );
+
+ log.debug( acl.toString() );
+ }
+ catch( WikiSecurityException wse )
+ {
+ return m_renderer.makeError( wse.getMessage() );
+ }
+
+ return "";
+ }
+
+ /**
+ * Handles metadata setting [{SET foo=bar}]
+ */
+ private String handleMetadata( String link )
+ {
+ try
+ {
+ String args = link.substring( link.indexOf(' '), link.length()-1 );
+
+ String name = args.substring( 0, args.indexOf('=') );
+ String val = args.substring( args.indexOf('=')+1, args.length() );
+
+ name = name.trim();
+ val = val.trim();
+
+ if( val.startsWith("'") ) val = val.substring( 1 );
+ if( val.endsWith("'") ) val = val.substring( 0, val.length()-1 );
+
+ // log.debug("SET name='"+name+"', value='"+val+"'.");
+
+ if( name.length() > 0 && val.length() > 0 )
+ {
+ val = m_engine.getVariableManager().expandVariables( m_context,
+ val );
+
+ m_context.getPage().setAttribute( name, val );
+ }
+ }
+ catch( Exception e )
+ {
+ ResourceBundle rb = m_context.getBundle(InternationalizationManager.CORE_BUNDLE);
+ Object[] args = { link };
+ m_renderer.makeError( MessageFormat.format( rb.getString( "markupparser.error.invalidset" ), args ) );
+ }
+
+ return "";
+ }
+
+ /**
+ * Gobbles up all hyperlinks that are encased in square brackets.
+ */
+ private String handleHyperlinks( String link )
+ {
+ StringBuffer sb = new StringBuffer();
+ String reallink;
+ int cutpoint;
+
+ if( isAccessRule( link ) )
+ {
+ return handleAccessRule( link );
+ }
+
+ if( isMetadata( link ) )
+ {
+ return handleMetadata( link );
+ }
+
+ if( PluginManager.isPluginLink( link ) )
+ {
+ String included = "";
+ try
+ {
+ if( m_enablePlugins )
+ {
+ included = m_engine.getPluginManager().execute( m_context, link );
+ }
+ }
+ catch( PluginException e )
+ {
+ log.info( "Failed to insert plugin", e );
+ log.info( "Root cause:",e.getRootThrowable() );
+
+ ResourceBundle rb = m_context.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+ Object[] args = { e.getMessage() };
+
+ included = m_renderer.makeError( MessageFormat.format( rb.getString( "plugin.error.insertionfailed" ), args ) );
+ }
+
+ sb.append( included );
+
+ return sb.toString();
+ }
+
+ link = TextUtil.replaceEntities( link );
+
+ if( (cutpoint = link.indexOf('|')) != -1 )
+ {
+ reallink = link.substring( cutpoint+1 ).trim();
+ link = link.substring( 0, cutpoint );
+ }
+ else
+ {
+ reallink = link.trim();
+ }
+
+ int interwikipoint = -1;
+
+ //
+ // Yes, we now have the components separated.
+ // link = the text the link should have
+ // reallink = the url or page name.
+ //
+ // In many cases these are the same. [link|reallink].
+ //
+ if( VariableManager.isVariableLink( link ) )
+ {
+ String value;
+
+ try
+ {
+ value = m_engine.getVariableManager().parseAndGetValue( m_context, link );
+ }
+ catch( NoSuchVariableException e )
+ {
+ value = m_renderer.makeError(e.getMessage());
+ }
+ catch( IllegalArgumentException e )
+ {
+ value = m_renderer.makeError(e.getMessage());
+ }
+
+ sb.append( value );
+ }
+ else if( isExternalLink( reallink ) )
+ {
+ // It's an external link, out of this Wiki
+
+ callMutatorChain( m_externalLinkMutatorChain, reallink );
+
+ if( isImageLink( reallink ) )
+ {
+ sb.append( handleImageLink( reallink, link, (cutpoint != -1) ) );
+ }
+ else
+ {
+ sb.append( makeLink( EXTERNAL, reallink, link ) );
+ sb.append( m_renderer.outlinkImage() );
+ }
+ }
+ else if( (interwikipoint = reallink.indexOf(":")) != -1 )
+ {
+ // It's an interwiki link
+ // InterWiki links also get added to external link chain
+ // after the links have been resolved.
+
+ // FIXME: There is an interesting issue here: We probably should
+ // URLEncode the wikiPage, but we can't since some of the
+ // Wikis use slashes (/), which won't survive URLEncoding.
+ // Besides, we don't know which character set the other Wiki
+ // is using, so you'll have to write the entire name as it appears
+ // in the URL. Bugger.
+
+ String extWiki = reallink.substring( 0, interwikipoint );
+ String wikiPage = reallink.substring( interwikipoint+1 );
+
+ String urlReference = m_engine.getInterWikiURL( extWiki );
+
+ if( urlReference != null )
+ {
+ urlReference = TextUtil.replaceString( urlReference, "%s", wikiPage );
+ callMutatorChain( m_externalLinkMutatorChain, urlReference );
+
+ sb.append( makeLink( INTERWIKI, urlReference, link ) );
+
+ if( isExternalLink(urlReference) )
+ {
+ sb.append( m_renderer.outlinkImage() );
+ }
+ }
+ else
+ {
+ ResourceBundle rb = m_context.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+ Object[] args = { extWiki };
+
+ sb.append( link+" "+m_renderer.makeError( MessageFormat.format( rb.getString( "plugin.error.nointerwikiref" ), args ) ) );
+ }
+ }
+ else if( reallink.startsWith("#") )
+ {
+ // It defines a local footnote
+ sb.append( makeLink( LOCAL, reallink, link ) );
+ }
+ else if( isNumber( reallink ) )
+ {
+ // It defines a reference to a local footnote
+ sb.append( makeLink( LOCALREF, reallink, link ) );
+ }
+ else
+ {
+ int hashMark = -1;
+
+ //
+ // Internal wiki link, but is it an attachment link?
+ //
+ String attachment = findAttachment( reallink );
+ if( attachment != null )
+ {
+ callMutatorChain( m_attachmentLinkMutatorChain, attachment );
+
+ if( isImageLink( reallink ) )
+ {
+ attachment = m_context.getContext().getURL( AttachActionBean.class, attachment );
+ sb.append( handleImageLink( attachment, link, (cutpoint != -1) ) );
+ }
+ else
+ {
+ sb.append( makeLink( ATTACHMENT, attachment, link ) );
+ }
+ }
+ else if( (hashMark = reallink.indexOf('#')) != -1 )
+ {
+ // It's an internal Wiki link, but to a named section
+
+ String namedSection = reallink.substring( hashMark+1 );
+ reallink = reallink.substring( 0, hashMark );
+
+ reallink = cleanLink( reallink );
+
+ callMutatorChain( m_localLinkMutatorChain, reallink );
+
+ String matchedLink;
+ if( (matchedLink = linkExists( reallink )) != null )
+ {
+ String sectref = "section-"+m_engine.encodeName(matchedLink)+"-"+namedSection;
+ sectref = sectref.replace('%', '_');
+ sb.append( makeLink( READ, matchedLink, link, sectref ) );
+ }
+ else
+ {
+ sb.append( makeLink( EDIT, reallink, link ) );
+ }
+ }
+ else
+ {
+ // It's an internal Wiki link
+ reallink = cleanLink( reallink );
+
+ callMutatorChain( m_localLinkMutatorChain, reallink );
+
+ String matchedLink = linkExists( reallink );
+
+ if( matchedLink != null )
+ {
+ sb.append( makeLink( READ, matchedLink, link ) );
+ }
+ else
+ {
+ sb.append( makeLink( EDIT, reallink, link ) );
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ private String findAttachment( String link )
+ {
+ AttachmentManager mgr = m_engine.getAttachmentManager();
+ Attachment att = null;
+
+ try
+ {
+ att = mgr.getAttachmentInfo( m_context, link );
+ }
+ catch( ProviderException e )
+ {
+ log.warn("Finding attachments failed: ",e);
+ return null;
+ }
+
+ if( att != null )
+ {
+ return att.getName();
+ }
+ else if( link.indexOf('/') != -1 )
+ {
+ return link;
+ }
+
+ return null;
+ }
+
+ /**
+ * Closes all annoying lists and things that the user might've
+ * left open.
+ */
+ private String closeAll()
+ {
+ StringBuffer buf = new StringBuffer();
+
+ if( m_isbold )
+ {
+ buf.append(m_renderer.closeTextEffect(BOLD));
+ m_isbold = false;
+ }
+
+ if( m_isitalic )
+ {
+ buf.append(m_renderer.closeTextEffect(ITALIC));
+ m_isitalic = false;
+ }
+
+ if( m_isTypedText )
+ {
+ buf.append(m_renderer.closeTextEffect(TYPED));
+ m_isTypedText = false;
+ }
+
+ /*
+ for( ; m_listlevel > 0; m_listlevel-- )
+ {
+ buf.append( "</ul>\n" );
+ }
+
+ for( ; m_numlistlevel > 0; m_numlistlevel-- )
+ {
+ buf.append( "</ol>\n" );
+ }
+ */
+ // cleanup OL and UL lists
+ buf.append(unwindGeneralList());
+
+ if( m_isPre )
+ {
+ buf.append(m_renderer.closePreformatted());
+ m_isEscaping = false;
+ m_isPre = false;
+ }
+
+ if( m_istable )
+ {
+ buf.append( m_renderer.closeTable() );
+ m_istable = false;
+ }
+
+ if( m_isOpenParagraph )
+ {
+ buf.append( m_renderer.closeParagraph() );
+ m_isOpenParagraph = false;
+ }
+
+ return buf.toString();
+ }
+
+
+ private int nextToken()
+ throws IOException
+ {
+ if( m_in == null ) return -1;
+ return m_in.read();
+ }
+
+ /**
+ * Push back any character to the current input. Does not
+ * push back a read EOF, though.
+ */
+ private void pushBack( int c )
+ throws IOException
+ {
+ if( c != -1 && m_in != null )
+ {
+ m_in.unread( c );
+ }
+ }
+
+ /**
+ * Pushes back any string that has been read. It will obviously
+ * be pushed back in a reverse order.
+ *
+ * @since 2.1.77
+ */
+ private void pushBack( String s )
+ throws IOException
+ {
+ for( int i = s.length()-1; i >= 0; i-- )
+ {
+ pushBack( s.charAt(i) );
+ }
+ }
+
+ private String handleBackslash()
+ throws IOException
+ {
+ int ch = nextToken();
+
+ if( ch == '\\' )
+ {
+ int ch2 = nextToken();
+
+ if( ch2 == '\\' )
+ {
+ return m_renderer.lineBreak(true);
+ }
+
+ pushBack( ch2 );
+
+ return m_renderer.lineBreak(false);
+ }
+
+ pushBack( ch );
+
+ return "\\";
+ }
+
+ private String handleUnderscore()
+ throws IOException
+ {
+ int ch = nextToken();
+ String res = "_";
+
+ if( ch == '_' )
+ {
+ res = m_isbold ? m_renderer.closeTextEffect(BOLD) : m_renderer.openTextEffect(BOLD);
+ m_isbold = !m_isbold;
+ }
+ else
+ {
+ pushBack( ch );
+ }
+
+ return res;
+ }
+
+ /**
+ * For example: italics.
+ */
+ private String handleApostrophe()
+ throws IOException
+ {
+ int ch = nextToken();
+ String res = "'";
+
+ if( ch == '\'' )
+ {
+ res = m_isitalic ? m_renderer.closeTextEffect(ITALIC) : m_renderer.openTextEffect(ITALIC);
+ m_isitalic = !m_isitalic;
+ }
+ else
+ {
+ pushBack( ch );
+ }
+
+ return res;
+ }
+
+ private String handleOpenbrace( boolean isBlock )
+ throws IOException
+ {
+ int ch = nextToken();
+ String res = "{";
+
+ if( ch == '{' )
+ {
+ int ch2 = nextToken();
+
+ if( ch2 == '{' )
+ {
+ res = startBlockLevel()+m_renderer.openPreformatted( isBlock );
+ m_isPre = true;
+ m_isEscaping = true;
+ }
+ else
+ {
+ pushBack( ch2 );
+
+ res = m_renderer.openTextEffect(TYPED);
+ m_isTypedText = true;
+ }
+ }
+ else
+ {
+ pushBack( ch );
+ }
+
+ return res;
+ }
+
+ /**
+ * Handles both }} and }}}
+ */
+ private String handleClosebrace()
+ throws IOException
+ {
+ String res = "}";
+
+ int ch2 = nextToken();
+
+ if( ch2 == '}' )
+ {
+ int ch3 = nextToken();
+
+ if( ch3 == '}' )
+ {
+ if( m_isPre )
+ {
+ m_isPre = false;
+ m_isEscaping = false;
+ res = m_renderer.closePreformatted();
+ }
+ else
+ {
+ res = "}}}";
+ }
+ }
+ else
+ {
+ pushBack( ch3 );
+
+ if( !m_isEscaping )
+ {
+ res = m_renderer.closeTextEffect(TYPED);
+ m_isTypedText = false;
+ }
+ else
+ {
+ pushBack( ch2 );
+ }
+ }
+ }
+ else
+ {
+ pushBack( ch2 );
+ }
+
+ return res;
+ }
+
+ private String handleDash()
+ throws IOException
+ {
+ int ch = nextToken();
+
+ if( ch == '-' )
+ {
+ int ch2 = nextToken();
+
+ if( ch2 == '-' )
+ {
+ int ch3 = nextToken();
+
+ if( ch3 == '-' )
+ {
+ // Empty away all the rest of the dashes.
+ // Do not forget to return the first non-match back.
+ while( (ch = nextToken()) == '-' );
+
+ pushBack(ch);
+ return startBlockLevel()+m_renderer.makeRuler();
+ }
+
+ pushBack( ch3 );
+ }
+ pushBack( ch2 );
+ }
+
+ pushBack( ch );
+
+ return "-";
+ }
+
+ /**
+ * This method peeks ahead in the stream until EOL and returns the result.
+ * It will keep the buffers untouched.
+ *
+ * @return The string from the current position to the end of line.
+ */
+
+ // FIXME: Always returns an empty line, even if the stream is full.
+ private String peekAheadLine()
+ throws IOException
+ {
+ String s = readUntilEOL().toString();
+ pushBack( s );
+
+ return s;
+ }
+
+ private String handleHeading()
+ throws IOException
+ {
+ StringBuffer buf = new StringBuffer();
+
+ int ch = nextToken();
+
+ Heading hd = new Heading();
+
+ if( ch == '!' )
+ {
+ int ch2 = nextToken();
+
+ if( ch2 == '!' )
+ {
+ String title = peekAheadLine();
+
+ buf.append( m_renderer.makeHeading( Heading.HEADING_LARGE, title, hd) );
+ }
+ else
+ {
+ pushBack( ch2 );
+ String title = peekAheadLine();
+ buf.append( m_renderer.makeHeading( Heading.HEADING_MEDIUM, title, hd ) );
+ }
+ }
+ else
+ {
+ pushBack( ch );
+ String title = peekAheadLine();
+ buf.append( m_renderer.makeHeading( Heading.HEADING_SMALL, title, hd ) );
+ }
+
+ callHeadingListenerChain( hd );
+
+ return buf.toString();
+ }
+
+ /**
+ * Reads the stream until the next EOL or EOF. Note that it will also read the
+ * EOL from the stream.
+ */
+ private StringBuffer readUntilEOL()
+ throws IOException
+ {
+ int ch;
+ StringBuffer buf = new StringBuffer();
+
+ while( true )
+ {
+ ch = nextToken();
+
+ if( ch == -1 )
+ break;
+
+ buf.append( (char) ch );
+
+ if( ch == '\n' )
+ break;
+ }
+
+ return buf;
+ }
+
+ /**
+ * Starts a block level element, therefore closing the
+ * a potential open paragraph tag.
+ */
+ private String startBlockLevel()
+ {
+ if( m_isOpenParagraph )
+ {
+ m_isOpenParagraph = false;
+ return m_renderer.closeParagraph();
+ }
+
+ return "";
+ }
+
+ /**
+ * Like original handleOrderedList() and handleUnorderedList()
+ * however handles both ordered ('#') and unordered ('*') mixed together.
+ */
+
+ // FIXME: Refactor this; it's a bit messy.
+
+ private String handleGeneralList()
+ throws IOException
+ {
+ StringBuffer buf = new StringBuffer();
+
+ buf.append( startBlockLevel() );
+
+ String strBullets = readWhile( "*#" );
+ // String strBulletsRaw = strBullets; // to know what was original before phpwiki style substitution
+ int numBullets = strBullets.length();
+
+ // override the beginning portion of bullet pattern to be like the previous
+ // to simulate PHPWiki style lists
+
+ if(m_allowPHPWikiStyleLists)
+ {
+ // only substitute if different
+ if(!( strBullets.substring(0,Math.min(numBullets,m_genlistlevel)).equals
+ (m_genlistBulletBuffer.substring(0,Math.min(numBullets,m_genlistlevel)) ) ) )
+ {
+ if(numBullets <= m_genlistlevel)
+ {
+ // Substitute all but the last character (keep the expressed bullet preference)
+ strBullets = (numBullets > 1 ? m_genlistBulletBuffer.substring(0, numBullets-1) : "")
+ + strBullets.substring(numBullets-1, numBullets);
+ }
+ else
+ {
+ strBullets = m_genlistBulletBuffer + strBullets.substring(m_genlistlevel, numBullets);
+ }
+ }
+ }
+
+ //
+ // Check if this is still of the same type
+ //
+ if( strBullets.substring(0,Math.min(numBullets,m_genlistlevel)).equals
+ (m_genlistBulletBuffer.substring(0,Math.min(numBullets,m_genlistlevel)) ) )
+ {
+ if( numBullets > m_genlistlevel )
+ {
+ buf.append( m_renderer.openList(strBullets.charAt(m_genlistlevel++)) );
+
+ for( ; m_genlistlevel < numBullets; m_genlistlevel++ )
+ {
+ // bullets are growing, get from new bullet list
+ buf.append( m_renderer.openListItem() );
+ buf.append( m_renderer.openList(strBullets.charAt(m_genlistlevel)) );
+ }
+ }
+ else if( numBullets < m_genlistlevel )
+ {
+ // Close the previous list item.
+ buf.append( m_renderer.closeListItem() );
+
+ for( ; m_genlistlevel > numBullets; m_genlistlevel-- )
+ {
+ // bullets are shrinking, get from old bullet list
+ buf.append( m_renderer.closeList(m_genlistBulletBuffer.charAt(m_genlistlevel - 1)) );
+ if( m_genlistlevel > 0 ) buf.append( m_renderer.closeListItem() );
+
+ }
+ }
+ else
+ {
+ if( m_genlistlevel > 0 ) buf.append( m_renderer.closeListItem() );
+ }
+ }
+ else
+ {
+ //
+ // The pattern has changed, unwind and restart
+ //
+ int numEqualBullets;
+ int numCheckBullets;
+
+ // find out how much is the same
+ numEqualBullets = 0;
+ numCheckBullets = Math.min(numBullets,m_genlistlevel);
+
+ while( numEqualBullets < numCheckBullets )
+ {
+ // if the bullets are equal so far, keep going
+ if( strBullets.charAt(numEqualBullets) == m_genlistBulletBuffer.charAt(numEqualBullets))
+ numEqualBullets++;
+ // otherwise giveup, we have found how many are equal
+ else
+ break;
+ }
+
+ //unwind
+ for( ; m_genlistlevel > numEqualBullets; m_genlistlevel-- )
+ {
+ buf.append( m_renderer.closeList( m_genlistBulletBuffer.charAt(m_genlistlevel - 1) ) );
+ if( m_genlistlevel > 0 ) buf.append( m_renderer.closeListItem() );
+ }
+
+ //rewind
+ buf.append( m_renderer.openList( strBullets.charAt(numEqualBullets++) ) );
+ for(int i = numEqualBullets; i < numBullets; i++)
+ {
+ buf.append( m_renderer.openListItem() );
+ buf.append( m_renderer.openList( strBullets.charAt(i) ) );
+ }
+ m_genlistlevel = numBullets;
+ }
+ buf.append( m_renderer.openListItem() );
+
+ // work done, remember the new bullet list (in place of old one)
+ m_genlistBulletBuffer.setLength(0);
+ m_genlistBulletBuffer.append(strBullets);
+
+ return buf.toString();
+ }
+
+ private String unwindGeneralList()
+ {
+ // String cStrShortName = "unwindGeneralList()";
+
+ StringBuffer buf = new StringBuffer();
+
+ //unwind
+ for( ; m_genlistlevel > 0; m_genlistlevel-- )
+ {
+ buf.append(m_renderer.closeListItem());
+ buf.append( m_renderer.closeList( m_genlistBulletBuffer.charAt(m_genlistlevel - 1) ) );
+ }
+
+ m_genlistBulletBuffer.setLength(0);
+
+ return buf.toString();
+ }
+
+
+ private String handleDefinitionList()
+ throws IOException
+ {
+ if( !m_isdefinition )
+ {
+ m_isdefinition = true;
+
+ m_closeTag = m_renderer.closeDefinitionItem()+m_renderer.closeDefinitionList();
+
+ return startBlockLevel()+m_renderer.openDefinitionList()+m_renderer.openDefinitionTitle();
+ }
+
+ return ";";
+ }
+
+ private String handleOpenbracket()
+ throws IOException
+ {
+ StringBuffer sb = new StringBuffer();
+ int ch;
+ boolean isPlugin = false;
+
+ while( (ch = nextToken()) == '[' )
+ {
+ sb.append( (char)ch );
+ }
+
+ if( ch == '{' )
+ {
+ isPlugin = true;
+ }
+
+ pushBack( ch );
+
+ if( sb.length() > 0 )
+ {
+ return sb.toString();
+ }
+
+ //
+ // Find end of hyperlink
+ //
+
+ ch = nextToken();
+
+ while( ch != -1 )
+ {
+ if( ch == ']' && (!isPlugin || sb.charAt( sb.length()-1 ) == '}' ) )
+ {
+ break;
+ }
+
+ sb.append( (char) ch );
+
+ ch = nextToken();
+ }
+
+ if( ch == -1 )
+ {
+ log.debug("Warning: unterminated link detected!");
+ return sb.toString();
+ }
+
+ return handleHyperlinks( sb.toString() );
+ }
+
+ /**
+ * Reads the stream until the current brace is closed or stream end.
+ */
+ private String readBraceContent( char opening, char closing )
+ throws IOException
+ {
+ StringBuffer sb = new StringBuffer();
+ int braceLevel = 1;
+ int ch;
+ while(( ch = nextToken() ) != -1 )
+ {
+ if( ch == '\\' )
+ {
+ continue;
+ }
+ else if ( ch == opening )
+ {
+ braceLevel++;
+ }
+ else if ( ch == closing )
+ {
+ braceLevel--;
+ if (braceLevel==0)
+ {
+ break;
+ }
+ }
+ sb.append( (char)ch );
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Reads the stream until it meets one of the specified
+ * ending characters, or stream end. The ending character will be left
+ * in the stream.
+ */
+ private String readUntil( String endChars )
+ throws IOException
+ {
+ StringBuffer sb = new StringBuffer();
+ int ch = nextToken();
+
+ while( ch != -1 )
+ {
+ if( ch == '\\' )
+ {
+ ch = nextToken();
+ if( ch == -1 )
+ {
+ break;
+ }
+ }
+ else
+ {
+ if( endChars.indexOf((char)ch) != -1 )
+ {
+ pushBack( ch );
+ break;
+ }
+ }
+ sb.append( (char) ch );
+ ch = nextToken();
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Reads the stream while the characters that have been specified are
+ * in the stream, returning then the result as a String.
+ */
+ private String readWhile( String endChars )
+ throws IOException
+ {
+ StringBuffer sb = new StringBuffer();
+ int ch = nextToken();
+
+ while( ch != -1 )
+ {
+ if( endChars.indexOf((char)ch) == -1 )
+ {
+ pushBack( ch );
+ break;
+ }
+
+ sb.append( (char) ch );
+ ch = nextToken();
+ }
+
+ return sb.toString();
+ }
+
+
+ /**
+ * Handles constructs of type %%(style) and %%class
+ * @param newLine
+ * @return
+ * @throws IOException
+ */
+ private String handleDiv( boolean newLine )
+ throws IOException
+ {
+ int ch = nextToken();
+
+ if( ch == '%' )
+ {
+ StringBuffer sb = new StringBuffer();
+
+ String style = null;
+ String clazz = null;
+
+ ch = nextToken();
+
+ //
+ // Style or class?
+ //
+ if( ch == '(' )
+ {
+ style = readBraceContent('(',')');
+ }
+ else if( Character.isLetter( (char) ch ) )
+ {
+ pushBack( ch );
+ clazz = readUntil( " \t\n\r" );
+ ch = nextToken();
+
+ //
+ // Pop out only spaces, so that the upcoming EOL check does not check the
+ // next line.
+ //
+ if( ch == '\n' || ch == '\r' )
+ {
+ pushBack(ch);
+ }
+ }
+ else
+ {
+ //
+ // Anything else stops.
+ //
+
+ pushBack(ch);
+
+ try
+ {
+ Boolean isSpan = (Boolean)m_styleStack.pop();
+
+ if( isSpan == null )
+ {
+ // Fail quietly
+ }
+ else if( isSpan.booleanValue() )
+ {
+ sb.append( m_renderer.closeSpan() );
+ }
+ else
+ {
+ sb.append( m_renderer.closeDiv() );
+ }
+ }
+ catch( EmptyStackException e )
+ {
+ log.debug("Page '"+m_context.getPage().getName()+"' closes a %%-block that has not been opened.");
+ }
+
+ return sb.toString();
+ }
+
+ //
+ // Decide if we should open a div or a span?
+ //
+ String eol = peekAheadLine();
+
+ if( eol.trim().length() > 0 )
+ {
+ // There is stuff after the class
+
+ sb.append( m_renderer.openSpan( style, clazz ) );
+
+ m_styleStack.push( Boolean.TRUE );
+ }
+ else
+ {
+ sb.append( startBlockLevel() );
+ sb.append( m_renderer.openDiv( style, clazz ) );
+ m_styleStack.push( Boolean.FALSE );
+ }
+
+ return sb.toString();
+ }
+
+ pushBack(ch);
+
+ return "%";
+ }
+
+ private String handleBar( boolean newLine )
+ throws IOException
+ {
+ StringBuffer sb = new StringBuffer();
+
+ if( !m_istable && !newLine )
+ {
+ return "|";
+ }
+
+ if( newLine )
+ {
+ if( !m_istable )
+ {
+ sb.append( startBlockLevel() );
+ sb.append( m_renderer.openTable() );
+ m_istable = true;
+ }
+
+ sb.append( m_renderer.openTableRow() );
+ m_closeTag = m_renderer.closeTableItem()+m_renderer.closeTableRow();
+ }
+
+ int ch = nextToken();
+
+ if( ch == '|' )
+ {
+ if( !newLine )
+ {
+ sb.append( m_renderer.closeTableHeading() );
+ }
+ sb.append( m_renderer.openTableHeading() );
+ m_closeTag = m_renderer.closeTableHeading()+m_renderer.closeTableRow();
+ }
+ else
+ {
+ if( !newLine )
+ {
+ sb.append( m_renderer.closeTableItem() );
+ }
+ sb.append( m_renderer.openTableItem() );
+ pushBack( ch );
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Generic escape of next character or entity.
+ */
+ private String handleTilde()
+ throws IOException
+ {
+ int ch = nextToken();
+
+ if( ch == '|' || ch == '~' || ch == '\\' || ch == '*' || ch == '#' ||
+ ch == '-' || ch == '!' || ch == '\'' || ch == '_' || ch == '[' ||
+ ch == '{' || ch == ']' || ch == '}' )
+ {
+ StringBuffer sb = new StringBuffer();
+ sb.append( (char)ch );
+ sb.append(readWhile( ""+(char)ch ));
+ return sb.toString();
+ }
+
+ if( Character.isUpperCase( (char) ch ) )
+ {
+ pushBack( ch );
+ return "";
+ }
+
+ // No escape.
+ pushBack( ch );
+
+ return "~";
+ }
+
+ private void fillBuffer()
+ throws IOException
+ {
+ StringBuffer buf = new StringBuffer();
+ StringBuffer word = null;
+ int previousCh = -2;
+ int start = 0;
+
+ boolean quitReading = false;
+ boolean newLine = true; // FIXME: not true if reading starts in middle of buffer
+
+ while(!quitReading)
+ {
+ int ch = nextToken();
+ String s = null;
+
+ //
+ // Check if we're actually ending the preformatted mode.
+ // We still must do an entity transformation here.
+ //
+ if( m_isEscaping )
+ {
+ if( ch == '}' )
+ {
+ buf.append( handleClosebrace() );
+ }
+ else if( ch == -1 )
+ {
+ quitReading = true;
+ }
+ else
+ {
+ m_renderer.doChar( buf, (char)ch );
+ }
+
+ continue;
+ }
+
+ //
+ // CamelCase detection, a non-trivial endeavour.
+ // We keep track of all white-space separated entities, which we
+ // hereby refer to as "words". We then check for an existence
+ // of a CamelCase format text string inside the "word", and
+ // if one exists, we replace it with a proper link.
+ //
+
+ if( m_camelCaseLinks )
+ {
+ // Quick parse of start of a word boundary.
+
+ if( word == null &&
+ (Character.isWhitespace( (char)previousCh ) ||
+ WORD_SEPARATORS.indexOf( (char)previousCh ) != -1 ||
+ newLine ) &&
+ !Character.isWhitespace( (char) ch ) )
+ {
+ word = new StringBuffer();
+ }
+
+ // Are we currently tracking a word?
+ if( word != null )
+ {
+ //
+ // Check for the end of the word.
+ //
+
+ if( Character.isWhitespace( (char)ch ) ||
+ ch == -1 ||
+ WORD_SEPARATORS.indexOf( (char) ch ) != -1 )
+ {
+ String potentialLink = word.toString();
+
+ String camelCase = checkForCamelCaseLink(potentialLink);
+
+ if( camelCase != null )
+ {
+ // System.out.println("Buffer is "+buf);
+
+ // System.out.println(" Replacing "+camelCase+" with proper link.");
+ start = buf.toString().lastIndexOf( camelCase );
+ buf.replace(start,
+ start+camelCase.length(),
+ makeCamelCaseLink(camelCase) );
+
+ // System.out.println(" Resulting with "+buf);
+ }
+ else
+ {
+ // System.out.println("Checking for potential URI: "+potentialLink);
+ if( isExternalLink( potentialLink ) )
+ {
+ // System.out.println("buf="+buf);
+ start = buf.toString().lastIndexOf( potentialLink );
+
+ if( start >= 0 )
+ {
+ String link = readUntil(" \t()[]{}!\"'\n|");
+
+ link = potentialLink + (char)ch + link; // Do not forget the start.
+
+ // System.out.println("start="+start+", pl="+potentialLink);
+
+ buf.replace( start,
+ start + potentialLink.length(),
+ makeDirectURILink( link ) );
+
+ // System.out.println("Resulting with "+buf);
+
+ ch = nextToken();
+ }
+ }
+ }
+
+ // We've ended a word boundary, so time to reset.
+ word = null;
+ }
+ else
+ {
+ // This should only be appending letters and digits.
+ word.append( (char)ch );
+ } // if end of word
+ } // if word's not null
+
+ // Always set the previous character to test for word starts.
+ previousCh = ch;
+
+ } // if m_camelCaseLinks
+
+ //
+ // An empty line stops a list
+ //
+ if( newLine && ch != '*' && ch != '#' && ch != ' ' && m_genlistlevel > 0 )
+ {
+ buf.append(unwindGeneralList());
+ }
+
+ if( newLine && ch != '|' && m_istable )
+ {
+ buf.append( m_renderer.closeTable() );
+ m_istable = false;
+ m_closeTag = null;
+ }
+
+ //
+ // Now, check the incoming token.
+ //
+ switch( ch )
+ {
+ case '\r':
+ // DOS linefeeds we forget
+ s = null;
+ break;
+
+ case '\n':
+ //
+ // Close things like headings, etc.
+ //
+ if( m_closeTag != null )
+ {
+ buf.append( m_closeTag );
+ m_closeTag = null;
+ }
+
+ m_isdefinition = false;
+
+ if( newLine )
+ {
+ // Paragraph change.
+ buf.append( startBlockLevel() );
+
+ //
+ // Figure out which elements cannot be enclosed inside
+ // a <p></p> pair according to XHTML rules.
+ //
+ String nextLine = peekAheadLine();
+ if( nextLine.length() == 0 ||
+ (nextLine.length() > 0 &&
+ !nextLine.startsWith("{{{") &&
+ !nextLine.startsWith("----") &&
+ !nextLine.startsWith("%%") &&
+ "*#!;".indexOf( nextLine.charAt(0) ) == -1) )
+ {
+ buf.append( m_renderer.openParagraph() );
+ m_isOpenParagraph = true;
+ }
+ }
+ else
+ {
+ buf.append("\n");
+ newLine = true;
+ }
+
+ break;
+
+ case '\\':
+ s = handleBackslash();
+ break;
+
+ case '_':
+ s = handleUnderscore();
+ break;
+
+ case '\'':
+ s = handleApostrophe();
+ break;
+
+ case '{':
+ s = handleOpenbrace( newLine );
+ break;
+
+ case '}':
+ s = handleClosebrace();
+ break;
+
+ case '-':
+ s = handleDash();
+ break;
+
+ case '!':
+ if( newLine )
+ {
+ s = handleHeading();
+ }
+ else
+ {
+ s = "!";
+ }
+ break;
+
+ case ';':
+ if( newLine )
+ {
+ s = handleDefinitionList();
+ }
+ else
+ {
+ s = ";";
+ }
+ break;
+
+ case ':':
+ if( m_isdefinition )
+ {
+ s = m_renderer.closeDefinitionTitle()+m_renderer.openDefinitionItem();
+ m_isdefinition = false;
+ }
+ else
+ {
+ s = ":";
+ }
+ break;
+
+ case '[':
+ s = handleOpenbracket();
+ break;
+
+ case '*':
+ if( newLine )
+ {
+ pushBack('*');
+ s = handleGeneralList();
+ }
+ else
+ {
+ s = "*";
+ }
+ break;
+
+ case '#':
+ if( newLine )
+ {
+ pushBack('#');
+ s = handleGeneralList();
+ }
+ else
+ {
+ s = "#";
+ }
+ break;
+
+ case '|':
+ s = handleBar( newLine );
+ break;
+
+ case '<':
+ s = m_allowHTML ? "<" : "<";
+ break;
+
+ case '>':
+ s = m_allowHTML ? ">" : ">";
+ break;
+
+ case '\"':
+ s = m_allowHTML ? "\"" : """;
+ break;
+
+ /*
+ case '&':
+ s = "&";
+ break;
+ */
+ case '~':
+ s = handleTilde();
+ break;
+
+ case '%':
+ s = handleDiv( newLine );
+ break;
+
+ case -1:
+ if( m_closeTag != null )
+ {
+ buf.append( m_closeTag );
+ m_closeTag = null;
+ }
+ quitReading = true;
+ break;
+
+ default:
+ buf.append( (char)ch );
+ newLine = false;
+ break;
+ }
+
+ if( s != null )
+ {
+ buf.append( s );
+ newLine = false;
+ }
+
+ }
+
+ m_data = new StringReader( buf.toString() );
+ }
+
+
+ public int read()
+ throws IOException
+ {
+ int val = m_data.read();
+
+ if( val == -1 )
+ {
+ fillBuffer();
+ val = m_data.read();
+
+ if( val == -1 )
+ {
+ m_data = new StringReader( closeAll() );
+
+ val = m_data.read();
+ }
+ }
+
+ return val;
+ }
+
+ public int read( char[] buf, int off, int len )
+ throws IOException
+ {
+ return m_data.read( buf, off, len );
+ }
+
+ public boolean ready()
+ throws IOException
+ {
+ log.debug("ready ? "+m_data.ready() );
+ if(!m_data.ready())
+ {
+ fillBuffer();
+ }
+
+ return m_data.ready();
+ }
+
+ public void close()
+ {
+ }
+
+ /**
+ * All HTML output stuff is here. This class is a helper class, and will
+ * be spawned later on with a proper API of its own so that we can have
+ * different kinds of renderers.
+ */
+
+ // FIXME: Not everything is yet, and in the future this class will be spawned
+ // out to be its own class.
+ private class HTMLRenderer
+ extends TextRenderer
+ {
+ private boolean m_isPreBlock = false;
+ private TranslatorReader m_cleanTranslator;
+
+ /*
+ FIXME: It's relatively slow to create two TranslatorReaders each time.
+ */
+ public HTMLRenderer()
+ {
+ }
+
+ /**
+ * Does a lazy init. Otherwise, we would get into a situation
+ * where HTMLRenderer would try and boot a TranslatorReader before
+ * the TranslatorReader it is contained by is up.
+ */
+ private TranslatorReader getCleanTranslator()
+ {
+ if( m_cleanTranslator == null )
+ {
+ WikiContext dummyContext = m_engine.getWikiActionBeanFactory().newViewActionBean( m_context.getPage() );
+ m_cleanTranslator = new TranslatorReader( dummyContext,
+ null,
+ new TextRenderer() );
+ m_cleanTranslator.m_allowHTML = true;
+ }
+
+ return m_cleanTranslator;
+ }
+
+ public void doChar( StringBuffer buf, char ch )
+ {
+ if( ch == '<' )
+ {
+ buf.append("<");
+ }
+ else if( ch == '>' )
+ {
+ buf.append(">");
+ }
+ else if( ch == '&' )
+ {
+ buf.append("&");
+ }
+ else
+ {
+ buf.append( ch );
+ }
+ }
+
+ public String openDiv( String style, String clazz )
+ {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append( "<div" );
+ sb.append( style != null ? " style=\""+style+"\"" : "" );
+ sb.append( clazz != null ? " class=\""+clazz+"\"" : "" );
+ sb.append( ">" );
+
+ return sb.toString();
+ }
+
+ public String openSpan( String style, String clazz )
+ {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append( "<span" );
+ sb.append( style != null ? " style=\""+style+"\"" : "" );
+ sb.append( clazz != null ? " class=\""+clazz+"\"" : "" );
+ sb.append( ">" );
+
+ return sb.toString();
+ }
+
+ public String closeDiv()
+ {
+ return "</div>";
+ }
+
+ public String closeSpan()
+ {
+ return "</span>";
+ }
+
+ public String openParagraph()
+ {
+ return "<p>";
+ }
+
+ public String closeParagraph()
+ {
+ return "</p>\n";
+ }
+
+ /**
+ * Writes out a text effect
+ */
+ public String openTextEffect( int effect )
+ {
+ switch( effect )
+ {
+ case BOLD:
+ return "<b>";
+ case ITALIC:
+ return "<i>";
+ case TYPED:
+ return "<tt>";
+ }
+
+ return "";
+ }
+
+ public String closeTextEffect( int effect )
+ {
+ switch( effect )
+ {
+ case BOLD:
+ return "</b>";
+ case ITALIC:
+ return "</i>";
+ case TYPED:
+ return "</tt>";
+ }
+
+ return "";
+ }
+
+ public String openDefinitionItem()
+ {
+ return "<dd>";
+ }
+
+ public String closeDefinitionItem()
+ {
+ return "</dd>\n";
+ }
+
+ public String openDefinitionTitle()
+ {
+ return "<dt>";
+ }
+ public String closeDefinitionTitle()
+ {
+ return "</dt>";
+ }
+
+ public String openDefinitionList()
+ {
+ return "<dl>\n";
+ }
+
+ public String closeDefinitionList()
+ {
+ return "</dl>";
+ }
+
+
+ /**
+ * Write a HTMLized link depending on its type.
+ *
+ * <p>This jsut calls makeLink() with "section" set to null.
+ */
+ public String makeLink( int type, String link, String text )
+ {
+ return makeLink( type, link, text, null );
+ }
+
+ private final String getURL( Class<? extends WikiActionBean> beanClass, String link )
+ {
+ return m_context.getContext().getURL( beanClass, link );
+ }
+
+ /**
+ * Write a HTMLized link depending on its type.
+ *
+ * @param type Type of the link.
+ * @param link The actual link.
+ * @param text The user-visible text for the link.
+ * @param section Which named anchor to point to. This may not have any
+ * effect on certain link types. If null, will ignore it.
+ */
+ public String makeLink( int type, String link, String text, String section )
+ {
+ String result;
+
+ if( text == null ) text = link;
+
+ section = (section != null) ? ("#"+section) : "";
+
+ // Make sure we make a link name that can be accepted
+ // as a valid URL.
+
+ String encodedlink = m_engine.encodeName( link );
+
+ if( encodedlink.length() == 0 )
+ {
+ type = EMPTY;
+ }
+
+ switch(type)
+ {
+ case READ:
+ result = "<a class=\"wikipage\" href=\""+getURL(ViewActionBean.class,
+ link)+section+"\">"+text+"</a>";
+ break;
+
+ case EDIT:
+ result = "<a class=\"createpage\" title=\"Create '"+link+"'\" href=\""+
+ getURL(EditActionBean.class, link)+"\">"+
+ text+"</a>";
+ break;
+
+ case EMPTY:
+ result = "<u>"+text+"</u>";
+ break;
+
+ //
+ // These two are for local references - footnotes and
+ // references to footnotes.
+ // We embed the page name (or whatever WikiContext gives us)
+ // to make sure the links are unique across Wiki.
+ //
+ case LOCALREF:
+ result = "<a class=\"footnoteref\" href=\"#ref-"+
+ m_context.getPage().getName()+"-"+
+ link+"\">["+text+"]</a>";
+ break;
+
+ case LOCAL:
+ result = "<a class=\"footnote\" name=\"ref-"+
+ m_context.getPage().getName()+"-"+
+ link.substring(1)+"\">["+text+"]</a>";
+ break;
+
+ //
+ // With the image, external and interwiki types we need to
+ // make sure nobody can put in Javascript or something else
+ // annoying into the links themselves. We do this by preventing
+ // a haxor from stopping the link name short with quotes in
+ // fillBuffer().
+ //
+ case IMAGE:
+ result = "<img class=\"inline\" src=\""+link+"\" alt=\""+text+"\" />";
+ break;
+
+ case IMAGELINK:
+ result = "<a href=\""+text+"\"><img class=\"inline\" src=\""+link+"\" alt=\""+text+"\"/></a>";
+ break;
+
+ case IMAGEWIKILINK:
+ String pagelink = getURL(ViewActionBean.class,text);
+ result = "<a class=\"wikipage\" href=\""+pagelink+"\"><img class=\"inline\" src=\""+link+"\" alt=\""+text+"\" /></a>";
+ break;
+
+ case EXTERNAL:
+ result = "<a class=\"external\" "+
+ (m_useRelNofollow ? "rel=\"nofollow\" " : "")+
+ "href=\""+link+section+"\">"+text+"</a>";
+ break;
+
+ case INTERWIKI:
+ result = "<a class=\"interwiki\" href=\""+link+section+"\">"+text+"</a>";
+ break;
+
+ case ATTACHMENT:
+ String attlink = getURL( AttachActionBean.class,
+ link );
+
+ String infolink = getURL( PageInfoActionBean.class,
+ link );
+
+ String imglink = getURL( NoneActionBean.class,
+ "images/attachment_small.png" );
+
+ result = "<a class=\"attachment\" href=\""+attlink+"\">"+text+"</a>"+
+ "<a href=\""+infolink+
+ "\"><img src=\""+imglink+"\" alt=\"(info)\"/></a>";
+ break;
+
+ default:
+ result = "";
+ break;
+ }
+
+ return result;
+ }
+
+ /**
+ * Writes HTML for error message.
+ */
+
+ public String makeError( String error )
+ {
+ return "<span class=\"error\">"+error+"</span>";
+ }
+
+ /**
+ * Emits a vertical line.
+ */
+
+ public String makeRuler()
+ {
+ return "<hr />";
+ }
+
+ /**
+ * Modifies the "hd" parameter to contain proper values. Because
+ * an "id" tag may only contain [a-zA-Z0-9:_-], we'll replace the
+ * % after url encoding with '_'.
+ */
+ private String makeHeadingAnchor( String baseName, String title, Heading hd )
+ {
+ hd.m_titleText = title;
+ title = cleanLink( title );
+ hd.m_titleSection = m_engine.encodeName(title);
+ hd.m_titleAnchor = "section-"+m_engine.encodeName(baseName)+
+ "-"+hd.m_titleSection;
+
+ hd.m_titleAnchor = hd.m_titleAnchor.replace( '%', '_' );
+ return hd.m_titleAnchor;
+ }
+
+ private String makeSectionTitle( String title )
+ {
+ title = title.trim();
+
+ StringWriter outTitle = new StringWriter();
+
+ try
+ {
+ TranslatorReader read = getCleanTranslator();
+ read.setInputReader( new StringReader(title) );
+ FileUtil.copyContents( read, outTitle );
+ }
+ catch( IOException e )
+ {
+ log.fatal("CleanTranslator not working", e);
+ throw new InternalWikiException("CleanTranslator not working as expected, when cleaning title"+ e.getMessage() );
+ }
+
+ return outTitle.toString();
+ }
+
+ /**
+ * Returns XHTML for the start of the heading. Also sets the
+ * line-end emitter.
+ * @param level
+ * @param headings A List to which heading should be added.
+ */
+ public String makeHeading( int level, String title, Heading hd )
+ {
+ String res = "";
+
+ String pageName = m_context.getPage().getName();
+
+ String outTitle = makeSectionTitle( title );
+
+ hd.m_level = level;
+
+ switch( level )
+ {
+ case Heading.HEADING_SMALL:
+ res = "<h4 id='"+makeHeadingAnchor( pageName, outTitle, hd )+"'>";
+ m_closeTag = "</h4>";
+ break;
+
+ case Heading.HEADING_MEDIUM:
+ res = "<h3 id='"+makeHeadingAnchor( pageName, outTitle, hd )+"'>";
+ m_closeTag = "</h3>";
+ break;
+
+ case Heading.HEADING_LARGE:
+ res = "<h2 id='"+makeHeadingAnchor( pageName, outTitle, hd )+"'>";
+ m_closeTag = "</h2>";
+ break;
+ }
+
+ return res;
+ }
+
+ /**
+ * @param bullet A character detailing which kind of a list
+ * we are dealing with here. Options are '#' and '*'.
+ */
+ public String openList( char bullet )
+ {
+ String res = "";
+
+ if( bullet == '#' )
+ res = "<ol>\n";
+ else if( bullet == '*' )
+ res = "<ul>\n";
+ else
+ log.info("Warning: unknown bullet character '" + bullet + "' at (+)" );
+
+ return res;
+ }
+
+ public String openListItem()
+ {
+ return "<li>";
+ }
+
+ public String closeListItem()
+ {
+ return "</li>\n";
+ }
+
+ /**
+ * @param bullet A character detailing which kind of a list
+ * we are dealing with here. Options are '#' and '*'.
+ */
+ public String closeList( char bullet )
+ {
+ String res = "";
+
+ if( bullet == '#' )
+ {
+ res = "</ol>\n";
+ }
+ else if( bullet == '*' )
+ {
+ res = "</ul>\n";
+ }
+ else
+ {
+ //FIXME unknown character -> error
+ log.info("Warning: unknown character in unwind '" + bullet + "'" );
+ }
+
+ return res;
+ }
+
+ public String openTable()
+ {
+ return "<table class=\"wikitable\" border=\"1\">\n";
+ }
+
+ public String closeTable()
+ {
+ return "</table>\n";
+ }
+
+ public String openTableRow()
+ {
+ return "<tr>";
+ }
+
+ public String closeTableRow()
+ {
+ return "</tr>";
+ }
+
+ public String openTableItem()
+ {
+ return "<td>";
+ }
+
+ public String closeTableItem()
+ {
+ return "</td>";
+ }
+
+ public String openTableHeading()
+ {
+ return "<th>";
+ }
+
+ public String closeTableHeading()
+ {
+ return "</th>";
+ }
+
+ public String openPreformatted( boolean isBlock )
+ {
+ m_isPreBlock = isBlock;
+
+ if( isBlock )
+ {
+ return "<pre>";
+ }
+
+ return "<span style=\"font-family:monospace; whitespace:pre;\">";
+ }
+
+ public String closePreformatted()
+ {
+ if( m_isPreBlock )
+ return "</pre>\n";
+
+ return "</span>";
+ }
+
+ /**
+ * If outlink images are turned on, returns a link to the outward
+ * linking image.
+ */
+ public String outlinkImage()
+ {
+ if( m_useOutlinkImage )
+ {
+ return "<img class=\"outlink\" src=\""+
+ getURL( NoneActionBean.class,"images/out.png" )+"\" alt=\"\" />";
+ }
+
+ return "";
+ }
+
+ /**
+ * @param clear If true, then flushes all thingies.
+ */
+ public String lineBreak( boolean clear )
+ {
+ if( clear )
+ return "<br clear=\"all\" />";
+
+ return "<br />";
+ }
+
+ } // HTMLRenderer
+
+ /**
+ * A very simple class for outputting plain text with no
+ * formatting.
+ */
+ public class TextRenderer
+ {
+ public TextRenderer() {}
+
+ public void doChar( StringBuffer buf, char ch )
+ {
+ buf.append( ch );
+ }
+
+ public String openDiv( String style, String clazz )
+ {
+ return "";
+ }
+
+ public String closeDiv()
+ {
+ return "";
+ }
+
+ public String openSpan( String style, String clazz )
+ {
+ return "";
+ }
+
+ public String closeSpan()
+ {
+ return "";
+ }
+
+ public String openParagraph()
+ {
+ return "";
+ }
+
+ public String closeParagraph()
+ {
+ return "\n\n";
+ }
+
+ /**
+ * Writes out a text effect
+ */
+ public String openTextEffect( int effect )
+ {
+ return "";
+ }
+
+ public String closeTextEffect( int effect )
+ {
+ return "";
+ }
+
+ public String openDefinitionItem()
+ {
+ return " : ";
+ }
+
+ public String closeDefinitionItem()
+ {
+ return "\n";
+ }
+
+ public String openDefinitionTitle()
+ {
+ return "";
+ }
+
+ public String closeDefinitionTitle()
+ {
+ return "";
+ }
+
+ public String openDefinitionList()
+ {
+ return "";
+ }
+
+ public String closeDefinitionList()
+ {
+ return "\n";
+ }
+
+
+ /**
+ * Write a HTMLized link depending on its type.
+ *
+ * <p>This jsut calls makeLink() with "section" set to null.
+ */
+ public String makeLink( int type, String link, String text )
+ {
+ return text;
+ }
+
[... 160 lines stripped ...]