incubator-jspwiki-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Andrew Jaquith <andrew.jaqu...@mac.com>
Subject Re: Problems with new "long" links, UTF-8, allowed punctuation...
Date Fri, 15 Feb 2008 13:11:14 GMT
Christophe --

I do not understand the purpose of this e-mail. Are these bugs you are  
trying to correct? Or proposed enhancements? In either case, JIRA is  
the right place to file them.

Andrew

On Feb 15, 2008, at 3:50, Christophe Dupriez <christophe.dupriez@destin.be 
 > wrote:

> Hi Again !
>
> I spent a few days to implement:
> http://www.destin.be/CAFE
>
> It is "La Clé", a dictionnary of litterary devices with entry headin 
> gs
> being terms with a lot of punctuation and accented letters.
>
> First, all my CONGRATULATIONS for departing from the WikiName  
> CamelCase
> paradygm: at Poison Centre, the chemical names are really bad when
> camelCased and for "La Clé" it was simply not an option. It seems to 
>  be
> still a (difficult) work in progress and please find below my
> contribution for debugging this.
>
> Suggestion: backward compatibility (camelCasing) could be a  
> configurable
> property. This would prevent having complex code to maintain both
> approaches in parrallel. A wiki would then be either "traditional" or
> "unrestricted" (for unrestricted names). A conversion program could
> allow to go from one to the other (for those who need it): this  
> program
> would probably not be lossless when going from "unrestricted" to
> "traditional".
>
> So, for this conversion, I made many tests, changed the data where
> acceptable and (minimaly) changed JSPWiki when I had to: I provide
> herewith the source code for 2.6.1. Changes are very punctual: with
> WinMerge, one sees what is happening in seconds.
>
> I still have problems with page renaming and page names in forms so  
> the
> herewith corrections are not sufficient for a release.
>
> The final conversion of imported ASCII characters (within names)  
> that I
> implement is:
>
> ' ': ONE space is kept (sequences of two or more spaces becomes only  
> one)
> Spaces at beginning and end of the name are completely removed.
>
> '.': ONE dot is kept (sequences of two or more dots becomes only one:
> this to protect Windows which does not like ".." in file names)
> Dots at the end of the name must be completely removed (This prevents
> Windows to badly manage a file name containing "..txt").
>
> '[': '(' : square brackets are links markup delimiters...
> ']': ')'
>
> '|': "=" : vertical bars are delimiting parts of a link definition.  
> They
> are replaced by "="
> "'": 0xE2,0x80,0x99 : The ASCII quote is replaced by the UTF-8
> apostrophe (like the one MS-Word generates in french texts). An help  
> for
> this will be necessary in the Wiki Page Editors.
>
> ':': "=" : this is the InterWiki prefix delimiter. I replace it by "="
> for now but I would prefer to have ":<space>" accepted in some  
> future...
> (some code already provided for this)
>
> '/': Introduces an attachment and it is better not to use it (for  
> now: I
> began to add support to accept /<space> within a name)
>
> '\': is systematically removed. Why?
> '`' (0x60): is systematically removed. Why?
> '~' (0x7E): is systematically removed. Why?
> '!': is systematically removed. Why?
>
> The main changes I had to do to JSPWiki was to make it accept ALL non
> ASCII characters ( code >= 0x80 )in page names (not only the  
> alphabetic
> ones).
>
> This occurs into:
>
> 1) In TranslatorReader.java, method cleanLink:
> for( int i = 0; i < clean.length(); i++ )
> {
> char ch = clean.charAt(i);
>
> if( !(ch >= 0x80 || // All non ASCII are allowed!!!
> Character.isLetterOrDigit(ch) ||
> PUNCTUATION_CHARS_ALLOWED.indexOf(ch) != -1 ))
> {
> clean.deleteCharAt(i);
> --i; // We just shortened this buffer.
> }
> }
>
> 2) In MarkupParser.java, method cleanLink:
> //
> // Check if it is allowed to use this char, and capitalize, if  
> necessary.
> //
> if( ch >= 0x80 || // All non ASCII are allowed!!!
> Character.isLetterOrDigit( ch ) ||
> allowedChars.indexOf(ch) != -1 )
> {
> // Is a letter
>
> if( isWord ) ch = Character.toUpperCase( ch );
> clean.append( ch );
> isWord = false;
> }
> else
> {
> isWord = true;
> }
>
>
> Two bugs where corrected when encoding UTF-8 in  
> DefaultURLConstructor.java:
> public String parsePage( String context,
> HttpServletRequest request,
> String encoding )
> throws UnsupportedEncodingException
> {
> request.setCharacterEncoding( encoding );
> String pagereq = request.getParameter( "page" );
>
> if( context.equals(WikiContext.ATTACH) )
> {
> pagereq = parsePageFromURL( request, encoding );
> }
> !!!! else pagereq = TextUtil.urlDecode( pagereq, encoding ); !!!! I am
> unsure if this is working when editing a page name within a POSTED  
> form ???
> log.debug("parsePage: "+encoding+":"+pagereq);
>
> return pagereq;
> }
> !!! AND ALSO, below, in parsePage, I uncommented the line:
> name = TextUtil.urlDecode( name, encoding );
>
> I notice a few discrepanties between the different classes working  
> with
> page names:
> - PageRenamer.java:
> private static final String LONG_LINK_PATTERN =
> "\\[([\\w\\s]+\\|)?([\\w\\s\\+-/\\?&;@:=%\\#<>$\\.,\\(\\)'\\*]+)?\\]";
> In MarkupParser.java:
> public static final String PUNCTUATION_CHARS_ALLOWED = " ()&+,-=._$";
> !!! NOT FULLY IN LINE WITH PageRenamer.java WHICH ALLOWS: "
> ()&+,-=._$/?;@:%#<>'*" ( space is in \s and _ in \w )
> !!! WHY DO ME FORBID OTHER CHARACTERS THAN "|", ":" (":<space>" should
> be allowed), "]") ?
>
> I still notice some problems with renaming and with forms where  
> UTF-8 is  decoded in ISO 8859.
>
> - PageRenamer.java, I have problems with renaming "long" names  
> references: "Null link while trying to rename! Culprit text is ..."
> in com.ecyrd.jspwiki.PageRenamer.replaceLongLinks(), line 330
>
>
> That is all for today!
> THANKS FOR ALL!
>
> Christophe
>
>
> /*
> JSPWiki - a JSP-based WikiWiki clone.
>
> Copyright (C) 2001-2006 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.parser;
>
> import java.io.BufferedReader;
> import java.io.IOException;
> import java.io.PushbackReader;
> import java.io.Reader;
> import java.util.ArrayList;
>
>
> import com.ecyrd.jspwiki.StringTransmutator;
> import com.ecyrd.jspwiki.WikiContext;
> import com.ecyrd.jspwiki.WikiEngine;
>
> /**
> *   Provides an abstract class for the parser instances.
> *
> *   @author Janne Jalkanen
> *   @since  2.4
> */
> public abstract class MarkupParser
> {
>   /** Allow this many characters to be pushed back in the stream.   
> In effect,
>       this limits the size of a single line.  */
>   protected static final int              PUSHBACK_BUFFER_SIZE =  
> 10*1024;
>   protected PushbackReader                m_in;
>   private int              m_pos = -1; // current position in reader  
> stream
>
>   protected WikiEngine     m_engine;
>   protected WikiContext    m_context;
>
>   /** Optionally stores internal wikilinks */
>   protected ArrayList      m_localLinkMutatorChain    = new ArrayList 
> ();
>   protected ArrayList      m_externalLinkMutatorChain = new ArrayList 
> ();
>   protected ArrayList      m_attachmentLinkMutatorChain = new  
> ArrayList();
>   protected ArrayList      m_headingListenerChain     = new ArrayList 
> ();
>   protected ArrayList      m_linkMutators             = new ArrayList 
> ();
>
>   protected boolean        m_inlineImages             = true;
>
>   protected boolean        m_parseAccessRules = true;
>   /** 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", enables plugins during parsing */
>   public static final String     PROP_RUNPLUGINS       =  
> "jspwiki.translatorReader.runPlugins";
>
>   /** Lists all punctuation characters allowed in WikiMarkup. These
>       will not be cleaned away. This is for compatibility for older  
> versions
>       of JSPWiki. */
>
>   protected static final String           LEGACY_CHARS_ALLOWED       
> = "._";
>
>   /** Lists all punctuation characters allowed in page names. */
>   public    static final String           PUNCTUATION_CHARS_ALLOWED  
> = " ()&+,-=._$" + "?;@:%#<>'*"; // "/" character is making problem  
> with attachments...
>
>   protected MarkupParser( WikiContext context, Reader in )
>   {
>       m_engine = context.getEngine();
>       m_context = context;
>       setInputReader( in );
>   }
>
>   /**
>    *  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;
>   }
>
>   /**
>    *  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 );
>       }
>   }
>
>   public void disableAccessRules()
>   {
>       m_parseAccessRules = false;
>   }
>
>   /**
>    *  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;
>   }
>
>   /**
>    *  Parses the document.
>    *  @return the parsed document, as a WikiDocument
>    *  @throws IOException
>    */
>   public abstract WikiDocument parse()
>        throws IOException;
>
>   /**
>    *  Return the current position in the reader stream.
>    *  The value will be -1 prior to reading.
>    * @return the reader position as an int.
>    */
>   public int getPosition()
>   {
>       return m_pos;
>   }
>
>   /**
>    * Returns the next token in the stream.  This is the most called  
> method
>    * in the entire parser, so it needs to be lean and mean.
>    *
>    * @return The next token in the stream; or, if the stream is  
> ended, -1.
>    * @throws IOException If something bad happens
>    * @throws NullPointerException If you have not yet created an  
> input document.
>    */
>   protected final int nextToken()
>       throws IOException
>   {
>       // if( m_in == null ) return -1;
>       m_pos++;
>       return m_in.read();
>   }
>
>   /**
>    *  Push back any character to the current input.  Does not
>    *  push back a read EOF, though.
>    */
>   protected void pushBack( int c )
>       throws IOException
>   {
>       if( c != -1 && m_in != null )
>       {
>           m_pos--;
>           m_in.unread( c );
>       }
>   }
>
>   /**
>    *  Cleans a Wiki name.  The functionality of this method was  
> changed in 2.6
>    *  so that the list of allowed characters is much larger.  Use  
> wikifyLink()
>    *  to get the legacy behaviour.
>    *  <P>
>    *  [ This is a link ] -&gt; This is a link
>    *
>    *  @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 )
>   {
>       return cleanLink(link, PUNCTUATION_CHARS_ALLOWED);
>   }
>
>   /**
>    *  Cleans a Wiki name based on a list of characters.  Also, any  
> multiple
>    *  whitespace is collapsed into a single space, and any leading  
> or trailing
>    *  space is removed. ALSO TRAILING DOTS ARE REMOVED FOR WINDOWS  
> COMPATIBILITY
>    *  ALSO REMOVAL OF DOUBLED DOTS...
>    *
>    *  @param link Link to be cleared. Null is safe, and causes this  
> to return null.
>    *  @param allowedChars Characters which are allowed in the string.
>    *  @return A cleaned link.
>    *
>    *  @since 2.6
>    */
>   public static String cleanLink( String link, String allowedChars )
>   {
>       if( link == null ) return null;
>
>       link = link.trim();
>       StringBuffer clean = new StringBuffer(link.length());
>
>       //
>       //  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.
>       //
>       //  Also capitalize things, if necessary. WHY????
>       //
>
>       boolean isWord = true;  // If true, we've just crossed a word  
> boundary
>       boolean wasSpace = false;
>       boolean wasDot = false;
>
>       for( int i = 0; i < link.length(); i++ )
>       {
>           char ch = link.charAt(i);
>
>           //
>           //  Cleans away repetitive whitespace and only uses the  
> first one.
>           //
>           if( Character.isWhitespace(ch) )
>           {
>               wasDot = false;
>               if( wasSpace )
>                   continue;
>               wasSpace = true;
>           }
>           else
>           {
>               wasSpace = false;
>           }
>
>           // Remove also doubled dots
>           if( ch=='.' )
>           {
>               if( wasDot )
>                   continue;
>               wasDot = true;
>           }
>           else
>           {
>               wasDot = false;
>           }
>
>           //
>           //  Check if it is allowed to use this char, and  
> capitalize, if necessary.
>           //
>           if( ch >= 0x80 || Character.isLetterOrDigit( ch ) ||  
> allowedChars.indexOf(ch) != -1 )
>           {
>               // Is a letter
>
>               if( isWord ) ch = Character.toUpperCase( ch );
>               clean.append( ch );
>               isWord = false;
>           }
>           else
>           {
>               isWord = true;
>           }
>       }
>       while (clean.length() > 0) {
>         if (clean.charAt(clean.length()-1) == '.')
>             clean.setLength(clean.length()-1);
>         else break;
>      }
>
>       return clean.toString();
>   }
>
>   /**
>    *  Cleans away extra legacy characters.  This method functions  
> exactly
>    *  like pre-2.6 cleanLink()
>    *  <P>
>    *  [ This is a link ] -&gt; ThisIsALink
>    *
>    *  @param link Link to be cleared. Null is safe, and causes this  
> to return null.
>    *  @return A cleaned link.
>    *  @since 2.6
>    */
>   public static String wikifyLink(String link)
>   {
>       return MarkupParser.cleanLink(link,  
> MarkupParser.LEGACY_CHARS_ALLOWED);
>   }
>
> }
>
> /*
> 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.url;
>
> import java.io.UnsupportedEncodingException;
> import java.net.MalformedURLException;
> import java.net.URL;
> import java.util.Properties;
>
> import javax.servlet.http.HttpServletRequest;
>
> import org.apache.commons.lang.StringUtils;
>
> import com.ecyrd.jspwiki.TextUtil;
> import com.ecyrd.jspwiki.WikiContext;
> import com.ecyrd.jspwiki.WikiEngine;
> import com.ecyrd.jspwiki.ui.Command;
> import com.ecyrd.jspwiki.ui.CommandResolver;
>
> import org.apache.log4j.Logger;
>
> /**
> *  Implements the default URL constructor using links directly to the
> *  JSP pages.  This is what JSPWiki by default is using.  For example,
> *  WikiContext.VIEW points at "Wiki.jsp", etc.
> *
> *  @author Janne Jalkanen
> *  @since 2.2
> */
> public class DefaultURLConstructor
>   implements URLConstructor
> {
>   static final Logger log = Logger.getLogger 
> (DefaultURLConstructor.class.getName());
>
>   protected WikiEngine m_engine;
>
>   /** Are URL styles relative or absolute? */
>   protected boolean          m_useRelativeURLStyle = true;
>
>   /**
>    *  Contains the absolute path of the JSPWiki Web application  
> without the
>    *  actual servlet (which is the m_urlPrefix).
>    */
>   protected String m_pathPrefix = "";
>
>   /**
>    *
>    * {@inheritDoc}
>    */
>   public void initialize( WikiEngine engine,
>                           Properties properties )
>   {
>       m_engine = engine;
>
>       m_useRelativeURLStyle = "relative".equals 
> ( properties.getProperty( WikiEngine.PROP_REFSTYLE,
>     
>     
>                                                                     
> "relative" ) );
>
>       String baseurl = engine.getBaseURL();
>
>       if( baseurl != null && baseurl.length() > 0 )
>       {
>           try
>           {
>               URL url = new URL( baseurl );
>
>               String path = url.getPath();
>
>               m_pathPrefix = path;
>           }
>           catch( MalformedURLException e )
>           {
>               m_pathPrefix = "/JSPWiki"; // Just a guess.
>           }
>       }
>   }
>
>   /**
>    *  Does replacement of some particular variables.  The variables  
> are:
>    *
>    *  <ul>
>    *  <li> "%u" - inserts either the base URL (when absolute is  
> required), or the base path
>    *       (which is an absolute path without the host name).
>    *  <li> "%U" - always inserts the base URL
>    *  <li> "%p" - always inserts the base path
>    *  <li> "%n" - inserts the page name
>    *  </ul>
>    *
>    * @param baseptrn  The pattern to use
>    * @param name The page name
>    * @param absolute If true, %u is always the entire base URL,  
> otherwise it depends on
>    *                 the setting in jspwiki.properties.
>    * @return A replacement.
>    */
>   protected final String doReplacement( String baseptrn, String  
> name, boolean absolute )
>   {
>       String baseurl = m_pathPrefix;
>
>       if( absolute ) baseurl = m_engine.getBaseURL();
>
>       baseptrn = TextUtil.replaceString( baseptrn, "%u", baseurl );
>       baseptrn = TextUtil.replaceString( baseptrn, "%U",  
> m_engine.getBaseURL() );
>       baseptrn = TextUtil.replaceString( baseptrn, "%n", encodeURI 
> (name) );
>       baseptrn = TextUtil.replaceString( baseptrn, "%p",  
> m_pathPrefix );
>
>       return baseptrn;
>   }
>
>   /**
>    *  URLEncoder returns pluses, when we want to have the percent
>    *  encoding.  See http://issues.apache.org/bugzilla/show_bug.cgi?id=39278
>    *  for more info.
>    *
>    *  We also convert any %2F's back to slashes to make nicer- 
> looking URLs.
>    */
>   private final String encodeURI( String uri )
>   {
>       uri = m_engine.encodeName(uri);
>
>       uri = StringUtils.replace( uri, "+", "%20" );
>       uri = StringUtils.replace( uri, "%2F", "/" );
>
>       return uri;
>   }
>
>   /**
>    * Returns the URL pattern for a supplied wiki request context.
>    * @param context the wiki context
>    * @param name the wiki page
>    * @return A pattern for replacement.
>    * @throws IllegalArgumentException if the context cannot be found
>    */
>   public static String getURLPattern( String context, String name )
>       throws IllegalArgumentException
>   {
>       if( context.equals(WikiContext.VIEW) )
>       {
>           if( name == null ) return "%uWiki.jsp"; // FIXME
>       }
>
>       // Find the action matching our pattern (could throw exception)
>       Command command = CommandResolver.findCommand( context );
>
>       return command.getURLPattern();
>   }
>
>   /**
>    *  Constructs the actual URL based on the context.
>    */
>   private String makeURL( String context,
>                           String name,
>                           boolean absolute )
>   {
>       return doReplacement( getURLPattern(context,name), name,  
> absolute );
>   }
>
>   /**
>    *  Constructs the URL with a bunch of parameters.
>    *  @param parameters If null or empty, no parameters are added.
>    *
>    *  {@inheritDoc}
>    */
>   public String makeURL( String context,
>                          String name,
>                          boolean absolute,
>                          String parameters )
>   {
>       if( parameters != null && parameters.length() > 0 )
>       {
>           if( context.equals(WikiContext.ATTACH) )
>           {
>               parameters = "?"+parameters;
>           }
>           else if( context.equals(WikiContext.NONE) )
>           {
>               parameters = (name.indexOf('?') != -1 ) ? "&amp;" :  
> "?" + parameters;
>           }
>           else
>           {
>               parameters = "&amp;"+parameters;
>           }
>       }
>       else
>       {
>           parameters = "";
>       }
>       return makeURL( context, name, absolute )+parameters;
>   }
>
>   /**
>    *  Should parse the "page" parameter from the actual
>    *  request.
>    *
>    *  {@inheritDoc}
>    */
>   public String parsePage( String context,
>                            HttpServletRequest request,
>                            String encoding )
>       throws UnsupportedEncodingException
>   {
>       request.setCharacterEncoding( encoding );
>       String pagereq = request.getParameter( "page" );
>
>       if( context.equals(WikiContext.ATTACH) )
>       {
>           pagereq = parsePageFromURL( request, encoding );
>       }
>       else pagereq = TextUtil.urlDecode( pagereq, encoding );
>   log.debug("parsePage: "+encoding+":"+pagereq);
>
>       return pagereq;
>   }
>
>   /**
>    *  There's a bug in Tomcat until 5.5.16 at least: The "+" sign is  
> not
>    *  properly decoded by the servlet container, and therefore  
> request.getPathInfo()
>    *  will return faulty results for paths which contains + signs to  
> signify spaces.
>    *  <p>
>    *  This method provides a workaround by simply parsing the  
> getRequestURI(), which
>    *  is returned from the servlet container undedecoded.
>    *  <p>
>    *  Please see <a href="http://issues.apache.org/bugzilla/show_bug.cgi?id=39278 
> ">Tomcat Bug 39278</a>
>    *  for more information.
>    *
>    *  @param request A HTTP servlet request
>    *  @param encoding The used encoding
>    *  @return a String, decoded by JSPWiki, specifying extra path  
> information that comes
>    *          after the servlet path but before the query string in  
> the request URL;
>    *          or null if the URL does not have any extra path  
> information
>    *  @throws UnsupportedEncodingException
>    */
>   /*
>   private static String getPathInfo( HttpServletRequest request,  
> String encoding )
>       throws UnsupportedEncodingException
>   {
>       String c = request.getContextPath(); // Undecoded
>       String s = request.getServletPath(); // Decoded
>       String u = request.getRequestURI();  // Undecoded
>
>       c = URLDecoder.decode( c, encoding );
>       u = URLDecoder.decode( u, encoding );
>
>       String pi = u.substring( s.length()+c.length() );
>
>       if( pi.length() == 0 ) pi = null;
>
>       return pi;
>   }
>   */
>   /**
>    *  Takes the name of the page from the request URI.
>    *  The initial slash is also removed.  If there is no page,
>    *  returns null.
>    *
>    *  @param request The request to parse
>    *  @param encoding The encoding to use
>    *
>    *  @return a parsed page name, or null, if it cannot be found
>    *
>    *  @throws UnsupportedEncodingException If the encoding is not  
> recognized.
>    */
>   public static String parsePageFromURL( HttpServletRequest request,
>                                          String encoding )
>       throws UnsupportedEncodingException
>   {
>       String name = request.getPathInfo();
>
>       if( name == null || name.length() <= 1 )
>       {
>           return null;
>       }
>       else if( name.charAt(0) == '/' )
>       {
>           name = name.substring(1);
>       }
>
>       //
>       //  This is required, because by default all URLs are handled
>       //  as Latin1, even if they are really UTF-8.
>       //
>       name = TextUtil.urlDecode( name, encoding );
>   log.debug("parsePageFromURL: "+encoding+":"+name);
>       return name;
>   }
>
>
>   /**
>    *  This method is not needed for the DefaultURLConstructor.
>    *
>    *  @return {@inheritDoc}
>    *  @param {@inheritDoc}
>    */
>   public String getForwardPage( HttpServletRequest request )
>   {
>       return request.getPathInfo();
>   }
> }
> /*
>   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.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          m_styleStack   = new Stack();
>
>    // 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      m_localLinkMutatorChain    = new ArrayList();
>   private ArrayList      m_externalLinkMutatorChain = new ArrayList();
>   private ArrayList      m_attachmentLinkMutatorChain = new ArrayList 
> ();
>   private ArrayList      m_headingListenerChain     = new ArrayList();
>
>   /** Keeps image regexp Patterns */
>   private ArrayList      m_inlineImagePatterns;
>
>   private PatternMatcher m_inlineMatcher = new Perl5Matcher();
>
>   private ArrayList      m_linkMutators = new ArrayList();
>
>   /**
>    *  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  ptrnlist = new ArrayList();
>
>       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 ] -&gt; 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( !(ch >= 0x80 ||  // All non ASCII are allowed!
>                 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.getURL( WikiContext.ATTACH,  
> 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 {
>         int cutpt = link.lastIndexOf('/');
>         if( cutpt != -1 ) {
>           if ( (cutpt+1) < link.length() ) { // Attachment name does  
> not start with a blank!
>             if ( link.charAt(cutpt+1) == ' ') cutpt = -1;
>           }
>           if( cutpt != -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 ? "<" : "&lt;";
>               break;
>
>             case '>':
>               s = m_allowHTML ? ">" : "&gt;";
>               break;
>
>             case '\"':
>               s = m_allowHTML ? "\"" : "&quot;";
>               break;
>
>               /*
>             case '&':
>               s = "&amp;";
>               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 = new WikiContext( m_engine,
>                                                            
> 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("&lt;");
>           }
>           else if( ch == '>' )
>           {
>               buf.append("&gt;");
>           }
>           else if( ch == '&' )
>           {
>               buf.append("&amp;");
>           }
>           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( String context, String link )
>       {
>           return m_context.getURL( context,
>                                    link,
>                                    null );
>       }
>
>       /**
>        *  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 
> (WikiContext.VIEW,
>                                                               link) 
> +section+"\">"+text+"</a>";
>               break;
>
>             case EDIT:
>               result = "<a class=\"createpage\" title=\"Create  
> '"+link+"'\" href=\""+
>                        getURL(WikiContext.EDIT, 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(WikiContext.VIEW,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( WikiContext.ATTACH,
>                                        link );
>
>               String infolink = getURL( WikiContext.INFO,
>                                         link );
>
>               String imglink = getURL( WikiContext.NONE,
>                                        "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( WikiContext.NONE,"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;
>       }
>
>       public String makeLink( int type, String link, String text,  
> String section )
>       {
>           return text;
>       }
>
>       /**
>        *  Writes HTML for error message.
>        */
>
>       public String makeError( String error )
>       {
>           return "ERROR: "+error;
>       }
>
>       /**
>        *  Emits a vertical line.
>        */
>
>       public String makeRuler()
>       {
>           return "----------------------------------";
>       }
>
>       /**
>        *  Returns XHTML for the start of the heading.  Also sets the
>        *  line-end emitter.
>        *  @param level
>        */
>       public String makeHeading( int level, String title, Heading hd )
>       {
>           String res = "";
>
>           title = title.trim();
>
>           hd.m_level = level;
>           hd.m_titleText = title;
>           hd.m_titleSection = "";
>           hd.m_titleAnchor = "";
>
>           switch( level )
>           {
>             case Heading.HEADING_SMALL:
>               res = title;
>               m_closeTag = "\n\n";
>               break;
>
>             case Heading.HEADING_MEDIUM:
>               res = title;
>               m_closeTag = "\n"+TextUtil.repeatString 
> ("-",title.length())+"\n\n";
>               break;
>
>             case Heading.HEADING_LARGE:
>               res = title.toUpperCase();
>               m_closeTag= "\n"+TextUtil.repeatString("=",title.length 
> ())+"\n\n";
>               break;
>           }
>
>           return res;
>       }
>
>       /**
>        *  @param bullet A character detailing which kind of a list
>        *  we are dealing with here.  Options are '#' and '*'.
>        */
>       // FIXME: Should really start a different kind of list depending
>       //        on the bullet type
>       public String openList( char bullet )
>       {
>           return "\n";
>       }
>
>       public String openListItem()
>       {
>           return "- ";
>       }
>
>       public String closeListItem()
>       {
>           return "\n";
>       }
>
>       /**
>        *  @param bullet A character detailing which kind of a list
>        *  we are dealing with here.  Options are '#' and '*'.
>        */
>       public String closeList( char bullet )
>       {
>           return "\n\n";
>       }
>
>       public String openTable()
>       {
>           return "\n";
>       }
>
>       public String closeTable()
>       {
>           return "\n";
>       }
>
>       public String openTableRow()
>       {
>           return "";
>       }
>
>       public String closeTableRow()
>       {
>           return "\n";
>       }
>
>       public String openTableItem()
>       {
>           return "\t";
>       }
>
>       public String closeTableItem()
>       {
>           return "";
>       }
>
>       public String openTableHeading()
>       {
>           return "\t";
>       }
>
>       public String closeTableHeading()
>       {
>           return "";
>       }
>
>       public String openPreformatted( boolean isBlock )
>       {
>           return "";
>       }
>
>       public String closePreformatted()
>       {
>           return "\n";
>       }
>
>       /**
>        *  If outlink images are turned on, returns a link to the  
> outward
>        *  linking image.
>        */
>       public String outlinkImage()
>       {
>           return "";
>       }
>
>       /**
>        *  @param clear If true, then flushes all thingies.
>        */
>       public String lineBreak( boolean clear )
>       {
>           return "\n";
>       }
>
>   } // TextRenderer
> }
> f the heading.  Also sets the
>        *  line-end emitter.
>        *  @param level
>        */
>       public String makeHeading( int level, String title, Heading hd )
>       {
>           String res = "";
>
>           title = title.trim();
>
>           hd.m_level = level;
>           hd.m_titleText = title;
>           hd.m_titleSection = "";
>           hd.m_titleAnchor = "";
>
>           switch( level )
>           {
>             case Heading.HEADING_SMALL:
>               res = title;
>               m_closeTag = "\n\n";
>               break;
>
>             case Heading.HEADING_MEDIUM:
>               res = title;
>               m_closeTag = "\n"+TextUtil.repeatString 
> ("-",title.length())+"\n\n";
>               break;
>
>             case Heading.HEADING_LARGE:
>               res = title.toUpperCase();
>               m_closeTag= "\n"+TextUtil.repeatString("=",title.length 
> ())+"\n\n";
>               break;
>           }
>
>           return res;
>       }
>
>       /**
>        *  @param bullet A character detailing which kind of a list
>        *  we are dealing with here.  Options are '#' and '*'.
>        */
>       // FIXME: Should really start a different kind of list depending
>       //        on the bullet type
>       public String openList( char bullet )
>       {
>           return "\n";
>       }
>
>       public String openListItem()
>       {
>           return "- ";
>       }
>
>       public String closeListItem()
>       {
>           return "\n";
>       }
>
>       /**
>        *  @param bullet A character detailing which kind of a list
>        *  we are dealing with here.  Options are '#' and '*'.
>        */
>       public String closeList( char bullet )
>       {
>           return "\n\n";
>       }
>
>       public String openTable()
>       {
>           return "\n";
>       }
>
>       public String closeTable()
>       {
>           return "\n";
>       }
>
>       public String openTableRow()
>       {
>           return "";
>       }
>
>       public String closeTableRow()
>       {
>           return "\n";
>       }
>
>       public String openTableItem()
>       {
>           return "\t";
>       }
>
>       public String closeTableItem()
>       {
>           return "";
>       }
>
>       public String openTableHeading()
>       {
>           return "\t";
>       }
>
>       public String closeTableHeading()
>       {
>           return "";
>       }
>
>       public String openPreformatted( boolean isBlock )
>       {
>           return "";
>       }
>
>       public String closePreformatted()
>       {
>           return "\n";
>       }
>
>       /**
>        *  If outlink images are turned on, returns a link to the  
> outward
>        *  linking image.
>        */
>       public String outlinkImage()
>       {
>           return "";
>       }
>
>       /**
>        *  @param clear If true, then flushes all thingies.
>        */
>       public String lineBreak( boolean clear )
>       {
>           return "\n";
>       }
>
>   } // TextRenderer
> }

Mime
View raw message