You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by ju...@apache.org on 2021/10/16 16:06:13 UTC

[jspwiki] 06/08: First stab at refactor

This is an automated email from the ASF dual-hosted git repository.

juanpablo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jspwiki.git

commit b60ef16ed69f10634e6dca7a5515df1873b36f56
Author: Juan Pablo Santos Rodríguez <ju...@gmail.com>
AuthorDate: Sat Oct 16 17:59:54 2021 +0200

    First stab at  refactor
    
    so it'll be easier in the future to make it output other types of wiki syntaxes
    
    * Deleted ForgetNullValuesLinkedHashMap, used only in one place, it's clearer to explicitly put if value not empty
    * Deleted PersistentMapDecorator, used only in one place, Stream's api is clearer + avoid rethrowing meaningless IOException
    * Print methods.. renamed to translate..
    * Extracted case statements on printChildren to their own decorator methods
    * Other parts of the class outputting wiki syntax extracted to their own de* Replaced privacorator methods
    te LiStack by plain java.util.Stack
    * Refactored private PreStack so it extends java.util.Stack
    * Moved getXPathElement method to XmlUtil, as a static method
---
 .../htmltowiki/ForgetNullValuesLinkedHashMap.java  |   44 -
 .../wiki/htmltowiki/PersistentMapDecorator.java    |  181 ---
 .../htmltowiki/XHtmlElementToWikiTranslator.java   | 1516 +++++++++-----------
 .../main/java/org/apache/wiki/util/XmlUtil.java    |    9 +
 .../java/org/apache/wiki/util/XmlUtilTest.java     |    8 +
 5 files changed, 723 insertions(+), 1035 deletions(-)

diff --git a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/ForgetNullValuesLinkedHashMap.java b/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/ForgetNullValuesLinkedHashMap.java
deleted file mode 100644
index e5b1973..0000000
--- a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/ForgetNullValuesLinkedHashMap.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
- */
-package org.apache.wiki.htmltowiki;
-
-import java.util.LinkedHashMap;
-
-/**
- * A LinkedHashMap that does not put null values into the map.
- *
- */
-public class ForgetNullValuesLinkedHashMap<K,V> extends LinkedHashMap<K,V>
-{
-    private static final long serialVersionUID = 0L;
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public V put(final K key, final V value )
-    {
-        if( value != null )
-        {
-            return super.put( key, value );
-        }
-
-        return null;
-    }
-}
diff --git a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/PersistentMapDecorator.java b/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/PersistentMapDecorator.java
deleted file mode 100644
index 251e3d6..0000000
--- a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/PersistentMapDecorator.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/* 
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.  
- */
-package org.apache.wiki.htmltowiki;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-
-/**
- * Adds the load / save - functionality known from the Properties - class to any
- * Map implementation.
- * 
- */
-public class PersistentMapDecorator extends Properties
-{
-    private static final long serialVersionUID = 0L;
-    
-    private final Map< Object, Object > m_delegate;
-
-    /**
-     *  Creates a new decorator for a given map.
-     *  
-     *  @param delegate The map to create a decorator for.
-     */
-    public PersistentMapDecorator(final Map< Object, Object > delegate )
-    {
-        m_delegate = delegate;
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public void clear()
-    {
-        m_delegate.clear();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public boolean containsKey(final Object key )
-    {
-        return m_delegate.containsKey( key );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public boolean containsValue(final Object value )
-    {
-        return m_delegate.containsValue( value );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Set< Map.Entry< Object, Object > > entrySet()
-    {
-        return m_delegate.entrySet();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public boolean equals(final Object obj )
-    {
-        return m_delegate.equals( obj );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Object get(final Object key )
-    {
-        return m_delegate.get( key );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public int hashCode()
-    {
-        return m_delegate.hashCode();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public boolean isEmpty()
-    {
-        return m_delegate.isEmpty();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Set< Object > keySet()
-    {
-        return m_delegate.keySet();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Object put(final Object arg0, final Object arg1 )
-    {
-        return m_delegate.put( arg0, arg1 );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public void putAll(final Map< ?, ? > arg0 )
-    {
-        m_delegate.putAll( arg0 );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Object remove(final Object key )
-    {
-        return m_delegate.remove( key );
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public int size()
-    {
-        return m_delegate.size();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public String toString()
-    {
-        return m_delegate.toString();
-    }
-
-    /**
-     *  {@inheritDoc}
-     */
-    @Override
-    public Collection< Object > values()
-    {
-        return m_delegate.values();
-    }
-}
diff --git a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java b/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java
index c2d3bbf..78ffbf7 100644
--- a/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java
+++ b/jspwiki-main/src/main/java/org/apache/wiki/htmltowiki/XHtmlElementToWikiTranslator.java
@@ -18,7 +18,9 @@
  */
 package org.apache.wiki.htmltowiki;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.StringEscapeUtils;
+import org.apache.wiki.util.XmlUtil;
 import org.jdom2.Attribute;
 import org.jdom2.Content;
 import org.jdom2.Element;
@@ -26,45 +28,39 @@ import org.jdom2.JDOMException;
 import org.jdom2.Text;
 import org.jdom2.xpath.XPathFactory;
 
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Stack;
 
 
 /**
- * Converting XHtml to Wiki Markup.  This is the class which does all of the heavy loading.
- *
+ * Converting XHtml to Wiki Markup.  This is the class which does all the heavy loading.
  */
-public class XHtmlElementToWikiTranslator
-{
+public class XHtmlElementToWikiTranslator {
 
-    private final XHtmlToWikiConfig m_config;
+    private final XHtmlToWikiConfig config;
 
-    private final WhitespaceTrimWriter m_outTimmer;
+    private final WhitespaceTrimWriter outTrimmer = new WhitespaceTrimWriter();
 
-    private final PrintWriter m_out;
+    private final PrintWriter out = new PrintWriter( outTrimmer );
 
-    private final LiStack m_liStack = new LiStack();
+    private final Stack< String > liStack = new Stack<>();
 
-    private final PreStack m_preStack = new PreStack();
+    private final Stack< String > preStack = new PreStack();
 
     /**
      *  Create a new translator using the default config.
      *
      *  @param base The base element from which to start translating.
-     *  @throws IOException If reading of the DOM tree fails.
      *  @throws JDOMException If the DOM tree is faulty.
      */
-    public XHtmlElementToWikiTranslator(final Element base ) throws IOException, JDOMException
-    {
+    public XHtmlElementToWikiTranslator(final Element base ) throws JDOMException {
         this( base, new XHtmlToWikiConfig() );
     }
 
@@ -73,941 +69,841 @@ public class XHtmlElementToWikiTranslator
      *
      *  @param base The base element from which to start translating.
      *  @param config The config to use.
-     *  @throws IOException If reading of the DOM tree fails.
      *  @throws JDOMException If the DOM tree is faulty.
      */
-    public XHtmlElementToWikiTranslator(final Element base, final XHtmlToWikiConfig config ) throws IOException, JDOMException
-    {
-        this.m_config = config;
-        m_outTimmer = new WhitespaceTrimWriter();
-        m_out = new PrintWriter( m_outTimmer );
-        print( base );
+    public XHtmlElementToWikiTranslator( final Element base, final XHtmlToWikiConfig config ) throws JDOMException {
+        this.config = config;
+        translate( base );
     }
 
     /**
-     *  FIXME: I have no idea what this does...
+     * Outputs parsed wikitext.
      *
-     *  @return Something.
+     * @return parsed wikitext.
      */
-    public String getWikiString()
-    {
-        return m_outTimmer.toString();
-    }
-
-    private void print( String s )
-    {
-        s = StringEscapeUtils.unescapeHtml4( s );
-        m_out.print( s );
-    }
-
-    private void print(final Object element ) throws IOException, JDOMException
-    {
-        if( element instanceof Text )
-        {
-            final Text t = (Text)element;
-            String s = t.getText();
-            if( m_preStack.isPreMode() )
-            {
-                m_out.print( s );
-            }
-            else
-            {
-                // remove all "line terminator" characters
-                s = s.replaceAll( "[\\r\\n\\f\\u0085\\u2028\\u2029]", "" );
-                m_out.print( s );
-            }
-        }
-        else if( element instanceof Element )
-        {
-            final Element base = (Element)element;
-            String n = base.getName().toLowerCase();
-            if( "imageplugin".equals( base.getAttributeValue( "class" ) ) )
-            {
-                printImage( base );
-            }
-            else if( "wikiform".equals( base.getAttributeValue( "class" ) ) )
-            {
+    public String getWikiString() {
+        return outTrimmer.toString();
+    }
+
+    private void translate( final Content element ) throws JDOMException {
+        if( element instanceof Text ) {
+            decorateMarkupForText( ( Text )element );
+        } else if( element instanceof Element ) {
+            final Element base = ( Element )element;
+            if( "imageplugin".equals( base.getAttributeValue( "class" ) ) ) {
+                translateImage( base );
+            } else if( "wikiform".equals( base.getAttributeValue( "class" ) ) ) {
                 // only print the children if the div's class="wikiform", but not the div itself.
-                printChildren( base );
+                translateChildren( base );
+            } else {
+                final ElementDecoratorData dto = buildElementDecoratorDataFrom( base );
+                decorateMarkupForElementWith( dto );
             }
-            else
-            {
-                boolean bold = false;
-                boolean italic = false;
-                boolean monospace = false;
-                String cssSpecial = null;
-                final String cssClass = base.getAttributeValue( "class" );
-
-                // accomodate a FCKeditor bug with Firefox: when a link is removed, it becomes <span class="wikipage">text</span>.
-                final boolean ignoredCssClass = cssClass != null && cssClass.matches( "wikipage|createpage|external|interwiki|attachment|inline-code" );
-
-                Map< Object, Object > styleProps = null;
-
-                // Only get the styles if it's not a link element. Styles for link elements are
-                // handled as an AugmentedWikiLink instead.
-                if( !n.equals( "a" ) )
-                {
-                    styleProps = getStylePropertiesLowerCase( base );
-                }
-
-                if("inline-code".equals(cssClass))
-                {
-                    monospace = true;
-                }
-
-                if( styleProps != null )
-                {
-                    final String weight = (String)styleProps.remove( "font-weight" );
-                    final String style = (String)styleProps.remove( "font-style" );
+        }
+    }
 
-                    if( n.equals( "p" ) )
-                    {
-                        // change it so we can print out the css styles for <p>
-                        n = "div";
-                    }
+    void decorateMarkupForString( final String s ) {
+        out.print( StringEscapeUtils.unescapeHtml4( s ) );
+    }
 
-                    italic = "oblique".equals( style ) || "italic".equals( style );
-                    bold = "bold".equals( weight ) || "bolder".equals( weight );
-                    if( !styleProps.isEmpty() )
-                    {
-                        cssSpecial = propsToStyleString( styleProps );
-                    }
-                }
-                if( cssClass != null && !ignoredCssClass )
-                {
-                    if( n.equals( "div" ) )
-                    {
-                        m_out.print( "\n%%" + cssClass + " \n" );
-                    }
-                    else if( n.equals( "span" ) )
-                    {
-                        m_out.print( "%%" + cssClass + " " );
-                    }
-                }
-                if( bold )
-                {
-                    m_out.print( "__" );
-                }
-                if( italic )
-                {
-                    m_out.print( "''" );
-                }
-                if( monospace )
-                {
-                    m_out.print( "{{{" );
-                    m_preStack.push();
-                }
-                if( cssSpecial != null )
-                {
-                    if( n.equals( "div" ) )
-                    {
-                        m_out.print( "\n%%(" + cssSpecial + " )\n" );
-                    }
-                    else
-                    {
-                        m_out.print( "%%(" + cssSpecial + " )" );
-                    }
-                }
-                printChildren( base );
-                if( cssSpecial != null )
-                {
-                    if( n.equals( "div" ) )
-                    {
-                        m_out.print( "\n/%\n" );
-                    }
-                    else
-                    {
-                        m_out.print( "/%" );
-                    }
-                }
-                if( monospace )
-                {
-                    m_preStack.pop();
-                    m_out.print( "}}}" );
-                }
-                if( italic )
-                {
-                    m_out.print( "''" );
-                }
-                if( bold )
-                {
-                    m_out.print( "__" );
-                }
-                if( cssClass != null && !ignoredCssClass )
-                {
-                    if( n.equals( "div" ) )
-                    {
-                        m_out.print( "\n/%\n" );
-                    }
-                    else if( n.equals( "span" ) )
-                    {
-                        m_out.print( "/%" );
-                    }
-                }
-            }
+    void decorateMarkupForText( final Text text ) {
+        String s = text.getText();
+        if( preStack.isEmpty() ) {
+            // remove all "line terminator" characters
+            s = s.replaceAll( "[\\r\\n\\f\\u0085\\u2028\\u2029]", "" );
         }
+        out.print( s );
     }
 
-    private void printChildren(final Element base ) throws IOException, JDOMException
-    {
-        for(final Iterator< Content > i = base.getContent().iterator(); i.hasNext(); )
-        {
-            final Content c = i.next();
-            if( c instanceof Element )
-            {
-                final Element e = (Element)c;
-                final String n = e.getName().toLowerCase();
-                switch ( n ) {
-                    case "h1":
-                        m_out.print( "\n!!! " );
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "h2":
-                        m_out.print( "\n!!! " );
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "h3":
-                        m_out.print( "\n!! " );
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "h4":
-                        m_out.print( "\n! " );
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "p":
-                        if ( e.getContentSize() != 0 ) // we don't want to print empty elements: <p></p>
-                        {
-                            m_out.println();
-                            print( e );
-                            m_out.println();
-                        }
-                        break;
-                    case "br":
-                        if ( m_preStack.isPreMode() ) {
-                            m_out.println();
-                        } else {
-                            final String parentElementName = base.getName().toLowerCase();
-
-                            //
-                            // To beautify the generated wiki markup, we print a newline character after a linebreak.
-                            // It's only safe to do this when the parent element is a <p> or <div>; when the parent
-                            // element is a table cell or list item, a newline character would break the markup.
-                            // We also check that this isn't being done inside a plugin body.
-                            //
-                            if ( parentElementName.matches( "p|div" )
-                                    && !base.getText().matches( "(?s).*\\[\\{.*\\}\\].*" ) ) {
-                                m_out.print( " \\\\\n" );
-                            } else {
-                                m_out.print( " \\\\" );
-                            }
-                        }
-                        print( e );
-                        break;
-                    case "hr":
-                        m_out.println();
-                        print( "----" );
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "table":
-                        if ( !m_outTimmer.isCurrentlyOnLineBegin() ) {
-                            m_out.println();
-                        }
-                        print( e );
-                        break;
-                    case "tr":
-                        print( e );
-                        m_out.println();
-                        break;
-                    case "td":
-                        m_out.print( "| " );
-                        print( e );
-                        if ( !m_preStack.isPreMode() ) {
-                            print( " " );
-                        }
-                        break;
-                    case "th":
-                        m_out.print( "|| " );
-                        print( e );
-                        if ( !m_preStack.isPreMode() ) {
-                            print( " " );
-                        }
-                        break;
-                    case "a":
-                        if ( !isIgnorableWikiMarkupLink( e ) ) {
-                            if ( e.getChild( "IMG" ) != null ) {
-                                printImage( e );
-                            } else {
-                                String ref = e.getAttributeValue( "href" );
-                                if ( ref == null ) {
-                                    if ( isUndefinedPageLink( e ) ) {
-                                        m_out.print( "[" );
-                                        print( e );
-                                        m_out.print( "]" );
-                                    } else {
-                                        print( e );
-                                    }
-                                } else {
-                                    ref = trimLink( ref );
-                                    if ( ref != null ) {
-                                        if ( ref.startsWith( "#" ) ) // This is a link to a footnote.
-                                        {
-                                            // convert "#ref-PageName-1" to just "1"
-                                            final String href = ref.replaceFirst( "#ref-.+-(\\d+)", "$1" );
-
-                                            // remove the brackets around "[1]"
-                                            final String textValue = e.getValue().substring( 1, ( e.getValue().length() - 1 ) );
-
-                                            if ( href.equals( textValue ) ) { // handles the simplest case. Example: [1]
-                                                print( e );
-                                            } else { // handles the case where the link text is different from the href. Example: [something|1]
-                                                m_out.print( "[" + textValue + "|" + href + "]" );
-                                            }
-                                        } else {
-                                            final Map<String, String> augmentedWikiLinkAttributes = getAugmentedWikiLinkAttributes( e );
-
-                                            m_out.print( "[" );
-                                            print( e );
-                                            if ( !e.getTextTrim().equalsIgnoreCase( ref ) ) {
-                                                m_out.print( "|" );
-                                                print( ref );
-
-                                                if ( !augmentedWikiLinkAttributes.isEmpty() ) {
-                                                    m_out.print( "|" );
-
-                                                    final String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
-                                                    m_out.print( augmentedWikiLink );
-                                                }
-                                            } else if ( !augmentedWikiLinkAttributes.isEmpty() ) {
-                                                // If the ref has the same value as the text and also if there
-                                                // are attributes, then just print: [ref|ref|attributes] .
-                                                m_out.print( "|" + ref + "|" );
-                                                final String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
-                                                m_out.print( augmentedWikiLink );
-                                            }
-
-                                            m_out.print( "]" );
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                        break;
-                    case "b":
-                    case "strong":
-                        m_out.print( "__" );
-                        print( e );
-                        m_out.print( "__" );
-                        break;
-                    case "i":
-                    case "em":
-                    case "address":
-                        m_out.print( "''" );
-                        print( e );
-                        m_out.print( "''" );
-                        break;
-                    case "u":
-                        m_out.print( "%%( text-decoration:underline; )" );
-                        print( e );
-                        m_out.print( "/%" );
-                        break;
-                    case "strike":
-                        m_out.print( "%%strike " );
-                        print( e );
-                        m_out.print( "/%" );
-                        // NOTE: don't print a space before or after the double percents because that can break words into two.
-                        // For example: %%(color:red)ABC%%%%(color:green)DEF%% is different from %%(color:red)ABC%% %%(color:green)DEF%%
-                        break;
-                    case "sup":
-                        m_out.print( "%%sup " );
-                        print( e );
-                        m_out.print( "/%" );
-                        break;
-                    case "sub":
-                        m_out.print( "%%sub " );
-                        print( e );
-                        m_out.print( "/%" );
-                        break;
-                    case "dl":
-                        m_out.print( "\n" );
-                        print( e );
-
-                        // print a newline after the definition list. If we don't,
-                        // it may cause problems for the subsequent element.
-                        m_out.print( "\n" );
-                        break;
-                    case "dt":
-                        m_out.print( ";" );
-                        print( e );
-                        break;
-                    case "dd":
-                        m_out.print( ":" );
-                        print( e );
-                        break;
-                    case "ul":
-                        m_out.println();
-                        m_liStack.push( "*" );
-                        print( e );
-                        m_liStack.pop();
-                        break;
-                    case "ol":
-                        m_out.println();
-                        m_liStack.push( "#" );
-                        print( e );
-                        m_liStack.pop();
-                        break;
-                    case "li":
-                        m_out.print( m_liStack + " " );
-                        print( e );
-
-                        // The following line assumes that the XHTML has been "pretty-printed"
-                        // (newlines separate child elements from their parents).
-                        final boolean lastListItem = base.indexOf( e ) == ( base.getContentSize() - 2 );
-                        final boolean sublistItem = m_liStack.toString().length() > 1;
-
-                        // only print a newline if this <li> element is not the last item within a sublist.
-                        if ( !sublistItem || !lastListItem ) {
-                            m_out.println();
-                        }
-                        break;
-                    case "pre":
-                        m_out.print( "\n{{{" ); // start JSPWiki "code blocks" on its own line
-
-                        m_preStack.push();
-                        print( e );
-                        m_preStack.pop();
-
-                        // print a newline after the closing braces
-                        // to avoid breaking any subsequent wiki markup that follows.
-                        m_out.print( "}}}\n" );
-                        break;
-                    case "code":
-                    case "tt":
-                        m_out.print( "{{" );
-                        m_preStack.push();
-                        print( e );
-                        m_preStack.pop();
-                        m_out.print( "}}" );
-                        // NOTE: don't print a newline after the closing brackets because if the Text is inside
-                        // a table or list, it would break it if there was a subsequent row or list item.
-                        break;
-                    case "img":
-                        if ( !isIgnorableWikiMarkupLink( e ) ) {
-                            m_out.print( "[" );
-                            print( trimLink( e.getAttributeValue( "src" ) ) );
-                            m_out.print( "]" );
-                        }
-                        break;
-                    case "form": {
-                        // remove the hidden input where name="formname" since a new one will be generated again when the xhtml is rendered.
-                        final Element formName = getXPathElement( e, "INPUT[@name='formname']" );
-                        if ( formName != null ) {
-                            formName.detach();
-                        }
+    ElementDecoratorData buildElementDecoratorDataFrom( final Element base ) {
+        String n = base.getName().toLowerCase();
+        boolean bold = false;
+        boolean italic = false;
+        boolean monospace = false;
+        String cssSpecial = null;
+        final String cssClass = base.getAttributeValue( "class" );
 
-                        final String name = e.getAttributeValue( "name" );
+        // accomodate a FCKeditor bug with Firefox: when a link is removed, it becomes <span class="wikipage">text</span>.
+        final boolean ignoredCssClass = cssClass != null && cssClass.matches( "wikipage|createpage|external|interwiki|attachment|inline-code" );
 
-                        m_out.print( "\n[{FormOpen" );
+        Map< Object, Object > styleProps = null;
 
-                        if ( name != null ) {
-                            m_out.print( " form='" + name + "'" );
-                        }
+        // Only get the styles if it's not a link element. Styles for link elements are handled as an AugmentedWikiLink instead.
+        if( !n.equals( "a" ) ) {
+            styleProps = getStylePropertiesLowerCase( base );
+        }
 
-                        m_out.print( "}]\n" );
+        if( "inline-code".equals( cssClass ) ) {
+            monospace = true;
+        }
 
-                        print( e );
-                        m_out.print( "\n[{FormClose}]\n" );
-                        break;
-                    }
-                    case "input": {
-                        final String type = e.getAttributeValue( "type" );
-                        String name = e.getAttributeValue( "name" );
-                        final String value = e.getAttributeValue( "value" );
-                        final String checked = e.getAttributeValue( "checked" );
+        if( styleProps != null ) {
+            final String weight = ( String ) styleProps.remove( "font-weight" );
+            final String style = ( String ) styleProps.remove( "font-style" );
 
-                        m_out.print( "[{FormInput" );
+            if ( n.equals( "p" ) ) {
+                // change it, so we can print out the css styles for <p>
+                n = "div";
+            }
 
-                        if ( type != null ) {
-                            m_out.print( " type='" + type + "'" );
-                        }
-                        if ( name != null ) {
-                            // remove the "nbf_" that was prepended since new one will be generated again when the xhtml is rendered.
-                            if ( name.startsWith( "nbf_" ) ) {
-                                name = name.substring( 4, name.length() );
-                            }
-                            m_out.print( " name='" + name + "'" );
-                        }
-                        if ( value != null && !value.equals( "" ) ) {
-                            m_out.print( " value='" + value + "'" );
-                        }
-                        if ( checked != null ) {
-                            m_out.print( " checked='" + checked + "'" );
-                        }
+            italic = "oblique".equals( style ) || "italic".equals( style );
+            bold = "bold".equals( weight ) || "bolder".equals( weight );
+            if ( !styleProps.isEmpty() ) {
+                cssSpecial = propsToStyleString( styleProps );
+            }
+        }
 
-                        m_out.print( "}]" );
+        final ElementDecoratorData dto = new ElementDecoratorData();
+        dto.base = base;
+        dto.bold = bold;
+        dto.cssClass = cssClass;
+        dto.cssSpecial = cssSpecial;
+        dto.htmlBase = n;
+        dto.ignoredCssClass = ignoredCssClass;
+        dto.italic = italic;
+        dto.monospace = monospace;
+        return dto;
+    }
 
-                        print( e );
-                        break;
-                    }
-                    case "textarea": {
-                        String name = e.getAttributeValue( "name" );
-                        final String rows = e.getAttributeValue( "rows" );
-                        final String cols = e.getAttributeValue( "cols" );
+    private Map< Object, Object > getStylePropertiesLowerCase( final Element base ) {
+        final String n = base.getName().toLowerCase();
 
-                        m_out.print( "[{FormTextarea" );
+        // "font-weight: bold; font-style: italic;"
+        String style = base.getAttributeValue( "style" );
+        if( style == null ) {
+            style = "";
+        }
 
-                        if ( name != null ) {
-                            if ( name.startsWith( "nbf_" ) ) {
-                                name = name.substring( 4, name.length() );
-                            }
-                            m_out.print( " name='" + name + "'" );
-                        }
-                        if ( rows != null ) {
-                            m_out.print( " rows='" + rows + "'" );
-                        }
-                        if ( cols != null ) {
-                            m_out.print( " cols='" + cols + "'" );
-                        }
+        if( n.equals( "p" ) || n.equals( "div" ) ) {
+            final String align = base.getAttributeValue( "align" );
+            if( align != null ) {
+                // only add the value of the align attribute if the text-align style didn't already exist.
+                if( !style.contains( "text-align" ) ) {
+                    style += ";text-align:" + align + ";";
+                }
+            }
+        }
 
-                        m_out.print( "}]" );
-                        print( e );
-                        break;
-                    }
-                    case "select": {
-                        String name = e.getAttributeValue( "name" );
+        if( n.equals( "font" ) ) {
+            final String color = base.getAttributeValue( "color" );
+            final String face = base.getAttributeValue( "face" );
+            final String size = base.getAttributeValue( "size" );
+            if( color != null ) {
+                style = style + "color:" + color + ";";
+            }
+            if( face != null ) {
+                style = style + "font-family:" + face + ";";
+            }
+            if( size != null ) {
+                switch ( size ) {
+                    case "1": style += "font-size:xx-small;"; break;
+                    case "2": style += "font-size:x-small;"; break;
+                    case "3": style += "font-size:small;"; break;
+                    case "4": style += "font-size:medium;"; break;
+                    case "5": style += "font-size:large;"; break;
+                    case "6": style += "font-size:x-large;"; break;
+                    case "7": style += "font-size:xx-large;"; break;
+                }
+            }
+        }
 
-                        m_out.print( "[{FormSelect" );
+        if( style.equals( "" ) ) {
+            return null;
+        }
 
-                        if ( name != null ) {
-                            if ( name.startsWith( "nbf_" ) ) {
-                                name = name.substring( 4, name.length() );
-                            }
-                            m_out.print( " name='" + name + "'" );
-                        }
+        final Map< Object, Object > m = new LinkedHashMap<>();
+        Arrays.stream( style.toLowerCase().split( ";" ) )
+              .filter( StringUtils::isNotEmpty )
+              .forEach( prop -> m.put( prop.split( ":" )[ 0 ].trim(), prop.split( ":" )[ 1 ].trim() ) );
+        return m;
+    }
 
-                        m_out.print( " value='" );
-                        print( e );
-                        m_out.print( "'}]" );
-                        break;
-                    }
-                    case "option": {
-                        // If this <option> element isn't the second child element within the parent <select>
-                        // element, then we need to print a semicolon as a separator. (The first child element
-                        // is expected to be a newline character which is at index of 0).
-                        if ( base.indexOf( e ) != 1 ) {
-                            m_out.print( ";" );
-                        }
+    private String propsToStyleString( final Map< Object, Object >  styleProps ) {
+        final StringBuilder style = new StringBuilder();
+        for( final Map.Entry< Object, Object > entry : styleProps.entrySet() ) {
+            style.append( " " ).append( entry.getKey() ).append( ": " ).append( entry.getValue() ).append( ";" );
+        }
+        return style.toString();
+    }
 
-                        final Attribute selected = e.getAttribute( "selected" );
-                        if ( selected != null ) {
-                            m_out.print( "*" );
-                        }
+    void decorateMarkupForElementWith( final ElementDecoratorData dto ) throws JDOMException {
+        decorateMarkupForCssClass( dto );
+    }
 
-                        final String value = e.getAttributeValue( "value" );
-                        if ( value != null ) {
-                            m_out.print( value );
-                        } else {
-                            print( e );
-                        }
-                        break;
-                    }
-                    default:
-                        print( e );
-                        break;
-                }
+    void decorateMarkupForCssClass( final ElementDecoratorData dto ) throws JDOMException {
+        if( dto.cssClass != null && !dto.ignoredCssClass ) {
+            if ( dto.htmlBase.equals( "div" ) ) {
+                out.print( "\n%%" + dto.cssClass + " \n" );
+            } else if ( dto.htmlBase.equals( "span" ) ) {
+                out.print( "%%" + dto.cssClass + " " );
             }
-            else
-            {
-                print( c );
+        }
+        decorateMarkupForBold( dto );
+        if( dto.cssClass != null && !dto.ignoredCssClass ) {
+            if ( dto.htmlBase.equals( "div" ) ) {
+                out.print( "\n/%\n" );
+            } else if ( dto.htmlBase.equals( "span" ) ) {
+                out.print( "/%" );
             }
         }
     }
 
-    private void printImage(final Element base )
-    {
-        Element child = getXPathElement( base, "TBODY/TR/TD/*" );
-        if( child == null )
-        {
-            child = base;
+    void decorateMarkupForBold( final ElementDecoratorData dto ) throws JDOMException {
+        if( dto.bold ) {
+            out.print( "__" );
         }
-        final Element img;
-        final String href;
-        final Map<Object,Object> map = new ForgetNullValuesLinkedHashMap<>();
-        if( child.getName().equals( "A" ) )
-        {
-            img = child.getChild( "IMG" );
-            href = child.getAttributeValue( "href" );
+        decorateMarkupForItalic( dto );
+        if( dto.bold ) {
+            out.print( "__" );
         }
-        else
-        {
-            img = child;
-            href = null;
+    }
+
+    void decorateMarkupForItalic( final ElementDecoratorData dto ) throws JDOMException {
+        if( dto.italic ) {
+            out.print( "''" );
         }
-        if( img == null )
-        {
-            return;
+        decorateMarkupForMonospace( dto );
+        if( dto.italic ) {
+            out.print( "''" );
         }
-        final String src = trimLink( img.getAttributeValue( "src" ) );
-        if( src == null )
-        {
-            return;
+    }
+
+    void decorateMarkupForMonospace( final ElementDecoratorData dto ) throws JDOMException {
+        if( dto.monospace ) {
+            out.print( "{{{" );
+            preStack.push( "{{{" );
+        }
+        decorateMarkupForCssSpecial( dto );
+        if( dto.monospace ) {
+            preStack.pop();
+            out.print( "}}}" );
+        }
+    }
+
+    void decorateMarkupForCssSpecial( final ElementDecoratorData dto ) throws JDOMException {
+        if( dto.cssSpecial != null ) {
+            if ( dto.htmlBase.equals( "div" ) ) {
+                out.print( "\n%%(" + dto.cssSpecial + " )\n" );
+            } else {
+                out.print( "%%(" + dto.cssSpecial + " )" );
+            }
+        }
+        translateChildren( dto.base );
+        if( dto.cssSpecial != null ) {
+            if ( dto.htmlBase.equals( "div" ) ) {
+                out.print( "\n/%\n" );
+            } else {
+                out.print( "/%" );
+            }
         }
-        map.put( "align", base.getAttributeValue( "align" ) );
-        map.put( "height", img.getAttributeValue( "height" ) );
-        map.put( "width", img.getAttributeValue( "width" ) );
-        map.put( "alt", img.getAttributeValue( "alt" ) );
-        map.put( "caption", emptyToNull( ( Element )XPathFactory.instance().compile(  "CAPTION" ).evaluateFirst( base ) ) );
-        map.put( "link", href );
-        map.put( "border", img.getAttributeValue( "border" ) );
-        map.put( "style", base.getAttributeValue( "style" ) );
-        if( map.size() > 0 )
-        {
-            m_out.print( "[{Image src='" + src + "'" );
-            for(final Iterator i = map.entrySet().iterator(); i.hasNext(); )
-            {
-                final Map.Entry entry = (Map.Entry)i.next();
-                if( !entry.getValue().equals( "" ) )
-                {
-                    m_out.print( " " + entry.getKey() + "='" + entry.getValue() + "'" );
+    }
+
+    private void translateChildren( final Element base ) throws JDOMException {
+        for( final Content c : base.getContent() ) {
+            if ( c instanceof Element ) {
+                final Element e = ( Element )c;
+                final String n = e.getName().toLowerCase();
+                switch( n ) {
+                    case "h1": decorateMarkupForH1( e ); break;
+                    case "h2": decorateMarkupForH2( e ); break;
+                    case "h3": decorateMarkupForH3( e ); break;
+                    case "h4": decorateMarkupForH4( e ); break;
+                    case "p": decorateMarkupForP( e ); break;
+                    case "br": decorateMarkupForBR( base, e ); break;
+                    case "hr": decorateMarkupForHR( e ); break;
+                    case "table": decorateMarkupForTable( e ); break;
+                    case "tr": decorateMarkupForTR( e ); break;
+                    case "td": decorateMarkupForTD( e ); break;
+                    case "th": decorateMarkupForTH( e ); break;
+                    case "a": decorateMarkupForA( e ); break;
+                    case "b":
+                    case "strong": decorateMarkupForStrong( e ); break;
+                    case "i":
+                    case "em":
+                    case "address": decorateMarkupForEM( e ); break;
+                    case "u": decorateMarkupForUnderline( e ); break;
+                    case "strike": decorateMarkupForStrike( e ); break;
+                    case "sup": decorateMarkupForSup( e ); break;
+                    case "sub": decorateMarkupForSub( e ); break;
+                    case "dl": decorateMarkupForDL( e ); break;
+                    case "dt": decorateMarkupForDT( e ); break;
+                    case "dd": decorateMarkupForDD( e ); break;
+                    case "ul": decorateMarkupForUL( e ); break;
+                    case "ol": decorateMarkupForOL( e ); break;
+                    case "li": decorateMarkupForLI( base, e ); break;
+                    case "pre": decorateMarkupForPre( e ); break;
+                    case "code":
+                    case "tt": decorateMarkupForCode( e ); break;
+                    case "img": decorateMarkupForImg( e ); break;
+                    case "form": decorateMarkupForForm( e ); break;
+                    case "input": decorateMarkupForInput( e ); break;
+                    case "textarea": decorateMarkupForTextarea( e ); break;
+                    case "select": decorateMarkupForSelect( e ); break;
+                    case "option": decorateMarkupForOption( base, e ); break;
+                    default: translate( e ); break;
                 }
+            } else {
+                translate( c );
             }
-            m_out.print( "}]" );
         }
-        else
-        {
-            m_out.print( "[" + src + "]" );
+    }
+
+    void decorateMarkupForH1( final Element e ) throws JDOMException {
+        out.print( "\n!!! " );
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForH2( final Element e ) throws JDOMException {
+        out.print( "\n!!! " );
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForH3( final Element e ) throws JDOMException {
+        out.print( "\n!! " );
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForH4( final Element e ) throws JDOMException {
+        out.print( "\n! " );
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForP( final Element e ) throws JDOMException {
+        if ( e.getContentSize() != 0 ) { // we don't want to print empty elements: <p></p>
+            out.println();
+            translate( e );
+            out.println();
         }
     }
 
-    Element getXPathElement( final Element base, final String expression ) {
-        final List< ? > nodes = XPathFactory.instance().compile( expression ).evaluate( base );
-        if( nodes == null || nodes.size() == 0 ) {
-            return null;
+    void decorateMarkupForHR( final Element e ) throws JDOMException {
+        out.println();
+        decorateMarkupForString( "----" );
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForBR( final Element base, final Element e ) throws JDOMException {
+        if ( !preStack.isEmpty() ) {
+            out.println();
         } else {
-            return ( Element )nodes.get( 0 );
+            final String parentElementName = base.getName().toLowerCase();
+
+            // To beautify the generated wiki markup, we print a newline character after a linebreak.
+            // It's only safe to do this when the parent element is a <p> or <div>; when the parent
+            // element is a table cell or list item, a newline character would break the markup.
+            // We also check that this isn't being done inside a plugin body.
+            if ( parentElementName.matches( "p|div" ) && !base.getText().matches( "(?s).*\\[\\{.*\\}\\].*" ) ) {
+                out.print( " \\\\\n" );
+            } else {
+                out.print( " \\\\" );
+            }
         }
+        translate( e );
     }
 
-    private String emptyToNull( final Element e ) {
-        if( e == null ) {
-            return null;
+    void decorateMarkupForTable( final Element e ) throws JDOMException {
+        if ( !outTrimmer.isCurrentlyOnLineBegin() ) {
+            out.println();
         }
-        final String s = e.getText();
-        return s == null ? null : ( s.replaceAll( "\\s", "" ).isEmpty() ? null : s );
+        translate( e );
     }
 
-    private String propsToStyleString( final Map< Object, Object >  styleProps ) {
-    	final StringBuilder style = new StringBuilder();
-        for(final Iterator< Map.Entry< Object, Object > > i = styleProps.entrySet().iterator(); i.hasNext(); ) {
-            final Map.Entry< Object, Object > entry = i.next();
-            style.append( " " ).append( entry.getKey() ).append( ": " ).append( entry.getValue() ).append( ";" );
+    void decorateMarkupForTR( final Element e ) throws JDOMException {
+        translate( e );
+        out.println();
+    }
+
+    void decorateMarkupForTD( final Element e ) throws JDOMException {
+        out.print( "| " );
+        translate( e );
+        if( preStack.isEmpty() ) {
+            decorateMarkupForString( " " );
         }
-        return style.toString();
     }
 
-    private boolean isIgnorableWikiMarkupLink( final Element a ) {
-        final String ref = a.getAttributeValue( "href" );
-        final String clazz = a.getAttributeValue( "class" );
-        return ( ref != null && ref.startsWith( m_config.getPageInfoJsp() ) )
-            || ( clazz != null && clazz.trim().equalsIgnoreCase( m_config.getOutlink() ) );
+    void decorateMarkupForTH( final Element e ) throws JDOMException {
+        out.print( "|| " );
+        translate( e );
+        if( preStack.isEmpty() ) {
+            decorateMarkupForString( " " );
+        }
+    }
+
+    void decorateMarkupForA( final Element e ) throws JDOMException {
+        if( isNotIgnorableWikiMarkupLink( e ) ) {
+            if( e.getChild( "IMG" ) != null ) {
+                translateImage( e );
+            } else {
+                String ref = e.getAttributeValue( "href" );
+                if ( ref == null ) {
+                    if ( isUndefinedPageLink( e ) ) {
+                        out.print( "[" );
+                        translate( e );
+                        out.print( "]" );
+                    } else {
+                        translate( e );
+                    }
+                } else {
+                    ref = trimLink( ref );
+                    if ( ref != null ) {
+                        if ( ref.startsWith( "#" ) ) { // This is a link to a footnote.
+                            // convert "#ref-PageName-1" to just "1"
+                            final String href = ref.replaceFirst( "#ref-.+-(\\d+)", "$1" );
+
+                            // remove the brackets around "[1]"
+                            final String textValue = e.getValue().substring( 1, ( e.getValue().length() - 1 ) );
+
+                            if ( href.equals( textValue ) ) { // handles the simplest case. Example: [1]
+                                translate( e );
+                            } else { // handles the case where the link text is different from the href. Example: [something|1]
+                                out.print( "[" + textValue + "|" + href + "]" );
+                            }
+                        } else {
+                            final Map< String, String > augmentedWikiLinkAttributes = getAugmentedWikiLinkAttributes( e );
+
+                            out.print( "[" );
+                            translate( e );
+                            if ( !e.getTextTrim().equalsIgnoreCase( ref ) ) {
+                                out.print( "|" );
+                                decorateMarkupForString( ref );
+
+                                if ( !augmentedWikiLinkAttributes.isEmpty() ) {
+                                    out.print( "|" );
+
+                                    final String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
+                                    out.print( augmentedWikiLink );
+                                }
+                            } else if ( !augmentedWikiLinkAttributes.isEmpty() ) {
+                                // If the ref has the same value as the text and also if there
+                                // are attributes, then just print: [ref|ref|attributes] .
+                                out.print( "|" + ref + "|" );
+                                final String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
+                                out.print( augmentedWikiLink );
+                            }
+
+                            out.print( "]" );
+                        }
+                    }
+                }
+            }
+        }
     }
 
     /**
      * Checks if the link points to an undefined page.
      */
-    private boolean isUndefinedPageLink(final Element a)
-    {
+    private boolean isUndefinedPageLink( final Element a ) {
         final String classVal = a.getAttributeValue( "class" );
-
         return "createpage".equals( classVal );
     }
 
     /**
      *  Returns a Map containing the valid augmented wiki link attributes.
      */
-    private Map<String,String> getAugmentedWikiLinkAttributes(final Element a )
-    {
-        final Map<String,String> attributesMap = new HashMap<>();
-
-        final String id = a.getAttributeValue( "id" );
-        if( id != null && !id.equals( "" ) )
-        {
-            attributesMap.put( "id", id.replaceAll( "'", "\"" ) );
-        }
-
+    private Map< String, String > getAugmentedWikiLinkAttributes( final Element a ) {
+        final Map< String, String > attributesMap = new HashMap<>();
         final String cssClass = a.getAttributeValue( "class" );
-        if( cssClass != null && !cssClass.equals( "" )
-            && !cssClass.matches( "wikipage|createpage|external|interwiki|attachment" ) )
-        {
+        if( StringUtils.isNotEmpty( cssClass )
+                && !cssClass.matches( "wikipage|createpage|external|interwiki|attachment" ) ) {
             attributesMap.put( "class", cssClass.replaceAll( "'", "\"" ) );
         }
+        addAttributeIfPresent( a, attributesMap, "accesskey" );
+        addAttributeIfPresent( a, attributesMap, "charset" );
+        addAttributeIfPresent( a, attributesMap, "dir" );
+        addAttributeIfPresent( a, attributesMap, "hreflang" );
+        addAttributeIfPresent( a, attributesMap, "id" );
+        addAttributeIfPresent( a, attributesMap, "lang" );
+        addAttributeIfPresent( a, attributesMap, "rel" );
+        addAttributeIfPresent( a, attributesMap, "rev" );
+        addAttributeIfPresent( a, attributesMap, "style" );
+        addAttributeIfPresent( a, attributesMap, "tabindex" );
+        addAttributeIfPresent( a, attributesMap, "target" );
+        addAttributeIfPresent( a, attributesMap, "title" );
+        addAttributeIfPresent( a, attributesMap, "type" );
+        return attributesMap;
+    }
 
-        final String style = a.getAttributeValue( "style" );
-        if( style != null && !style.equals( "" ) )
-        {
-            attributesMap.put( "style", style.replaceAll( "'", "\"" ) );
+    private void addAttributeIfPresent( final Element a, final Map< String, String > attributesMap, final String attribute ) {
+        final String attr = a.getAttributeValue( attribute );
+        if( StringUtils.isNotEmpty( attr ) ) {
+            attributesMap.put( attribute, attr.replaceAll( "'", "\"" ) );
         }
+    }
 
-        final String title = a.getAttributeValue( "title" );
-        if( title != null && !title.equals( "" ) )
-        {
-            attributesMap.put( "title", title.replaceAll( "'", "\"" ) );
-        }
+    /**
+     * Converts the entries in the map to a string for use in a wiki link.
+     */
+    private String augmentedWikiLinkMapToString( final Map< String, String > attributesMap ) {
+        final StringBuilder sb = new StringBuilder();
+        for( final Map.Entry< String, String > entry : attributesMap.entrySet() ) {
+            final String attributeName = entry.getKey();
+            final String attributeValue = entry.getValue();
 
-        final String lang = a.getAttributeValue( "lang" );
-        if( lang != null && !lang.equals( "" ) )
-        {
-            attributesMap.put( "lang", lang.replaceAll( "'", "\"" ) );
+            sb.append( " " ).append( attributeName ).append( "='" ).append( attributeValue ).append( "'" );
         }
 
-        final String dir = a.getAttributeValue( "dir" );
-        if( dir != null && !dir.equals( "" ) )
-        {
-            attributesMap.put( "dir", dir.replaceAll( "'", "\"" ) );
-        }
+        return sb.toString().trim();
+    }
 
-        final String charset = a.getAttributeValue( "charset" );
-        if( charset != null && !charset.equals("") )
-        {
-            attributesMap.put( "charset", charset.replaceAll( "'", "\"" ) );
-        }
+    void decorateMarkupForStrong( final Element e ) throws JDOMException {
+        out.print( "__" );
+        translate( e );
+        out.print( "__" );
+    }
 
-        final String type = a.getAttributeValue( "type" );
-        if( type != null && !type.equals( "" ) )
-        {
-            attributesMap.put( "type", type.replaceAll( "'", "\"" ) );
-        }
+    void decorateMarkupForEM( final Element e ) throws JDOMException {
+        out.print( "''" );
+        translate( e );
+        out.print( "''" );
+    }
+
+    void decorateMarkupForUnderline( final Element e ) throws JDOMException {
+        out.print( "%%( text-decoration:underline; )" );
+        translate( e );
+        out.print( "/%" );
+    }
+
+    void decorateMarkupForStrike( final Element e ) throws JDOMException {
+        out.print( "%%strike " );
+        translate( e );
+        out.print( "/%" );
+        // NOTE: don't print a space before or after the double percents because that can break words into two.
+        // For example: %%(color:red)ABC%%%%(color:green)DEF%% is different from %%(color:red)ABC%% %%(color:green)DEF%%
+    }
+
+    void decorateMarkupForSup( final Element e ) throws JDOMException {
+        out.print( "%%sup " );
+        translate( e );
+        out.print( "/%" );
+    }
+
+    void decorateMarkupForSub( final Element e ) throws JDOMException {
+        out.print( "%%sub " );
+        translate( e );
+        out.print( "/%" );
+    }
+
+    void decorateMarkupForDL( final Element e ) throws JDOMException {
+        out.print( "\n" );
+        translate( e );
+
+        // print a newline after the definition list. If we don't,
+        // it may cause problems for the subsequent element.
+        out.print( "\n" );
+    }
+
+    void decorateMarkupForDT( final Element e ) throws JDOMException {
+        out.print( ";" );
+        translate( e );
+    }
+
+    void decorateMarkupForDD( final Element e ) throws JDOMException {
+        out.print( ":" );
+        translate( e );
+    }
+
+    void decorateMarkupForUL( final Element e ) throws JDOMException {
+        out.println();
+        liStack.push( "*" );
+        translate( e );
+        liStack.pop();
+    }
+
+    void decorateMarkupForOL( final Element e ) throws JDOMException {
+        out.println();
+        liStack.push( "#" );
+        translate( e );
+        liStack.pop();
+    }
 
-        final String hreflang = a.getAttributeValue( "hreflang" );
-        if( hreflang != null && !hreflang.equals( "" ) )
-        {
-            attributesMap.put( "hreflang", hreflang.replaceAll( "'", "\"" ) );
+    void decorateMarkupForLI( final Element base, final Element e ) throws JDOMException {
+        out.print( String.join( "", liStack ) + " " );
+        translate( e );
+
+        // The following line assumes that the XHTML has been "pretty-printed"
+        // (newlines separate child elements from their parents).
+        final boolean lastListItem = base.indexOf( e ) == ( base.getContentSize() - 2 );
+        final boolean sublistItem = liStack.size() > 1;
+
+        // only print a newline if this <li> element is not the last item within a sublist.
+        if ( !sublistItem || !lastListItem ) {
+            out.println();
         }
+    }
+
+    void decorateMarkupForPre( final Element e ) throws JDOMException {
+        out.print( "\n{{{" ); // start JSPWiki "code blocks" on its own line
+
+        preStack.push( "\n{{{" );
+        translate( e );
+        preStack.pop();
 
-        final String rel = a.getAttributeValue( "rel" );
-        if( rel != null && !rel.equals( "" ) )
-        {
-            attributesMap.put( "rel", rel.replaceAll( "'", "\"" ) );
+        // print a newline after the closing braces to avoid breaking any subsequent wiki markup that follows.
+        out.print( "}}}\n" );
+    }
+
+    void decorateMarkupForCode( final Element e ) throws JDOMException {
+        out.print( "{{" );
+        preStack.push( "{{" );
+        translate( e );
+        preStack.pop();
+        out.print( "}}" );
+        // NOTE: don't print a newline after the closing brackets because if the Text is inside
+        // a table or list, it would break it if there was a subsequent row or list item.
+    }
+
+    void decorateMarkupForImg( final Element e ) {
+        if( isNotIgnorableWikiMarkupLink( e ) ) {
+            out.print( "[" );
+            decorateMarkupForString( trimLink( e.getAttributeValue( "src" ) ) );
+            out.print( "]" );
         }
+    }
 
-        final String rev = a.getAttributeValue( "rev" );
-        if( rev != null && !rev.equals( "" ) )
-        {
-            attributesMap.put( "rev", rev.replaceAll( "'", "\"" ) );
+    void decorateMarkupForForm( final Element e ) throws JDOMException {
+        // remove the hidden input where name="formname" since a new one will be generated again when the xhtml is rendered.
+        final Element formName = XmlUtil.getXPathElement( e, "INPUT[@name='formname']" );
+        if ( formName != null ) {
+            formName.detach();
         }
 
-        final String accesskey = a.getAttributeValue( "accesskey" );
-        if( accesskey != null && !accesskey.equals( "" ) )
-        {
-            attributesMap.put( "accesskey", accesskey.replaceAll( "'", "\"" ) );
+        final String name = e.getAttributeValue( "name" );
+
+        out.print( "\n[{FormOpen" );
+
+        if( name != null ) {
+            out.print( " form='" + name + "'" );
         }
 
-        final String tabindex = a.getAttributeValue( "tabindex" );
-        if( tabindex != null && !tabindex.equals( "" ) )
-        {
-            attributesMap.put( "tabindex", tabindex.replaceAll( "'", "\"" ) );
+        out.print( "}]\n" );
+
+        translate( e );
+        out.print( "\n[{FormClose}]\n" );
+    }
+
+    void decorateMarkupForInput( final Element e ) throws JDOMException {
+        final String type = e.getAttributeValue( "type" );
+        String name = e.getAttributeValue( "name" );
+        final String value = e.getAttributeValue( "value" );
+        final String checked = e.getAttributeValue( "checked" );
+
+        out.print( "[{FormInput" );
+
+        if ( type != null ) {
+            out.print( " type='" + type + "'" );
+        }
+        if ( name != null ) {
+            // remove the "nbf_" that was prepended since new one will be generated again when the xhtml is rendered.
+            if ( name.startsWith( "nbf_" ) ) {
+                name = name.substring( 4 );
+            }
+            out.print( " name='" + name + "'" );
+        }
+        if ( value != null && !value.equals( "" ) ) {
+            out.print( " value='" + value + "'" );
         }
+        if ( checked != null ) {
+            out.print( " checked='" + checked + "'" );
+        }
+
+        out.print( "}]" );
+
+        translate( e );
+    }
+
+    void decorateMarkupForTextarea( final Element e ) throws JDOMException {
+        String name = e.getAttributeValue( "name" );
+        final String rows = e.getAttributeValue( "rows" );
+        final String cols = e.getAttributeValue( "cols" );
 
-        final String target = a.getAttributeValue( "target" );
-        if( target != null && !target.equals( "" ) )
-        {
-            attributesMap.put( "target", target.replaceAll( "'", "\"" ) );
+        out.print( "[{FormTextarea" );
+
+        if ( name != null ) {
+            if ( name.startsWith( "nbf_" ) ) {
+                name = name.substring( 4 );
+            }
+            out.print( " name='" + name + "'" );
+        }
+        if ( rows != null ) {
+            out.print( " rows='" + rows + "'" );
+        }
+        if ( cols != null ) {
+            out.print( " cols='" + cols + "'" );
         }
 
-        return attributesMap;
+        out.print( "}]" );
+        translate( e );
     }
 
-    /**
-     * Converts the entries in the map to a string for use in a wiki link.
-     */
-    private String augmentedWikiLinkMapToString(final Map attributesMap )
-    {
-    	final StringBuilder sb = new StringBuilder();
+    void decorateMarkupForSelect( final Element e ) throws JDOMException {
+        String name = e.getAttributeValue( "name" );
 
-        for (final Iterator itr = attributesMap.entrySet().iterator(); itr.hasNext(); )
-        {
-            final Map.Entry entry = (Map.Entry)itr.next();
-            final String attributeName = (String)entry.getKey();
-            final String attributeValue = (String)entry.getValue();
+        out.print( "[{FormSelect" );
 
-            sb.append( " " + attributeName + "='" + attributeValue + "'" );
+        if ( name != null ) {
+            if ( name.startsWith( "nbf_" ) ) {
+                name = name.substring( 4 );
+            }
+            out.print( " name='" + name + "'" );
         }
 
-        return sb.toString().trim();
+        out.print( " value='" );
+        translate( e );
+        out.print( "'}]" );
     }
 
-    private Map< Object, Object > getStylePropertiesLowerCase(final Element base ) throws IOException
-    {
-        final String n = base.getName().toLowerCase();
+    void decorateMarkupForOption( final Element base, final Element e ) throws JDOMException {
+        // If this <option> element isn't the second child element within the parent <select>
+        // element, then we need to print a semicolon as a separator. (The first child element
+        // is expected to be a newline character which is at index of 0).
+        if ( base.indexOf( e ) != 1 ) {
+            out.print( ";" );
+        }
 
-        //"font-weight: bold; font-style: italic;"
-        String style = base.getAttributeValue( "style" );
-        if( style == null )
-        {
-            style = "";
+        final Attribute selected = e.getAttribute( "selected" );
+        if ( selected != null ) {
+            out.print( "*" );
         }
 
-        if( n.equals( "p" ) || n.equals( "div" ) )
-        {
-            final String align = base.getAttributeValue( "align" );
-            if( align != null )
-            {
-                // only add the value of the align attribute if the text-align style didn't already exist.
-                if( style.indexOf( "text-align" ) == -1 )
-                {
-                    style = style + ";text-align:" + align + ";";
-                }
-            }
+        final String value = e.getAttributeValue( "value" );
+        if ( value != null ) {
+            out.print( value );
+        } else {
+            translate( e );
         }
+    }
 
+    private void translateImage( final Element base ) {
+        Element child = XmlUtil.getXPathElement( base, "TBODY/TR/TD/*" );
+        if( child == null ) {
+            child = base;
+        }
+        final Element img;
+        final String href;
+        if( child.getName().equals( "A" ) ) {
+            img = child.getChild( "IMG" );
+            href = child.getAttributeValue( "href" );
+        } else {
+            img = child;
+            href = null;
+        }
+        if( img == null ) {
+            return;
+        }
+        final String src = trimLink( img.getAttributeValue( "src" ) );
+        if( src == null ) {
+            return;
+        }
 
+        final Map< String, Object > imageAttrs = new LinkedHashMap<>();
+        putIfNotEmpty( imageAttrs, "align", base.getAttributeValue( "align" ) );
+        putIfNotEmpty( imageAttrs, "height", img.getAttributeValue( "height" ) );
+        putIfNotEmpty( imageAttrs, "width", img.getAttributeValue( "width" ) );
+        putIfNotEmpty( imageAttrs, "alt", img.getAttributeValue( "alt" ) );
+        putIfNotEmpty( imageAttrs, "caption", emptyToNull( ( Element )XPathFactory.instance().compile(  "CAPTION" ).evaluateFirst( base ) ) );
+        putIfNotEmpty( imageAttrs, "link", href );
+        putIfNotEmpty( imageAttrs, "border", img.getAttributeValue( "border" ) );
+        putIfNotEmpty( imageAttrs, "style", base.getAttributeValue( "style" ) );
+        decorateMarkupforImage( src, imageAttrs );
+    }
 
-        if( n.equals( "font" ) )
-        {
-            final String color = base.getAttributeValue( "color" );
-            final String face = base.getAttributeValue( "face" );
-            final String size = base.getAttributeValue( "size" );
-            if( color != null )
-            {
-                style = style + "color:" + color + ";";
-            }
-            if( face != null )
-            {
-                style = style + "font-family:" + face + ";";
-            }
-            if( size != null )
-            {
-                switch ( size ) {
-                    case "1":
-                        style = style + "font-size:xx-small;";
-                        break;
-                    case "2":
-                        style = style + "font-size:x-small;";
-                        break;
-                    case "3":
-                        style = style + "font-size:small;";
-                        break;
-                    case "4":
-                        style = style + "font-size:medium;";
-                        break;
-                    case "5":
-                        style = style + "font-size:large;";
-                        break;
-                    case "6":
-                        style = style + "font-size:x-large;";
-                        break;
-                    case "7":
-                        style = style + "font-size:xx-large;";
-                        break;
-                }
-            }
+    private void putIfNotEmpty( final Map< String, Object > map, final String key, final Object value ) {
+        if( value != null ) {
+            map.put( key, value );
         }
+    }
 
-        if( style.equals( "" ) )
-        {
+    private String emptyToNull( final Element e ) {
+        if( e == null ) {
             return null;
         }
+        final String s = e.getText();
+        return s == null ? null : ( s.replaceAll( "\\s", "" ).isEmpty() ? null : s );
+    }
 
-        style = style.replace( ';', '\n' ).toLowerCase();
-        final LinkedHashMap< Object, Object > m = new LinkedHashMap<>();
-        new PersistentMapDecorator( m ).load( new ByteArrayInputStream( style.getBytes() ) );
-        return m;
+    void decorateMarkupforImage( final String src, final Map< String, Object > imageAttrs ) {
+        if( imageAttrs.isEmpty() ) {
+            out.print( "[" + src + "]" );
+        } else {
+            out.print( "[{Image src='" + src + "'" );
+            for( final Map.Entry< String, Object > objectObjectEntry : imageAttrs.entrySet() ) {
+                if ( !objectObjectEntry.getValue().equals( "" ) ) {
+                    out.print( " " + objectObjectEntry.getKey() + "='" + objectObjectEntry.getValue() + "'" );
+                }
+            }
+            out.print( "}]" );
+        }
+    }
+
+    private boolean isNotIgnorableWikiMarkupLink( final Element a ) {
+        final String ref = a.getAttributeValue( "href" );
+        final String clazz = a.getAttributeValue( "class" );
+        return ( ref == null || !ref.startsWith( config.getPageInfoJsp() ) )
+                && ( clazz == null || !clazz.trim().equalsIgnoreCase( config.getOutlink() ) );
     }
 
-    private String trimLink( String ref )
-    {
-        if( ref == null )
-        {
+    private String trimLink( String ref ) {
+        if( ref == null ) {
             return null;
         }
-        try
-        {
+        try {
             ref = URLDecoder.decode( ref, StandardCharsets.UTF_8.name() );
             ref = ref.trim();
-            if( ref.startsWith( m_config.getAttachPage() ) )
-            {
-                ref = ref.substring( m_config.getAttachPage().length() );
+            if( ref.startsWith( config.getAttachPage() ) ) {
+                ref = ref.substring( config.getAttachPage().length() );
             }
-            if( ref.startsWith( m_config.getWikiJspPage() ) )
-            {
-                ref = ref.substring( m_config.getWikiJspPage().length() );
+            if( ref.startsWith( config.getWikiJspPage() ) ) {
+                ref = ref.substring( config.getWikiJspPage().length() );
 
                 // Handle links with section anchors.
                 // For example, we need to translate the html string "TargetPage#section-TargetPage-Heading2"
                 // to this wiki string "TargetPage#Heading2".
                 ref = ref.replaceFirst( ".+#section-(.+)-(.+)", "$1#$2" );
             }
-            if( ref.startsWith( m_config.getEditJspPage() ) )
-            {
-                ref = ref.substring( m_config.getEditJspPage().length() );
+            if( ref.startsWith( config.getEditJspPage() ) ) {
+                ref = ref.substring( config.getEditJspPage().length() );
             }
-            if( m_config.getPageName() != null )
-            {
-                if( ref.startsWith( m_config.getPageName() ) )
-                {
-                    ref = ref.substring( m_config.getPageName().length() );
+            if( config.getPageName() != null ) {
+                if( ref.startsWith( config.getPageName() ) ) {
+                    ref = ref.substring( config.getPageName().length() );
                 }
             }
-        }
-        catch ( final UnsupportedEncodingException e )
-        {
+        } catch ( final UnsupportedEncodingException e ) {
             // Shouldn't happen...
         }
         return ref;
     }
 
-    // FIXME: These should probably be better used with java.util.Stack
-
-    private static class LiStack
-    {
+    private class PreStack extends Stack< String > {
 
-        private StringBuffer m_li = new StringBuffer();
-
-        public void push(final String c )
-        {
-            m_li.append( c );
+        public String push( final String item ) {
+            final String push = super.push( item );
+            outTrimmer.setWhitespaceTrimMode( isEmpty() );
+            return push;
         }
 
-        public void pop()
-        {
-            m_li = m_li.deleteCharAt( m_li.length()-1 );
-            // m_li = m_li.substring( 0, m_li.length() - 1 );
-        }
-
-        @Override
-        public String toString()
-        {
-            return m_li.toString();
+        public String pop() {
+            final String pop = super.pop();
+            outTrimmer.setWhitespaceTrimMode( isEmpty() );
+            return pop;
         }
 
     }
 
-    private class PreStack
-    {
-
-        private int m_pre;
-
-        public boolean isPreMode()
-        {
-            return m_pre > 0;
-        }
-
-        public void push()
-        {
-            m_pre++;
-            m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
-        }
-
-        public void pop()
-        {
-            m_pre--;
-            m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
-        }
-
+    static class ElementDecoratorData {
+        Element base;
+        String htmlBase;
+        String cssClass;
+        String cssSpecial;
+        boolean monospace;
+        boolean bold;
+        boolean italic;
+        boolean ignoredCssClass;
     }
 
 }
diff --git a/jspwiki-util/src/main/java/org/apache/wiki/util/XmlUtil.java b/jspwiki-util/src/main/java/org/apache/wiki/util/XmlUtil.java
index 3da8bf7..1b48cb6 100644
--- a/jspwiki-util/src/main/java/org/apache/wiki/util/XmlUtil.java
+++ b/jspwiki-util/src/main/java/org/apache/wiki/util/XmlUtil.java
@@ -134,4 +134,13 @@ public final class XmlUtil  {
 		return sb.toString();
 	}
 
+	public static Element getXPathElement( final Element base, final String expression ) {
+		final List< ? > nodes = XPathFactory.instance().compile( expression ).evaluate( base );
+		if( nodes == null || nodes.size() == 0 ) {
+			return null;
+		} else {
+			return ( Element )nodes.get( 0 );
+		}
+	}
+
 }
\ No newline at end of file
diff --git a/jspwiki-util/src/test/java/org/apache/wiki/util/XmlUtilTest.java b/jspwiki-util/src/test/java/org/apache/wiki/util/XmlUtilTest.java
index 68aa485..dc955ff 100644
--- a/jspwiki-util/src/test/java/org/apache/wiki/util/XmlUtilTest.java
+++ b/jspwiki-util/src/test/java/org/apache/wiki/util/XmlUtilTest.java
@@ -97,4 +97,12 @@ public class XmlUtilTest {
         }
     }
 
+    @Test
+    public void testGetXPathElement() {
+        final Element base = XmlUtil.parse( "ini/jspwiki_module.xml", "/modules" ).get( 0 );
+        Assertions.assertNull( XmlUtil.getXPathElement( base, "folter" ) );
+        final Element element = XmlUtil.getXPathElement( base, "filter" );
+        Assertions.assertEquals( "org.apache.wiki.filters.SpamFilter", element.getAttributeValue( "class" ) );
+    }
+
 }