You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [24/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/parser/JSPWikiMarkupParser.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/parser/JSPWikiMarkupParser.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/parser/JSPWikiMarkupParser.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/parser/JSPWikiMarkupParser.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,2924 @@
+/*
+    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.parser;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.text.MessageFormat;
+import java.util.*;
+
+import javax.xml.transform.Result;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+import org.apache.oro.text.GlobCompiler;
+import org.apache.oro.text.regex.*;
+import org.jdom.*;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.*;
+import com.ecyrd.jspwiki.attachment.Attachment;
+import com.ecyrd.jspwiki.attachment.AttachmentManager;
+import com.ecyrd.jspwiki.auth.WikiSecurityException;
+import com.ecyrd.jspwiki.auth.acl.Acl;
+import com.ecyrd.jspwiki.i18n.InternationalizationManager;
+import com.ecyrd.jspwiki.plugin.PluginException;
+import com.ecyrd.jspwiki.plugin.PluginManager;
+import com.ecyrd.jspwiki.plugin.WikiPlugin;
+import com.ecyrd.jspwiki.providers.ProviderException;
+import com.ecyrd.jspwiki.render.CleanTextRenderer;
+import com.ecyrd.jspwiki.render.RenderingManager;
+
+/**
+ *  Parses JSPWiki-style markup into a WikiDocument DOM tree.  This class is the
+ *  heart and soul of JSPWiki : make sure you test properly anything that is added,
+ *  or else it breaks down horribly.
+ *
+ *  @author Janne Jalkanen
+ *  @since  2.4
+ */
+public class JSPWikiMarkupParser
+    extends MarkupParser
+{
+    /** Name of the outlink image; relative path to the JSPWiki directory. */
+    private static final String OUTLINK_IMAGE = "images/out.png";
+
+    /** The value for anchor element <tt>class</tt> attributes when used
+      * for wiki page (normal) links. The value is "wikipage". */
+    public static final String CLASS_WIKIPAGE = "wikipage";
+
+    /** The value for anchor element <tt>class</tt> attributes when used
+      * for edit page links. The value is "createpage". */
+    public static final String CLASS_EDITPAGE = "createpage";
+
+    /** The value for anchor element <tt>class</tt> attributes when used
+      * for interwiki page links. The value is "interwiki". */
+    public static final String CLASS_INTERWIKI = "interwiki";
+
+    protected static final int              READ          = 0;
+    protected static final int              EDIT          = 1;
+    protected static final int              EMPTY         = 2;  // Empty message
+    protected static final int              LOCAL         = 3;
+    protected static final int              LOCALREF      = 4;
+    protected static final int              IMAGE         = 5;
+    protected static final int              EXTERNAL      = 6;
+    protected static final int              INTERWIKI     = 7;
+    protected static final int              IMAGELINK     = 8;
+    protected static final int              IMAGEWIKILINK = 9;
+    protected static final int              ATTACHMENT    = 10;
+
+    private static Logger log = Logger.getLogger( JSPWikiMarkupParser.class );
+
+    private boolean        m_isbold       = false;
+    private boolean        m_isitalic     = false;
+    private boolean        m_istable      = false;
+    private boolean        m_isPre        = false;
+    private boolean        m_isEscaping   = false;
+    private boolean        m_isdefinition = false;
+    private boolean        m_isPreBlock   = 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(10);  // stores the # and * pattern
+    private boolean        m_allowPHPWikiStyleLists = true;
+
+
+    private boolean        m_isOpenParagraph = false;
+
+    /** Keeps image regexp Patterns */
+    private List           m_inlineImagePatterns;
+
+    /** Parser for extended link functionality. */
+    private LinkParser     m_linkParser = new LinkParser();
+
+    private PatternMatcher m_inlineMatcher = new Perl5Matcher();
+
+    /** Keeps track of any plain text that gets put in the Text nodes */
+    private StringBuffer   m_plainTextBuf = new StringBuffer(20);
+
+    private Element        m_currentElement;
+
+    /**
+     *  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 true, all outward attachment info links have a small link image appended. */
+    public static final String     PROP_USEATTACHMENTIMAGE = "jspwiki.translatorReader.useAttachmentImage";
+
+    /** If set to "true", all external links are tagged with 'rel="nofollow"' */
+    public static final String     PROP_USERELNOFOLLOW   = "jspwiki.translatorReader.useRelNofollow";
+
+    private static final Map<String,String> NO_PARAMS = Collections.unmodifiableMap( new HashMap<String,String>() );
+
+    /** If true, then considers CamelCase links as well. */
+    private boolean                m_camelCaseLinks      = false;
+
+    /** If true, then generate special output for wysiwyg editing in certain cases */
+    private boolean                m_wysiwygEditorMode     = 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;
+
+    private boolean                m_useAttachmentImage  = true;
+
+    /** If true, allows raw HTML. */
+    private boolean                m_allowHTML           = false;
+
+    private boolean                m_useRelNofollow      = false;
+
+    private PatternCompiler        m_compiler = new Perl5Compiler();
+
+    static final String WIKIWORD_REGEX = "(^|[[:^alnum:]]+)([[:upper:]]+[[:lower:]]+[[:upper:]]+[[:alnum:]]*|(http://|https://|mailto:)([A-Za-z0-9_/\\.\\+\\?\\#\\-\\@=&;~%]+))";
+
+    private PatternMatcher         m_camelCaseMatcher = new Perl5Matcher();
+    private Pattern                m_camelCasePattern;
+
+    private int                    m_rowNum              = 1;
+
+
+    /**
+     *  The default inlining pattern.  Currently "*.png"
+     */
+    public static final String     DEFAULT_INLINEPATTERN = "*.png";
+
+    /**
+     *  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.
+     *
+     *  This array is sorted during class load, so you can just dump
+     *  here whatever you want in whatever order you want.
+     */
+    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:" };
+
+    private static final String INLINE_IMAGE_PATTERNS = "JSPWikiMarkupParser.inlineImagePatterns";
+
+    private static final String CAMELCASE_PATTERN     = "JSPWikiMarkupParser.camelCasePattern";
+
+    private static final String[] CLASS_TYPES =
+    {
+       CLASS_WIKIPAGE,
+       CLASS_EDITPAGE,
+       "",
+       "footnote",
+       "footnoteref",
+       "",
+       "external",
+       CLASS_INTERWIKI,
+       "external",
+       CLASS_WIKIPAGE,
+       "attachment"
+    };
+
+
+    /**
+     *  This Comparator is used to find an external link from c_externalLinks.  It
+     *  checks if the link starts with the other arraythingie.
+     */
+    private static Comparator c_startingComparator = new StartingComparator();
+
+    static
+    {
+        Arrays.sort( c_externalLinks );
+    }
+
+    /**
+     *  Creates a markup parser.
+     */
+    public JSPWikiMarkupParser( WikiContext context, Reader in )
+    {
+        super( context, in );
+        initialize();
+    }
+
+    /**
+     *  @param m_engine The WikiEngine this reader is attached to.  Is
+     * used to figure out of a page exits.
+     */
+
+    // FIXME: parsers should be pooled for better performance.
+    private void initialize()
+    {
+        PatternCompiler compiler         = new GlobCompiler();
+        List            compiledpatterns;
+
+        //
+        //  We cache compiled patterns in the engine, since their creation is
+        //  really expensive
+        //
+        compiledpatterns = (List)m_engine.getAttribute( INLINE_IMAGE_PATTERNS );
+
+        if( compiledpatterns == null )
+        {
+            compiledpatterns = new ArrayList(20);
+            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(),
+                                                            GlobCompiler.DEFAULT_MASK|GlobCompiler.READ_ONLY_MASK ) );
+                }
+                catch( MalformedPatternException e )
+                {
+                    log.error("Malformed pattern in properties: ", e );
+                }
+            }
+
+            m_engine.setAttribute( INLINE_IMAGE_PATTERNS, compiledpatterns );
+        }
+
+        m_inlineImagePatterns = Collections.unmodifiableList(compiledpatterns);
+
+        m_camelCasePattern = (Pattern) m_engine.getAttribute( CAMELCASE_PATTERN );
+        if( m_camelCasePattern == null )
+        {
+            try
+            {
+                m_camelCasePattern = m_compiler.compile( WIKIWORD_REGEX,
+                                                         Perl5Compiler.DEFAULT_MASK|Perl5Compiler.READ_ONLY_MASK );
+            }
+            catch( MalformedPatternException e )
+            {
+                log.fatal("Internal error: Someone put in a faulty pattern.",e);
+                throw new InternalWikiException("Faulty camelcasepattern in TranslatorReader");
+            }
+            m_engine.setAttribute( CAMELCASE_PATTERN, m_camelCasePattern );
+        }
+        //
+        //  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 );
+        }
+
+
+
+        Boolean wysiwygVariable = (Boolean)m_context.getVariable( RenderingManager.WYSIWYG_EDITOR_MODE );
+        if( wysiwygVariable != null )
+        {
+            m_wysiwygEditorMode = wysiwygVariable.booleanValue();
+        }
+
+        m_plainUris           = getLocalBooleanProperty( m_context,
+                                                         props,
+                                                         PROP_PLAINURIS,
+                                                         m_plainUris );
+        m_useOutlinkImage     = getLocalBooleanProperty( m_context,
+                                                         props,
+                                                         PROP_USEOUTLINKIMAGE,
+                                                         m_useOutlinkImage );
+        m_useAttachmentImage  = getLocalBooleanProperty( m_context,
+                                                         props,
+                                                         PROP_USEATTACHMENTIMAGE,
+                                                         m_useAttachmentImage );
+        m_allowHTML           = getLocalBooleanProperty( m_context,
+                                                         props,
+                                                         MarkupParser.PROP_ALLOWHTML,
+                                                         m_allowHTML );
+
+        m_useRelNofollow      = getLocalBooleanProperty( m_context,
+                                                         props,
+                                                         PROP_USERELNOFOLLOW,
+                                                         m_useRelNofollow );
+
+        if( m_engine.getUserManager().getUserDatabase() == null || m_engine.getAuthorizationManager() == null )
+        {
+            disableAccessRules();
+        }
+
+        m_context.getPage().setHasMetadata();
+    }
+
+    /**
+     *  This is just a simple helper method which will first check the context
+     *  if there is already an override in place, and if there is not,
+     *  it will then check the given properties.
+     *
+     *  @param context WikiContext to check first
+     *  @param props   Properties to check next
+     *  @param key     What key are we searching for?
+     *  @param defValue Default value for the boolean
+     *  @return True or false
+     */
+    private static boolean getLocalBooleanProperty( WikiContext context,
+                                                    Properties  props,
+                                                    String      key,
+                                                    boolean     defValue )
+    {
+        Object bool = context.getVariable(key);
+
+        if( bool != null )
+        {
+            return TextUtil.isPositive( (String) bool );
+        }
+
+        return TextUtil.getBooleanProperty( props, key, defValue );
+    }
+
+    /**
+     *  Figure out which image suffixes should be inlined.
+     *  @return Collection of Strings with patterns.
+     */
+
+    // FIXME: Does not belong here; should be elsewhere
+    public 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 = TextUtil.getStringProperty( props, name, null );
+
+                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.
+     */
+
+    protected 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;
+    }
+
+    /**
+     * Calls the heading listeners.
+     *
+     * @param param A Heading object.
+     */
+    protected 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 );
+        }
+    }
+
+    /**
+     *  Creates a JDOM anchor element.  Can be overridden to change the URL creation,
+     *  if you really know what you are doing.
+     *
+     *  @param type One of the types above
+     *  @param link URL to which to link to
+     *  @param text Link text
+     *  @param section If a particular section identifier is required.
+     *  @return An A element.
+     *  @since 2.4.78
+     */
+    protected Element createAnchor(int type, String link, String text, String section)
+    {
+        text = escapeHTMLEntities( text );
+        section = escapeHTMLEntities( section );
+        Element el = new Element("a");
+        el.setAttribute("class",CLASS_TYPES[type]);
+        el.setAttribute("href",link+section);
+        el.addContent(text);
+        return el;
+    }
+
+    private Element makeLink( int type, String link, String text, String section, Iterator attributes )
+    {
+        Element el = null;
+
+        if( text == null ) text = link;
+
+        text = callMutatorChain( m_linkMutators, text );
+
+        section = (section != null) ? ("#"+section) : "";
+
+        // Make sure we make a link name that can be accepted
+        // as a valid URL.
+
+        if( link.length() == 0 )
+        {
+            type = EMPTY;
+        }
+        ResourceBundle rb = m_context.getBundle(InternationalizationManager.CORE_BUNDLE);
+        Object[] args = { link };
+        boolean makeAbsoluteLinks = "absolute".equals( m_context.getVariable( WikiEngine.PROP_REFSTYLE )  );
+
+        switch(type)
+        {
+            case READ:
+                el = createAnchor( READ, m_context.getContext().getURL(ViewActionBean.class, link), text, section );
+                break;
+
+            case EDIT:
+                el = createAnchor( EDIT, m_context.getContext().getURL( EditActionBean.class, link, NO_PARAMS, makeAbsoluteLinks ), text, "" ); 
+                el.setAttribute("title", MessageFormat.format( rb.getString( "markupparser.link.create" ), args ) );
+                break;
+
+            case EMPTY:
+                el = new Element("u").addContent(text);
+                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:
+                el = createAnchor( LOCALREF, "#ref-"+m_context.getName()+"-"+link, "["+text+"]", "" );
+                break;
+
+            case LOCAL:
+                el = new Element("a").setAttribute("class","footnote");
+                el.setAttribute("name", "ref-"+m_context.getName()+"-"+link.substring(1));
+                el.addContent("["+text+"]");
+                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:
+                el = new Element("img").setAttribute("class","inline");
+                el.setAttribute("src",link);
+                el.setAttribute("alt",text);
+                break;
+
+            case IMAGELINK:
+                el = new Element("img").setAttribute("class","inline");
+                el.setAttribute("src",link);
+                el.setAttribute("alt",text);
+                el = createAnchor(IMAGELINK,text,"","").addContent(el);
+                break;
+
+            case IMAGEWIKILINK:
+                String pagelink = m_context.getContext().getURL(ViewActionBean.class,text);
+                el = new Element("img").setAttribute("class","inline");
+                el.setAttribute("src",link);
+                el.setAttribute("alt",text);
+                el = createAnchor(IMAGEWIKILINK,pagelink,"","").addContent(el);
+                break;
+
+            case EXTERNAL:
+                el = createAnchor( EXTERNAL, link, text, section );
+                if( m_useRelNofollow ) el.setAttribute("rel","nofollow");
+                break;
+
+            case INTERWIKI:
+                el = createAnchor( INTERWIKI, link, text, section );
+                break;
+
+            case ATTACHMENT:
+                String attlink = m_context.getContext().getURL( AttachActionBean.class,
+                                                   link, NO_PARAMS, makeAbsoluteLinks );
+
+                String infolink = m_context.getContext().getURL( PageInfoActionBean.class,
+                                                    link, NO_PARAMS, makeAbsoluteLinks );
+
+                String imglink = m_context.getContext().getURL( NoneActionBean.class,
+                                                   "images/attachment_small.png", NO_PARAMS, makeAbsoluteLinks );
+
+                el = createAnchor( ATTACHMENT, attlink, text, "" );
+
+                pushElement(el);
+                popElement(el.getName());
+
+                if( m_useAttachmentImage )
+                {
+                    el = new Element("img").setAttribute("src",imglink);
+                    el.setAttribute("border","0");
+                    el.setAttribute("alt","(info)");
+
+                    el = new Element("a").setAttribute("href",infolink).addContent(el);
+                }
+                else
+                {
+                    el = null;
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        if( el != null && attributes != null )
+        {
+            while( attributes.hasNext() )
+            {
+                Attribute attr = (Attribute)attributes.next();
+                if( attr != null )
+                {
+                    el.setAttribute(attr);
+                }
+            }
+        }
+
+        if( el != null )
+        {
+            flushPlainText();
+            m_currentElement.addContent( el );
+        }
+        return el;
+    }
+
+
+    /**
+     *  Figures out if a link is an off-site link.  This recognizes
+     *  the most common protocols by checking how it starts.
+     *
+     *  @since 2.4
+     */
+
+    public static boolean isExternalLink( String link )
+    {
+        int idx = Arrays.binarySearch( c_externalLinks, link,
+                                       c_startingComparator );
+
+        //
+        //  We need to check here once again; otherwise we might
+        //  get a match for something like "h".
+        //
+        if( idx >= 0 && link.startsWith(c_externalLinks[idx]) ) 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 )
+        {
+            link = link.toLowerCase();
+
+            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");
+    }
+
+    /**
+     *  These are all of the HTML 4.01 block-level elements.
+     */
+    private static final String[] BLOCK_ELEMENTS = {
+        "address", "blockquote", "div", "dl", "fieldset", "form",
+        "h1", "h2", "h3", "h4", "h5", "h6",
+        "hr", "noscript", "ol", "p", "pre", "table", "ul"
+    };
+
+    private static final boolean isBlockLevel( String name )
+    {
+        return Arrays.binarySearch( BLOCK_ELEMENTS, name ) >= 0;
+    }
+
+    /**
+     *  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();
+
+        if( s.length() > PUSHBACK_BUFFER_SIZE )
+        {
+            log.warn("Line is longer than maximum allowed size ("+PUSHBACK_BUFFER_SIZE+" characters.  Attempting to recover...");
+            pushBack( s.substring(0,PUSHBACK_BUFFER_SIZE-1) );
+        }
+        else
+        {
+            try
+            {
+                pushBack( s );
+            }
+            catch( IOException e )
+            {
+                log.warn("Pushback failed: the line is probably too long.  Attempting to recover.");
+            }
+        }
+        return s;
+    }
+
+
+    /**
+     *  Writes HTML for error message.
+     */
+
+    public static Element makeError( String error )
+    {
+        return new Element("span").setAttribute("class","error").addContent(error);
+    }
+
+    private int flushPlainText()
+    {
+        int numChars = m_plainTextBuf.length();
+
+        if( numChars > 0 )
+        {
+            String buf;
+
+            if( !m_allowHTML )
+            {
+                buf = escapeHTMLEntities(m_plainTextBuf.toString());
+            }
+            else
+            {
+                buf = m_plainTextBuf.toString();
+            }
+            //
+            //  We must first empty the buffer because the side effect of
+            //  calling makeCamelCaseLink() is to call this routine.
+            //
+
+            m_plainTextBuf = new StringBuffer(20);
+
+            try
+            {
+                //
+                //  This is the heaviest part of parsing, and therefore we can
+                //  do some optimization here.
+                //
+                //  1) Only when the length of the buffer is big enough, we try to do the match
+                //
+
+                if( m_camelCaseLinks && !m_isEscaping && buf.length() > 3 )
+                {
+                    // System.out.println("Buffer="+buf);
+
+                    while( m_camelCaseMatcher.contains( buf, m_camelCasePattern ) )
+                    {
+                        MatchResult result = m_camelCaseMatcher.getMatch();
+
+                        String firstPart = buf.substring(0,result.beginOffset(0));
+                        String prefix = result.group(1);
+
+                        if( prefix == null ) prefix = "";
+
+                        String camelCase = result.group(2);
+                        String protocol  = result.group(3);
+                        String uri       = protocol+result.group(4);
+                        buf              = buf.substring(result.endOffset(0));
+
+                        m_currentElement.addContent( firstPart );
+
+                        //
+                        //  Check if the user does not wish to do URL or WikiWord expansion
+                        //
+                        if( prefix.endsWith("~") || prefix.indexOf('[') != -1 )
+                        {
+                            if( prefix.endsWith("~") )
+                            {
+                                if( m_wysiwygEditorMode )
+                                {
+                                    m_currentElement.addContent( "~" );
+                                }
+                                prefix = prefix.substring(0,prefix.length()-1);
+                            }
+                            if( camelCase != null )
+                            {
+                                m_currentElement.addContent( prefix+camelCase );
+                            }
+                            else if( protocol != null )
+                            {
+                                m_currentElement.addContent( prefix+uri );
+                            }
+                            continue;
+                        }
+
+                        //
+                        //  Fine, then let's check what kind of a link this was
+                        //  and emit the proper elements
+                        //
+                        if( protocol != null )
+                        {
+                            char c = uri.charAt(uri.length()-1);
+                            if( c == '.' || c == ',' )
+                            {
+                                uri = uri.substring(0,uri.length()-1);
+                                buf = c + buf;
+                            }
+                            // System.out.println("URI match "+uri);
+                            m_currentElement.addContent( prefix );
+                            makeDirectURILink( uri );
+                        }
+                        else
+                        {
+                            // System.out.println("Matched: '"+camelCase+"'");
+                            // System.out.println("Split to '"+firstPart+"', and '"+buf+"'");
+                            // System.out.println("prefix="+prefix);
+                            m_currentElement.addContent( prefix );
+
+                            makeCamelCaseLink( camelCase );
+                        }
+                    }
+
+                    m_currentElement.addContent( buf );
+                }
+                else
+                {
+                    //
+                    //  No camelcase asked for, just add the elements
+                    //
+                    m_currentElement.addContent( buf );
+                }
+            }
+            catch( IllegalDataException e )
+            {
+                //
+                // Sometimes it's possible that illegal XML chars is added to the data.
+                // Here we make sure it does not stop parsing.
+                //
+                m_currentElement.addContent( makeError(cleanupSuspectData( e.getMessage() )) );
+            }
+        }
+
+        return numChars;
+    }
+
+    /**
+     *  Escapes XML entities in a HTML-compatible way (i.e. does not escape
+     *  entities that are already escaped).
+     *
+     *  @param buf
+     *  @return
+     */
+    private String escapeHTMLEntities(String buf)
+    {
+        StringBuffer tmpBuf = new StringBuffer( buf.length() + 20 );
+
+        for( int i = 0; i < buf.length(); i++ )
+        {
+            char ch = buf.charAt(i);
+
+            if( ch == '<' )
+            {
+                tmpBuf.append("&lt;");
+            }
+            else if( ch == '>' )
+            {
+                tmpBuf.append("&gt;");
+            }
+            else if( ch == '\"' )
+            {
+                tmpBuf.append("&quot;");
+            }
+            else if( ch == '&' )
+            {
+                for( int j = (i < buf.length()-1 ) ? i+1 : i; j < buf.length(); j++ )
+                {
+                    int ch2 = buf.charAt(j);
+                    if( ch2 == ';' )
+                    {
+                        tmpBuf.append(ch);
+                        break;
+                    }
+                    if( ch2 != '#' && !Character.isLetterOrDigit( (char)ch2) )
+                    {
+                        tmpBuf.append("&amp;"); break;
+                    }
+                }
+            }
+            else
+            {
+                tmpBuf.append( ch );
+            }
+        }
+
+        return tmpBuf.toString();
+    }
+
+    private Element pushElement( Element e )
+    {
+        flushPlainText();
+        m_currentElement.addContent( e );
+        m_currentElement = e;
+
+        return e;
+    }
+
+    private Element addElement( Content e )
+    {
+        if( e != null )
+        {
+            flushPlainText();
+            m_currentElement.addContent( e );
+        }
+        return m_currentElement;
+    }
+
+    /**
+     *  All elements that can be empty by the HTML DTD.
+     */
+    //  Keep sorted.
+    private static final String[] EMPTY_ELEMENTS = {
+        "area", "base", "br", "col", "hr", "img", "input", "link", "meta", "p", "param"
+    };
+
+    /**
+     *  Goes through the current element stack and pops all elements until this
+     *  element is found - this essentially "closes" and element.
+     *
+     *  @param s
+     *  @return The new current element, or null, if there was no such element in the entire stack.
+     */
+    private Element popElement( String s )
+    {
+        int flushedBytes = flushPlainText();
+
+        Element currEl = m_currentElement;
+
+        while( currEl.getParentElement() != null )
+        {
+            if( currEl.getName().equals(s) && !currEl.isRootElement() )
+            {
+                m_currentElement = currEl.getParentElement();
+
+                //
+                //  Check if it's okay for this element to be empty.  Then we will
+                //  trick the JDOM generator into not generating an empty element,
+                //  by putting an empty string between the tags.  Yes, it's a kludge
+                //  but what'cha gonna do about it. :-)
+                //
+
+                if( flushedBytes == 0 && Arrays.binarySearch( EMPTY_ELEMENTS, s ) < 0 )
+                {
+                    currEl.addContent("");
+                }
+
+                return m_currentElement;
+            }
+
+            currEl = currEl.getParentElement();
+        }
+
+        return null;
+    }
+
+
+    /**
+     *  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( 80 );
+        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( 80 );
+        int ch = nextToken();
+
+        while( ch != -1 )
+        {
+            if( endChars.indexOf((char)ch) == -1 )
+            {
+                pushBack( ch );
+                break;
+            }
+
+            sb.append( (char) ch );
+            ch = nextToken();
+        }
+
+        return sb.toString();
+    }
+
+    private JSPWikiMarkupParser m_cleanTranslator;
+
+    /**
+     *  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 JSPWikiMarkupParser getCleanTranslator()
+    {
+        if( m_cleanTranslator == null )
+        {
+            WikiContext dummyContext = m_engine.getWikiActionBeanFactory().newViewActionBean(
+                                                           m_context.getContext().getRequest(),
+                                                           m_context.getContext().getResponse(),
+                                                           m_context.getPage() );            
+            m_cleanTranslator = new JSPWikiMarkupParser( dummyContext, null );
+
+            m_cleanTranslator.m_allowHTML = true;
+        }
+
+        return m_cleanTranslator;
+    }
+    /**
+     *  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 '_'.
+     */
+    // FIXME: This method should probably be public and in an util class somewhere
+    private String makeHeadingAnchor( String baseName, String title, Heading hd )
+    {
+        hd.m_titleText = title;
+        title = MarkupParser.wikifyLink( 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( '%', '_' );
+        hd.m_titleAnchor = hd.m_titleAnchor.replace( '/', '_' );
+        return hd.m_titleAnchor;
+    }
+
+    private String makeSectionTitle( String title )
+    {
+        title = title.trim();
+        String outTitle;
+
+        try
+        {
+            JSPWikiMarkupParser dtr = getCleanTranslator();
+            dtr.setInputReader( new StringReader(title) );
+
+            CleanTextRenderer ctt = new CleanTextRenderer(m_context, dtr.parse());
+
+            outTitle = ctt.getString();
+        }
+        catch( IOException e )
+        {
+            log.fatal("CleanTranslator not working", e);
+            throw new InternalWikiException("CleanTranslator not working as expected, when cleaning title"+ e.getMessage() );
+        }
+
+        return outTitle;
+    }
+
+    /**
+     *  Returns XHTML for the start of the heading.  Also sets the
+     *  line-end emitter.
+     *  @param level
+     *  @param title the title for the heading
+     *  @param hd a List to which heading should be added
+     */
+    public Element makeHeading( int level, String title, Heading hd )
+    {
+        Element el = null;
+
+        String pageName = m_context.getPage().getName();
+
+        String outTitle = makeSectionTitle( title );
+
+        hd.m_level = level;
+
+        switch( level )
+        {
+          case Heading.HEADING_SMALL:
+            el = new Element("h4").setAttribute("id",makeHeadingAnchor( pageName, outTitle, hd ));
+            break;
+
+          case Heading.HEADING_MEDIUM:
+            el = new Element("h3").setAttribute("id",makeHeadingAnchor( pageName, outTitle, hd ));
+            break;
+
+          case Heading.HEADING_LARGE:
+            el = new Element("h2").setAttribute("id",makeHeadingAnchor( pageName, outTitle, hd ));
+            break;
+
+          default:
+            throw new InternalWikiException("Illegal heading type "+level);
+        }
+
+        return el;
+    }
+
+    /**
+     *  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 Element makeCamelCaseLink( String wikiname )
+    {
+        String matchedLink;
+
+        callMutatorChain( m_localLinkMutatorChain, wikiname );
+
+        if( (matchedLink = linkExists( wikiname )) != null )
+        {
+            makeLink( READ, matchedLink, wikiname, null, null );
+        }
+        else
+        {
+            makeLink( EDIT, wikiname, wikiname, null, null );
+        }
+
+        return m_currentElement;
+    }
+
+    /** Holds the image URL for the duration of this parser */
+    private String m_outlinkImageURL = null;
+
+    /**
+     *  Returns an element for the external link image (out.png).  However,
+     *  this method caches the URL for the lifetime of this MarkupParser,
+     *  because it's commonly used, and we'll end up with possibly hundreds
+     *  our thousands of references to it...  It's a lot faster, too.
+     *
+     *  @return  An element containing the HTML for the outlink image.
+     */
+    private Element outlinkImage()
+    {
+        Element el = null;
+
+        if( m_useOutlinkImage )
+        {
+            if( m_outlinkImageURL == null )
+            {
+                m_outlinkImageURL = m_context.getContext().getURL( NoneActionBean.class, OUTLINK_IMAGE );
+            }
+
+            el = new Element("img").setAttribute("class", "outlink");
+            el.setAttribute( "src", m_outlinkImageURL );
+            el.setAttribute("alt","");
+        }
+
+        return el;
+    }
+
+    /**
+     *  Takes an URL and turns it into a regular wiki link.  Unfortunately,
+     *  because of the way that flushPlainText() works, it already encodes
+     *  all of the XML entities.  But so does WikiContext.getURL(), so we
+     *  have to do a reverse-replace here, so that it can again be replaced in makeLink.
+     *  <p>
+     *  What a crappy problem.
+     *
+     * @param url
+     * @return
+     */
+    private Element makeDirectURILink( String url )
+    {
+        Element result;
+        String last = null;
+
+        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( StringUtils.replace(url,"&amp;","&"), url, false );
+        }
+        else
+        {
+            result = makeLink( EXTERNAL, StringUtils.replace(url,"&amp;","&"), url, null, null );
+            addElement( outlinkImage() );
+        }
+
+        if( last != null )
+        {
+            m_plainTextBuf.append(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.
+     */
+
+    // FIXME: isExternalLink() is called twice.
+    private Element handleImageLink( String reallink, String link, boolean hasLinkText )
+    {
+        String possiblePage = MarkupParser.cleanLink( link );
+
+        if( isExternalLink( link ) && hasLinkText )
+        {
+            return makeLink( IMAGELINK, reallink, link, null, null );
+        }
+        else if( ( linkExists( possiblePage ) ) != null &&
+                 hasLinkText )
+        {
+            // System.out.println("Orig="+link+", Matched: "+matchedLink);
+            callMutatorChain( m_localLinkMutatorChain, possiblePage );
+
+            return makeLink( IMAGEWIKILINK, reallink, link, null, null );
+        }
+        else
+        {
+            return makeLink( IMAGE, reallink, link, null, null );
+        }
+    }
+
+    private Element handleAccessRule( String ruleLine )
+    {
+        if( m_wysiwygEditorMode )
+        {
+            m_currentElement.addContent( "[" + ruleLine + "]" );
+        }
+
+        if( !m_parseAccessRules ) return m_currentElement;
+        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 );
+
+        if( log.isDebugEnabled() ) log.debug("page="+page.getName()+", ACL = "+ruleLine);
+
+        try
+        {
+            acl = m_engine.getAclManager().parseAcl( page, ruleLine );
+
+            page.setAcl( acl );
+
+            if( log.isDebugEnabled() ) log.debug( acl.toString() );
+        }
+        catch( WikiSecurityException wse )
+        {
+            return makeError( wse.getMessage() );
+        }
+
+        return m_currentElement;
+    }
+
+    /**
+     *  Handles metadata setting [{SET foo=bar}]
+     */
+    private Element handleMetadata( String link )
+    {
+        if( m_wysiwygEditorMode )
+        {
+            m_currentElement.addContent( "[" + 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 };
+            return makeError( MessageFormat.format( rb.getString( "markupparser.error.invalidset" ), args ) );
+        }
+
+        return m_currentElement;
+    }
+
+    /**
+     *  Emits a processing instruction that will disable markup escaping. This is
+     *  very useful if you want to emit HTML directly into the stream.
+     *
+     */
+    private void disableOutputEscaping()
+    {
+        addElement( new ProcessingInstruction(Result.PI_DISABLE_OUTPUT_ESCAPING, "") );
+    }
+
+    /**
+     *  Gobbles up all hyperlinks that are encased in square brackets.
+     */
+    private Element handleHyperlinks( String linktext, int pos )
+    {
+        ResourceBundle rb = m_context.getBundle(InternationalizationManager.CORE_BUNDLE);
+        
+        StringBuffer sb = new StringBuffer(linktext.length()+80);
+
+        if( isAccessRule( linktext ) )
+        {
+            return handleAccessRule( linktext );
+        }
+
+        if( isMetadata( linktext ) )
+        {
+            return handleMetadata( linktext );
+        }
+
+        if( PluginManager.isPluginLink( linktext ) )
+        {
+            try
+            {
+                PluginContent pluginContent = m_engine.getPluginManager().parsePluginLine( m_context,
+                                                                                           linktext,
+                                                                                           pos );
+
+                addElement( pluginContent );
+
+                pluginContent.executeParse( m_context );
+            }
+            catch( PluginException e )
+            {
+                log.info( "Failed to insert plugin: "+e.getMessage() );
+                //log.info( "Root cause:",e.getRootThrowable() );
+                if( !m_wysiwygEditorMode )
+                {
+                    ResourceBundle rbPlugin = m_context.getBundle(WikiPlugin.CORE_PLUGINS_RESOURCEBUNDLE);
+                    Object[] args = { e.getMessage() };
+                    return addElement( makeError( MessageFormat.format( rbPlugin.getString( "plugin.error.insertionfailed" ), args ) ) );
+                }
+            }
+
+            return m_currentElement;
+        }
+
+        try
+        {
+            LinkParser.Link link = m_linkParser.parse(linktext);
+            linktext       = link.getText();
+            String linkref = link.getReference();
+
+            //
+            //  Yes, we now have the components separated.
+            //  linktext = the text the link should have
+            //  linkref  = the url or page name.
+            //
+            //  In many cases these are the same.  [linktext|linkref].
+            //
+            if( VariableManager.isVariableLink( linktext ) )
+            {
+                Content el = new VariableContent(linktext);
+
+                addElement( el );
+            }
+            else if( isExternalLink( linkref ) )
+            {
+                // It's an external link, out of this Wiki
+
+                callMutatorChain( m_externalLinkMutatorChain, linkref );
+
+                if( isImageLink( linkref ) )
+                {
+                    handleImageLink( linkref, linktext, link.hasReference() );
+                }
+                else
+                {
+                    makeLink( EXTERNAL, linkref, linktext, null, link.getAttributes() );
+                    addElement( outlinkImage() );
+                }
+            }
+            else if( link.isInterwikiLink() )
+            {
+                // 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  = link.getExternalWiki();
+                String wikiPage = link.getExternalWikiPage();
+
+                if( m_wysiwygEditorMode )
+                {
+                    makeLink( INTERWIKI, extWiki + ":" + wikiPage, linktext, null, link.getAttributes() );
+                }
+                else
+                {
+                    String urlReference = m_engine.getInterWikiURL( extWiki );
+
+                    if( urlReference != null )
+                    {
+                        urlReference = TextUtil.replaceString( urlReference, "%s", wikiPage );
+                        urlReference = callMutatorChain( m_externalLinkMutatorChain, urlReference );
+
+                        if( isImageLink(urlReference) )
+                        {
+                            handleImageLink( urlReference, linktext, link.hasReference() );
+                        }
+                        else
+                        {
+                            makeLink( INTERWIKI, urlReference, linktext, null, link.getAttributes() );
+                        }
+
+                        if( isExternalLink(urlReference) )
+                        {
+                            addElement( outlinkImage() );
+                        }
+                    }
+                    else
+                    {
+                        Object[] args = { extWiki };
+                        addElement( makeError( MessageFormat.format( rb.getString( "markupparser.error.nointerwikiref" ), args ) ) );
+                    }
+                }
+            }
+            else if( linkref.startsWith("#") )
+            {
+                // It defines a local footnote
+                makeLink( LOCAL, linkref, linktext, null, link.getAttributes() );
+            }
+            else if( TextUtil.isNumber( linkref ) )
+            {
+                // It defines a reference to a local footnote
+                makeLink( LOCALREF, linkref, linktext, null, link.getAttributes() );
+            }
+            else
+            {
+                int hashMark = -1;
+
+                //
+                //  Internal wiki link, but is it an attachment link?
+                //
+                String attachment = findAttachment( linkref );
+                if( attachment != null )
+                {
+                    callMutatorChain( m_attachmentLinkMutatorChain, attachment );
+
+                    if( isImageLink( linkref ) )
+                    {
+                        attachment = m_context.getContext().getURL( AttachActionBean.class, attachment );
+                        sb.append( handleImageLink( attachment, linktext, link.hasReference() ) );
+                    }
+                    else
+                    {
+                        makeLink( ATTACHMENT, attachment, linktext, null, link.getAttributes() );
+                    }
+                }
+                else if( (hashMark = linkref.indexOf('#')) != -1 )
+                {
+                    // It's an internal Wiki link, but to a named section
+
+                    String namedSection = linkref.substring( hashMark+1 );
+                    linkref = linkref.substring( 0, hashMark );
+
+                    linkref = MarkupParser.cleanLink( linkref );
+
+                    callMutatorChain( m_localLinkMutatorChain, linkref );
+
+                    String matchedLink;
+                    if( (matchedLink = linkExists( linkref )) != null )
+                    {
+                        String sectref = "section-"+m_engine.encodeName(matchedLink)+"-"+wikifyLink(namedSection);
+                        sectref = sectref.replace('%', '_');
+                        makeLink( READ, matchedLink, linktext, sectref, link.getAttributes() );
+                    }
+                    else
+                    {
+                        makeLink( EDIT, linkref, linktext, null, link.getAttributes() );
+                    }
+                }
+                else
+                {
+                    // It's an internal Wiki link
+                    linkref = MarkupParser.cleanLink( linkref );
+
+                    callMutatorChain( m_localLinkMutatorChain, linkref );
+
+                    String matchedLink = linkExists( linkref );
+
+                    if( matchedLink != null )
+                    {
+                        makeLink( READ, matchedLink, linktext, null, link.getAttributes() );
+                    }
+                    else
+                    {
+                        makeLink( EDIT, linkref, linktext, null, link.getAttributes() );
+                    }
+                }
+            }
+        }
+        catch( ParseException e )
+        {
+            log.info("Parser failure: ",e);
+            Object[] args = { e.getMessage() };
+            addElement( makeError( MessageFormat.format( rb.getString( "markupparser.error.parserfailure" ), args ) ) );
+        }
+
+        return m_currentElement;
+    }
+
+    private String findAttachment( String linktext )
+    {
+        AttachmentManager mgr = m_engine.getAttachmentManager();
+        Attachment att = null;
+
+        try
+        {
+            att = mgr.getAttachmentInfo( m_context, linktext );
+        }
+        catch( ProviderException e )
+        {
+            log.warn("Finding attachments failed: ",e);
+            return null;
+        }
+
+        if( att != null )
+        {
+            return att.getName();
+        }
+        else if( linktext.indexOf('/') != -1 )
+        {
+            return linktext;
+        }
+
+        return null;
+    }
+
+    /**
+     *  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 Element handleBackslash()
+        throws IOException
+    {
+        int ch = nextToken();
+
+        if( ch == '\\' )
+        {
+            int ch2 = nextToken();
+
+            if( ch2 == '\\' )
+            {
+                pushElement( new Element("br").setAttribute("clear","all"));
+                return popElement("br");
+            }
+
+            pushBack( ch2 );
+
+            pushElement( new Element("br") );
+            return popElement("br");
+        }
+
+        pushBack( ch );
+
+        return null;
+    }
+
+    private Element handleUnderscore()
+        throws IOException
+    {
+        int ch = nextToken();
+        Element el = null;
+
+        if( ch == '_' )
+        {
+            if( m_isbold )
+            {
+                el = popElement("b");
+            }
+            else
+            {
+                el = pushElement( new Element("b") );
+            }
+            m_isbold = !m_isbold;
+        }
+        else
+        {
+            pushBack( ch );
+        }
+
+        return el;
+    }
+
+
+    /**
+     *  For example: italics.
+     */
+    private Element handleApostrophe()
+        throws IOException
+    {
+        int ch = nextToken();
+        Element el = null;
+
+        if( ch == '\'' )
+        {
+            if( m_isitalic )
+            {
+                el = popElement("i");
+            }
+            else
+            {
+                el = pushElement( new Element("i") );
+            }
+            m_isitalic = !m_isitalic;
+        }
+        else
+        {
+            pushBack( ch );
+        }
+
+        return el;
+    }
+
+    private Element handleOpenbrace( boolean isBlock )
+        throws IOException
+    {
+        int ch = nextToken();
+
+        if( ch == '{' )
+        {
+            int ch2 = nextToken();
+
+            if( ch2 == '{' )
+            {
+                m_isPre = true;
+                m_isEscaping = true;
+                m_isPreBlock = isBlock;
+
+                if( isBlock )
+                {
+                    startBlockLevel();
+                    return pushElement( new Element("pre") );
+                }
+
+                return pushElement( new Element("span").setAttribute("style","font-family:monospace; white-space:pre;") );
+            }
+
+            pushBack( ch2 );
+
+            return pushElement( new Element("tt") );
+        }
+
+        pushBack( ch );
+
+        return null;
+    }
+
+    /**
+     *  Handles both }} and }}}
+     */
+    private Element handleClosebrace()
+        throws IOException
+    {
+        int ch2 = nextToken();
+
+        if( ch2 == '}' )
+        {
+            int ch3 = nextToken();
+
+            if( ch3 == '}' )
+            {
+                if( m_isPre )
+                {
+                    if( m_isPreBlock )
+                    {
+                        popElement( "pre" );
+                    }
+                    else
+                    {
+                        popElement( "span" );
+                    }
+
+                    m_isPre = false;
+                    m_isEscaping = false;
+                    return m_currentElement;
+                }
+
+                m_plainTextBuf.append("}}}");
+                return m_currentElement;
+            }
+
+            pushBack( ch3 );
+
+            if( !m_isEscaping )
+            {
+                return popElement("tt");
+            }
+        }
+
+        pushBack( ch2 );
+
+        return null;
+    }
+
+    private Element 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);
+                    startBlockLevel();
+                    pushElement( new Element("hr") );
+                    return popElement( "hr" );
+                }
+
+                pushBack( ch3 );
+            }
+            pushBack( ch2 );
+        }
+
+        pushBack( ch );
+
+        return null;
+    }
+
+    private Element handleHeading()
+        throws IOException
+    {
+        Element el = null;
+
+        int ch  = nextToken();
+
+        Heading hd = new Heading();
+
+        if( ch == '!' )
+        {
+            int ch2 = nextToken();
+
+            if( ch2 == '!' )
+            {
+                String title = peekAheadLine();
+
+                el = makeHeading( Heading.HEADING_LARGE, title, hd);
+            }
+            else
+            {
+                pushBack( ch2 );
+                String title = peekAheadLine();
+                el = makeHeading( Heading.HEADING_MEDIUM, title, hd );
+            }
+        }
+        else
+        {
+            pushBack( ch );
+            String title = peekAheadLine();
+            el = makeHeading( Heading.HEADING_SMALL, title, hd );
+        }
+
+        callHeadingListenerChain( hd );
+
+        if( el != null ) pushElement(el);
+
+        return el;
+    }
+
+    /**
+     *  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( 256 );
+
+        while( true )
+        {
+            ch = nextToken();
+
+            if( ch == -1 )
+                break;
+
+            buf.append( (char) ch );
+
+            if( ch == '\n' )
+                break;
+        }
+        return buf;
+    }
+
+    /** Controls whether italic is restarted after a paragraph shift */
+
+    private boolean m_restartitalic = false;
+    private boolean m_restartbold   = false;
+
+    private boolean m_newLine;
+
+    /**
+     *  Starts a block level element, therefore closing
+     *  a potential open paragraph tag.
+     */
+    private void startBlockLevel()
+    {
+        // These may not continue over block level limits in XHTML
+
+        popElement("i");
+        popElement("b");
+        popElement("tt");
+
+        if( m_isOpenParagraph )
+        {
+            m_isOpenParagraph = false;
+            popElement("p");
+            m_plainTextBuf.append("\n"); // Just small beautification
+        }
+
+        m_restartitalic = m_isitalic;
+        m_restartbold   = m_isbold;
+
+        m_isitalic = false;
+        m_isbold   = false;
+    }
+
+    private static String getListType( char c )
+    {
+        if( c == '*' )
+        {
+            return "ul";
+        }
+        else if( c == '#' )
+        {
+            return "ol";
+        }
+        throw new InternalWikiException("Parser got faulty list type: "+c);
+    }
+    /**
+     *  Like original handleOrderedList() and handleUnorderedList()
+     *  however handles both ordered ('#') and unordered ('*') mixed together.
+     */
+
+    // FIXME: Refactor this; it's a bit messy.
+
+    private Element handleGeneralList()
+        throws IOException
+    {
+         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 )
+             {
+                 pushElement( new Element( getListType(strBullets.charAt(m_genlistlevel++) ) ) );
+
+                 for( ; m_genlistlevel < numBullets; m_genlistlevel++ )
+                 {
+                     // bullets are growing, get from new bullet list
+                     pushElement( new Element("li") );
+                     pushElement( new Element( getListType(strBullets.charAt(m_genlistlevel)) ));
+                 }
+             }
+             else if( numBullets < m_genlistlevel )
+             {
+                 //  Close the previous list item.
+                 // buf.append( m_renderer.closeListItem() );
+                 popElement( "li" );
+
+                 for( ; m_genlistlevel > numBullets; m_genlistlevel-- )
+                 {
+                     // bullets are shrinking, get from old bullet list
+
+                     popElement( getListType(m_genlistBulletBuffer.charAt(m_genlistlevel-1)) );
+                     if( m_genlistlevel > 0 )
+                     {
+                         popElement( "li" );
+                     }
+
+                 }
+             }
+             else
+             {
+                 if( m_genlistlevel > 0 )
+                 {
+                     popElement( "li" );
+                 }
+             }
+         }
+         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-- )
+             {
+                 popElement( getListType( m_genlistBulletBuffer.charAt(m_genlistlevel-1) ) );
+                 if( m_genlistlevel > 0 )
+                 {
+                     popElement("li");
+                 }
+             }
+
+             //rewind
+
+             pushElement( new Element(getListType( strBullets.charAt(numEqualBullets++) ) ) );
+             for(int i = numEqualBullets; i < numBullets; i++)
+             {
+                 pushElement( new Element("li") );
+                 pushElement( new Element( getListType( strBullets.charAt(i) ) ) );
+             }
+             m_genlistlevel = numBullets;
+         }
+
+         //
+         //  Push a new list item, and eat away any extra whitespace
+         //
+         pushElement( new Element("li") );
+         readWhile(" ");
+
+         // work done, remember the new bullet list (in place of old one)
+         m_genlistBulletBuffer.setLength(0);
+         m_genlistBulletBuffer.append(strBullets);
+
+         return m_currentElement;
+    }
+
+    private Element unwindGeneralList()
+    {
+        //unwind
+        for( ; m_genlistlevel > 0; m_genlistlevel-- )
+        {
+            popElement( "li" );
+            popElement( getListType(m_genlistBulletBuffer.charAt(m_genlistlevel-1)) );
+        }
+
+        m_genlistBulletBuffer.setLength(0);
+
+        return null;
+    }
+
+
+    private Element handleDefinitionList()
+        throws IOException
+    {
+        if( !m_isdefinition )
+        {
+            m_isdefinition = true;
+
+            startBlockLevel();
+
+            pushElement( new Element("dl") );
+            return pushElement( new Element("dt") );
+        }
+
+        return null;
+    }
+
+    private Element handleOpenbracket()
+        throws IOException
+    {
+        StringBuffer sb = new StringBuffer(40);
+        int pos = getPosition();
+        int ch = nextToken();
+        boolean isPlugin = false;
+
+        if( ch == '[' )
+        {
+            if( m_wysiwygEditorMode )
+            {
+                sb.append( '[' );
+            }
+
+            sb.append( (char)ch );
+
+            while( (ch = nextToken()) == '[' )
+            {
+                sb.append( (char)ch );
+            }
+        }
+
+
+        if( ch == '{' )
+        {
+            isPlugin = true;
+        }
+
+        pushBack( ch );
+
+        if( sb.length() > 0 )
+        {
+            m_plainTextBuf.append( sb );
+            return m_currentElement;
+        }
+
+        //
+        //  Find end of hyperlink
+        //
+
+        ch = nextToken();
+        int nesting = 1;    // Check for nested plugins
+
+        while( ch != -1 )
+        {
+            int ch2 = nextToken(); pushBack(ch2);
+
+            if( isPlugin )
+            {
+                if( ch == '[' && ch2 == '{' )
+                {
+                    nesting++;
+                }
+                else if( nesting == 0 && ch == ']' && sb.charAt(sb.length()-1) == '}' )
+                {
+                    break;
+                }
+                else if( ch == '}' && ch2 == ']' )
+                {
+                    // NB: This will be decremented once at the end
+                    nesting--;
+                }
+            }
+            else
+            {
+                if( ch == ']' )
+                {
+                    break;
+                }
+            }
+
+            sb.append( (char) ch );
+
+            ch = nextToken();
+        }
+
+        //
+        //  If the link is never finished, do some tricks to display the rest of the line
+        //  unchanged.
+        //
+        if( ch == -1 )
+        {
+            log.debug("Warning: unterminated link detected!");
+            m_isEscaping = true;
+            m_plainTextBuf.append( sb );
+            flushPlainText();
+            m_isEscaping = false;
+            return m_currentElement;
+        }
+
+        return handleHyperlinks( sb.toString(), pos );
+    }
+
+    /**
+     *  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(40);
+        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();
+    }
+
+
+    /**
+     *  Handles constructs of type %%(style) and %%class
+     * @param newLine
+     * @return
+     * @throws IOException
+     */
+    private Element handleDiv( boolean newLine )
+        throws IOException
+    {
+        int ch = nextToken();
+        Element el = null;
+
+        if( ch == '%' )
+        {
+            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() )
+                    {
+                        el = popElement( "span" );
+                    }
+                    else
+                    {
+                        el = popElement( "div" );
+                    }
+                }
+                catch( EmptyStackException e )
+                {
+                    log.debug("Page '"+m_context.getName()+"' closes a %%-block that has not been opened.");
+                    return m_currentElement;
+                }
+
+                return el;
+            }
+
+            //
+            //  Check if there is an attempt to do something nasty
+            //
+            
+            try
+            {
+                style = StringEscapeUtils.unescapeHtml(style);
+                if( style != null && style.indexOf("javascript:") != -1 )
+                {
+                    log.debug("Attempt to output javascript within CSS:"+style);
+                    ResourceBundle rb = m_context.getBundle(InternationalizationManager.CORE_BUNDLE);
+                    return addElement( makeError( rb.getString( "markupparser.error.javascriptattempt" ) ) );
+                }
+            }
+            catch( NumberFormatException e )
+            {
+                //
+                //  If there are unknown entities, we don't want the parser to stop.
+                //
+                ResourceBundle rb = m_context.getBundle(InternationalizationManager.CORE_BUNDLE);
+                Object[] args = { e.getMessage() };
+                String msg = MessageFormat.format( rb.getString( "markupparser.error.parserfailure"), args );
+                return addElement( makeError( msg ) );
+            }
+
+            //
+            //  Decide if we should open a div or a span?
+            //
+            String eol = peekAheadLine();
+
+            if( eol.trim().length() > 0 )
+            {
+                // There is stuff after the class
+
+                el = new Element("span");
+
+                m_styleStack.push( Boolean.TRUE );
+            }
+            else
+            {
+                startBlockLevel();
+                el = new Element("div");
+                m_styleStack.push( Boolean.FALSE );
+            }
+
+            if( style != null ) el.setAttribute("style", style);
+            if( clazz != null ) el.setAttribute("class", clazz );
+            el = pushElement( el );
+
+            return el;
+        }
+
+        pushBack(ch);
+
+        return el;
+    }
+
+    private Element handleSlash( boolean newLine )
+        throws IOException
+    {
+        int ch = nextToken();
+
+        pushBack(ch);
+        if( ch == '%' && !m_styleStack.isEmpty() )
+        {
+            return handleDiv( newLine );
+        }
+
+        return null;
+    }
+
+    private Element handleBar( boolean newLine )
+        throws IOException
+    {
+        Element el = null;
+
+        if( !m_istable && !newLine )
+        {
+            return null;
+        }
+
+        //
+        //  If the bar is in the first column, we will either start
+        //  a new table or continue the old one.
+        //
+
+        if( newLine )
+        {
+            if( !m_istable )
+            {
+                startBlockLevel();
+                el = pushElement( new Element("table").setAttribute("class","wikitable").setAttribute("border","1") );
+                m_istable = true;
+                m_rowNum = 0;
+            }
+
+            m_rowNum++;
+            Element tr = ( m_rowNum % 2 != 0 )
+                       ? new Element("tr").setAttribute("class", "odd")
+                       : new Element("tr");
+            el = pushElement( tr );
+        }
+
+        //
+        //  Check out which table cell element to start;
+        //  a header element (th) or a regular element (td).
+        //
+        int ch = nextToken();
+
+        if( ch == '|' )
+        {
+            if( !newLine )
+            {
+                el = popElement("th");
+                if( el == null ) popElement("td");
+            }
+            el = pushElement( new Element("th") );
+        }
+        else
+        {
+            if( !newLine )
+            {
+                el = popElement("td");
+                if( el == null ) popElement("th");
+            }
+
+            el = pushElement( new Element("td") );
+
+            pushBack( ch );
+        }
+
+        return el;
+    }
+
+    /**
+     *  Generic escape of next character or entity.
+     */
+    private Element handleTilde()
+        throws IOException
+    {
+        int ch = nextToken();
+
+        if( ch == ' ' )
+        {
+            if( m_wysiwygEditorMode )
+            {
+                m_plainTextBuf.append( "~ " );
+            }
+            return m_currentElement;
+        }
+
+        if( ch == '|' || ch == '~' || ch == '\\' || ch == '*' || ch == '#' ||
+            ch == '-' || ch == '!' || ch == '\'' || ch == '_' || ch == '[' ||
+            ch == '{' || ch == ']' || ch == '}' || ch == '%' )
+        {
+            if( m_wysiwygEditorMode )
+            {
+                m_plainTextBuf.append( '~' );
+            }
+
+            m_plainTextBuf.append( (char)ch );
+            m_plainTextBuf.append(readWhile( ""+(char)ch ));
+            return m_currentElement;
+        }
+
+        // No escape.
+        pushBack( ch );
+
+        return null;
+    }
+
+    private void fillBuffer( Element startElement )
+        throws IOException
+    {
+        m_currentElement = startElement;
+
+        boolean quitReading = false;
+        m_newLine = true;
+        disableOutputEscaping();
+
+        while(!quitReading)
+        {
+            int ch = nextToken();
+
+            if( ch == -1 ) break;
+
+            //
+            //  Check if we're actually ending the preformatted mode.
+            //  We still must do an entity transformation here.
+            //
+            if( m_isEscaping )
+            {
+                if( ch == '}' )
+                {
+                    if( handleClosebrace() == null ) m_plainTextBuf.append( (char) ch );
+                }
+                else if( ch == -1 )
+                {
+                    quitReading = true;
+                }
+                else if( ch == '\r' )
+                {
+                    // DOS line feeds we ignore.
+                }
+                else if( ch == '<' )
+                {
+                    m_plainTextBuf.append( "&lt;" );
+                }
+                else if( ch == '>' )
+                {
+                    m_plainTextBuf.append( "&gt;" );
+                }
+                else if( ch == '&' )
+                {
+                    m_plainTextBuf.append( "&amp;" );
+                }
+                else if( ch == '~' )
+                {
+                    String braces = readWhile("}");
+                    if( braces.length() >= 3 )
+                    {
+                        m_plainTextBuf.append("}}}");
+
+                        braces = braces.substring(3);
+                    }
+                    else
+                    {
+                        m_plainTextBuf.append( (char) ch );
+                    }
+
+                    for( int i = braces.length()-1; i >= 0; i-- )
+                    {
+                        pushBack(braces.charAt(i));
+                    }
+                }
+                else
+                {
+                    m_plainTextBuf.append( (char) ch );
+                }
+
+                continue;
+            }
+
+            //
+            //  An empty line stops a list
+            //
+            if( m_newLine && ch != '*' && ch != '#' && ch != ' ' && m_genlistlevel > 0 )
+            {
+                m_plainTextBuf.append(unwindGeneralList());
+            }
+
+            if( m_newLine && ch != '|' && m_istable )
+            {
+                popElement("table");
+                m_istable = false;
+            }
+
+            int skip = IGNORE;
+            
+            //
+            //  Do the actual parsing and catch any errors.
+            //
+            try
+            {
+                skip = parseToken( ch );
+            }
+            catch( IllegalDataException e )
+            {
+                log.info("Page "+m_context.getPage().getName()+" contains data which cannot be added to DOM tree: "+e.getMessage());
+                
+                makeError("Error: "+cleanupSuspectData(e.getMessage()) );
+            }
+            
+            //
+            //   The idea is as follows:  If the handler method returns
+            //   an element (el != null), it is assumed that it has been
+            //   added in the stack.  Otherwise the character is added
+            //   as is to the plaintext buffer.
+            //
+            //   For the transition phase, if s != null, it also gets
+            //   added in the plaintext buffer.
+            //
+
+            switch( skip )
+            {
+                case ELEMENT:
+                    m_newLine = false;
+                    break;
+
+                case CHARACTER:
+                    m_plainTextBuf.append( (char) ch );
+                    m_newLine = false;
+                    break;
+
+                case IGNORE:
+                default:
+                    break;
+            }
+        }
+
+        popElement("domroot");
+    }
+
+    private String cleanupSuspectData( String s )
+    {
+        StringBuffer sb = new StringBuffer( s.length() );
+        
+        for( int i = 0; i < s.length(); i++ )
+        {
+            char c = s.charAt(i);
+            
+            if( Verifier.isXMLCharacter( c ) ) sb.append( c );
+            else sb.append( "0x"+Integer.toString(c,16).toUpperCase() );
+        }
+        
+        return sb.toString();
+    }
+    
+    public static final int CHARACTER = 0;
+    public static final int ELEMENT   = 1;
+    public static final int IGNORE    = 2;
+
+    /**
+     *  Return CHARACTER, if you think this was a plain character; ELEMENT, if
+     *  you think this was a wiki markup element, and IGNORE, if you think
+     *  we should ignore this altogether.
+     *
+     * @param ch
+     * @return {@link #ELEMENT}, {@link #CHARACTER} or {@link #IGNORE}.
+     * @throws IOException
+     */
+    protected int parseToken( int ch )
+        throws IOException
+    {
+        Element el = null;
+
+        //
+        //  Now, check the incoming token.
+        //
+        switch( ch )
+        {
+          case '\r':
+            // DOS linefeeds we forget
+            return IGNORE;
+
+          case '\n':
+            //
+            //  Close things like headings, etc.
+            //
+
+            // FIXME: This is not really very fast
+            popElement("dl"); // Close definition lists.
+            popElement("h2");
+            popElement("h3");
+            popElement("h4");
+            if( m_istable )
+            {
+                popElement("tr");
+            }
+
+            m_isdefinition = false;
+
+            if( m_newLine )
+            {
+                // Paragraph change.
+                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) )
+                {
+                    pushElement( new Element("p") );
+                    m_isOpenParagraph = true;
+
+                    if( m_restartitalic )
+                    {
+                        pushElement( new Element("i") );
+                        m_isitalic = true;
+                        m_restartitalic = false;
+                    }
+                    if( m_restartbold )
+                    {
+                        pushElement( new Element("b") );
+                        m_isbold = true;
+                        m_restartbold = false;
+                    }
+                }
+            }
+            else
+            {
+                m_plainTextBuf.append("\n");
+                m_newLine = true;
+            }
+            return IGNORE;
+
+
+          case '\\':
+            el = handleBackslash();
+            break;
+
+          case '_':
+            el = handleUnderscore();
+            break;
+
+          case '\'':
+            el = handleApostrophe();
+            break;
+
+          case '{':
+            el = handleOpenbrace( m_newLine );
+            break;
+
+          case '}':
+            el = handleClosebrace();
+            break;
+
+          case '-':
+            if( m_newLine )
+                el = handleDash();
+
+            break;
+
+          case '!':
+            if( m_newLine )
+            {
+                el = handleHeading();
+            }
+            break;
+
+          case ';':
+            if( m_newLine )
+            {
+                el = handleDefinitionList();
+            }
+            break;
+
+          case ':':
+            if( m_isdefinition )
+            {
+                popElement("dt");
+                el = pushElement( new Element("dd") );
+                m_isdefinition = false;
+            }
+            break;
+
+          case '[':
+            el = handleOpenbracket();
+            break;
+
+          case '*':
+            if( m_newLine )
+            {
+                pushBack('*');
+                el = handleGeneralList();
+            }
+            break;
+
+          case '#':
+            if( m_newLine )
+            {
+                pushBack('#');
+                el = handleGeneralList();

[... 137 lines stripped ...]