001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.htmltowiki;
020
021import org.apache.commons.text.StringEscapeUtils;
022import org.jdom2.Attribute;
023import org.jdom2.Content;
024import org.jdom2.Element;
025import org.jdom2.JDOMException;
026import org.jdom2.Text;
027import org.jdom2.xpath.XPathFactory;
028
029import java.io.ByteArrayInputStream;
030import java.io.IOException;
031import java.io.PrintWriter;
032import java.io.UnsupportedEncodingException;
033import java.net.URLDecoder;
034import java.util.HashMap;
035import java.util.Iterator;
036import java.util.LinkedHashMap;
037import java.util.List;
038import java.util.Map;
039
040
041/**
042 * Converting XHtml to Wiki Markup.  This is the class which does all of the heavy loading.
043 *
044 */
045public class XHtmlElementToWikiTranslator
046{
047    private static final String UTF8 = "UTF-8";
048
049    private XHtmlToWikiConfig m_config;
050
051    private WhitespaceTrimWriter m_outTimmer;
052
053    private PrintWriter m_out;
054
055    private LiStack m_liStack = new LiStack();
056
057    private PreStack m_preStack = new PreStack();
058
059    /**
060     *  Create a new translator using the default config.
061     *
062     *  @param base The base element from which to start translating.
063     *  @throws IOException If reading of the DOM tree fails.
064     *  @throws JDOMException If the DOM tree is faulty.
065     */
066    public XHtmlElementToWikiTranslator( Element base ) throws IOException, JDOMException
067    {
068        this( base, new XHtmlToWikiConfig() );
069    }
070
071    /**
072     *  Create a new translator using the specified config.
073     *
074     *  @param base The base element from which to start translating.
075     *  @param config The config to use.
076     *  @throws IOException If reading of the DOM tree fails.
077     *  @throws JDOMException If the DOM tree is faulty.
078     */
079    public XHtmlElementToWikiTranslator( Element base, XHtmlToWikiConfig config ) throws IOException, JDOMException
080    {
081        this.m_config = config;
082        m_outTimmer = new WhitespaceTrimWriter();
083        m_out = new PrintWriter( m_outTimmer );
084        print( base );
085    }
086
087    /**
088     *  FIXME: I have no idea what this does...
089     *
090     *  @return Something.
091     */
092    public String getWikiString()
093    {
094        return m_outTimmer.toString();
095    }
096
097    private void print( String s )
098    {
099        s = StringEscapeUtils.unescapeHtml4( s );
100        m_out.print( s );
101    }
102
103    private void print( Object element ) throws IOException, JDOMException
104    {
105        if( element instanceof Text )
106        {
107            Text t = (Text)element;
108            String s = t.getText();
109            if( m_preStack.isPreMode() )
110            {
111                m_out.print( s );
112            }
113            else
114            {
115                // remove all "line terminator" characters
116                s = s.replaceAll( "[\\r\\n\\f\\u0085\\u2028\\u2029]", "" );
117                m_out.print( s );
118            }
119        }
120        else if( element instanceof Element )
121        {
122            Element base = (Element)element;
123            String n = base.getName().toLowerCase();
124            if( "imageplugin".equals( base.getAttributeValue( "class" ) ) )
125            {
126                printImage( base );
127            }
128            else if( "wikiform".equals( base.getAttributeValue( "class" ) ) )
129            {
130                // only print the children if the div's class="wikiform", but not the div itself.
131                printChildren( base );
132            }
133            else
134            {
135                boolean bold = false;
136                boolean italic = false;
137                boolean monospace = false;
138                String cssSpecial = null;
139                String cssClass = base.getAttributeValue( "class" );
140
141                // accomodate a FCKeditor bug with Firefox: when a link is removed, it becomes <span class="wikipage">text</span>.
142                boolean ignoredCssClass = cssClass != null && cssClass.matches( "wikipage|createpage|external|interwiki|attachment|inline-code" );
143
144                Map< Object, Object > styleProps = null;
145
146                // Only get the styles if it's not a link element. Styles for link elements are
147                // handled as an AugmentedWikiLink instead.
148                if( !n.equals( "a" ) )
149                {
150                    styleProps = getStylePropertiesLowerCase( base );
151                }
152
153                if( cssClass != null && "inline-code".equals(cssClass) )
154                {
155                    monospace = true;
156                }
157
158                if( styleProps != null )
159                {
160                    String weight = (String)styleProps.remove( "font-weight" );
161                    String style = (String)styleProps.remove( "font-style" );
162
163                    if( n.equals( "p" ) )
164                    {
165                        // change it so we can print out the css styles for <p>
166                        n = "div";
167                    }
168
169                    italic = "oblique".equals( style ) || "italic".equals( style );
170                    bold = "bold".equals( weight ) || "bolder".equals( weight );
171                    if( !styleProps.isEmpty() )
172                    {
173                        cssSpecial = propsToStyleString( styleProps );
174                    }
175                }
176                if( cssClass != null && !ignoredCssClass )
177                {
178                    if( n.equals( "div" ) )
179                    {
180                        m_out.print( "\n%%" + cssClass + " \n" );
181                    }
182                    else if( n.equals( "span" ) )
183                    {
184                        m_out.print( "%%" + cssClass + " " );
185                    }
186                }
187                if( bold )
188                {
189                    m_out.print( "__" );
190                }
191                if( italic )
192                {
193                    m_out.print( "''" );
194                }
195                if( monospace )
196                {
197                    m_out.print( "{{{" );
198                    m_preStack.push();
199                }
200                if( cssSpecial != null )
201                {
202                    if( n.equals( "div" ) )
203                    {
204                        m_out.print( "\n%%(" + cssSpecial + " )\n" );
205                    }
206                    else
207                    {
208                        m_out.print( "%%(" + cssSpecial + " )" );
209                    }
210                }
211                printChildren( base );
212                if( cssSpecial != null )
213                {
214                    if( n.equals( "div" ) )
215                    {
216                        m_out.print( "\n/%\n" );
217                    }
218                    else
219                    {
220                        m_out.print( "/%" );
221                    }
222                }
223                if( monospace )
224                {
225                    m_preStack.pop();
226                    m_out.print( "}}}" );
227                }
228                if( italic )
229                {
230                    m_out.print( "''" );
231                }
232                if( bold )
233                {
234                    m_out.print( "__" );
235                }
236                if( cssClass != null && !ignoredCssClass )
237                {
238                    if( n.equals( "div" ) )
239                    {
240                        m_out.print( "\n/%\n" );
241                    }
242                    else if( n.equals( "span" ) )
243                    {
244                        m_out.print( "/%" );
245                    }
246                }
247            }
248        }
249    }
250
251    private void printChildren( Element base ) throws IOException, JDOMException
252    {
253        for( Iterator< Content > i = base.getContent().iterator(); i.hasNext(); )
254        {
255            Content c = i.next();
256            if( c instanceof Element )
257            {
258                Element e = (Element)c;
259                String n = e.getName().toLowerCase();
260                if( n.equals( "h1" ) )
261                {
262                    m_out.print( "\n!!! " );
263                    print( e );
264                    m_out.println();
265                }
266                else if( n.equals( "h2" ) )
267                {
268                    m_out.print( "\n!!! " );
269                    print( e );
270                    m_out.println();
271                }
272                else if( n.equals( "h3" ) )
273                {
274                    m_out.print( "\n!! " );
275                    print( e );
276                    m_out.println();
277                }
278                else if( n.equals( "h4" ) )
279                {
280                    m_out.print( "\n! " );
281                    print( e );
282                    m_out.println();
283                }
284                else if( n.equals( "p" ) )
285                {
286                    if( e.getContentSize() != 0 ) // we don't want to print empty elements: <p></p>
287                    {
288                        m_out.println();
289                        print( e );
290                        m_out.println();
291                    }
292                }
293                else if( n.equals( "br" ) )
294                {
295                    if( m_preStack.isPreMode() )
296                    {
297                        m_out.println();
298                    }
299                    else
300                    {
301                        String parentElementName = base.getName().toLowerCase();
302
303                        //
304                        // To beautify the generated wiki markup, we print a newline character after a linebreak.
305                        // It's only safe to do this when the parent element is a <p> or <div>; when the parent
306                        // element is a table cell or list item, a newline character would break the markup.
307                        // We also check that this isn't being done inside a plugin body.
308                        //
309                        if( parentElementName.matches( "p|div" )
310                            && !base.getText().matches( "(?s).*\\[\\{.*\\}\\].*" ) )
311                        {
312                            m_out.print( " \\\\\n" );
313                        }
314                        else
315                        {
316                            m_out.print( " \\\\" );
317                        }
318                    }
319                    print( e );
320                }
321                else if( n.equals( "hr" ) )
322                {
323                    m_out.println();
324                    print( "----" );
325                    print( e );
326                    m_out.println();
327                }
328                else if( n.equals( "table" ) )
329                {
330                    if( !m_outTimmer.isCurrentlyOnLineBegin() )
331                    {
332                        m_out.println();
333                    }
334                    print( e );
335                }
336                else if( n.equals( "tr" ) )
337                {
338                    print( e );
339                    m_out.println();
340                }
341                else if( n.equals( "td" ) )
342                {
343                    m_out.print( "| " );
344                    print( e );
345                    if( !m_preStack.isPreMode() )
346                    {
347                        print( " " );
348                    }
349                }
350                else if( n.equals( "th" ) )
351                {
352                    m_out.print( "|| " );
353                    print( e );
354                    if( !m_preStack.isPreMode() )
355                    {
356                        print( " " );
357                    }
358                }
359                else if( n.equals( "a" ) )
360                {
361                    if( !isIgnorableWikiMarkupLink( e ) )
362                    {
363                        if( e.getChild( "IMG" ) != null )
364                        {
365                            printImage( e );
366                        }
367                        else
368                        {
369                            String ref = e.getAttributeValue( "href" );
370                            if( ref == null )
371                            {
372                                if( isUndefinedPageLink( e ) )
373                                {
374                                    m_out.print( "[" );
375                                    print( e );
376                                    m_out.print( "]" );
377                                }
378                                else
379                                {
380                                    print( e );
381                                }
382                            }
383                            else
384                            {
385                                ref = trimLink( ref );
386                                if( ref != null )
387                                {
388                                    if( ref.startsWith( "#" ) ) // This is a link to a footnote.
389                                    {
390                                        // convert "#ref-PageName-1" to just "1"
391                                        String href = ref.replaceFirst( "#ref-.+-(\\d+)", "$1" );
392
393                                        // remove the brackets around "[1]"
394                                        String textValue = e.getValue().substring( 1, (e.getValue().length() - 1) );
395
396                                        if( href.equals( textValue ) ){ // handles the simplest case. Example: [1]
397                                            print( e );
398                                        }
399                                        else{ // handles the case where the link text is different from the href. Example: [something|1]
400                                            m_out.print( "[" + textValue + "|" + href + "]" );
401                                        }
402                                    }
403                                    else
404                                    {
405                                        Map<String, String> augmentedWikiLinkAttributes = getAugmentedWikiLinkAttributes( e );
406
407                                        m_out.print( "[" );
408                                        print( e );
409                                        if( !e.getTextTrim().equalsIgnoreCase( ref ) )
410                                        {
411                                            m_out.print( "|" );
412                                            print( ref );
413
414                                            if( !augmentedWikiLinkAttributes.isEmpty() )
415                                            {
416                                                m_out.print( "|" );
417
418                                                String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
419                                                m_out.print( augmentedWikiLink );
420                                            }
421                                        }
422                                        else if( !augmentedWikiLinkAttributes.isEmpty() )
423                                        {
424                                            // If the ref has the same value as the text and also if there
425                                            // are attributes, then just print: [ref|ref|attributes] .
426                                            m_out.print( "|" + ref + "|" );
427                                            String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
428                                            m_out.print( augmentedWikiLink );
429                                        }
430
431                                        m_out.print( "]" );
432                                    }
433                                }
434                            }
435                        }
436                    }
437                }
438                else if( n.equals( "b" ) || n.equals("strong") )
439                {
440                    m_out.print( "__" );
441                    print( e );
442                    m_out.print( "__" );
443                }
444                else if( n.equals( "i" ) || n.equals("em") || n.equals( "address" ) )
445                {
446                    m_out.print( "''" );
447                    print( e );
448                    m_out.print( "''" );
449                }
450                else if( n.equals( "u" ) )
451                {
452                    m_out.print( "%%( text-decoration:underline; )" );
453                    print( e );
454                    m_out.print( "/%" );
455                }
456                else if( n.equals( "strike" ) )
457                {
458                    m_out.print( "%%strike " );
459                    print( e );
460                    m_out.print( "/%" );
461                    // NOTE: don't print a space before or after the double percents because that can break words into two.
462                    // For example: %%(color:red)ABC%%%%(color:green)DEF%% is different from %%(color:red)ABC%% %%(color:green)DEF%%
463                }
464                else if( n.equals( "sup" ) )
465                {
466                    m_out.print( "%%sup " );
467                    print( e );
468                    m_out.print( "/%" );
469                }
470                else if( n.equals( "sub" ) )
471                {
472                    m_out.print( "%%sub " );
473                    print( e );
474                    m_out.print( "/%" );
475                }
476                else if( n.equals("dl") )
477                {
478                    m_out.print( "\n" );
479                    print( e );
480
481                    // print a newline after the definition list. If we don't,
482                    // it may cause problems for the subsequent element.
483                    m_out.print( "\n" );
484                }
485                else if( n.equals("dt") )
486                {
487                    m_out.print( ";" );
488                    print( e );
489                }
490                else if( n.equals("dd") )
491                {
492                    m_out.print( ":" );
493                    print( e );
494                }
495                else if( n.equals( "ul" ) )
496                {
497                    m_out.println();
498                    m_liStack.push( "*" );
499                    print( e );
500                    m_liStack.pop();
501                }
502                else if( n.equals( "ol" ) )
503                {
504                    m_out.println();
505                    m_liStack.push( "#" );
506                    print( e );
507                    m_liStack.pop();
508                }
509                else if( n.equals( "li" ) )
510                {
511                    m_out.print( m_liStack + " " );
512                    print( e );
513
514                    // The following line assumes that the XHTML has been "pretty-printed"
515                    // (newlines separate child elements from their parents).
516                    boolean lastListItem = base.indexOf( e ) == ( base.getContentSize() - 2 );
517                    boolean sublistItem = m_liStack.toString().length() > 1;
518
519                    // only print a newline if this <li> element is not the last item within a sublist.
520                    if( !sublistItem || !lastListItem )
521                    {
522                        m_out.println();
523                    }
524                }
525                else if( n.equals( "pre" ) )
526                {
527                    m_out.print( "\n{{{" ); // start JSPWiki "code blocks" on its own line
528                    m_preStack.push();
529                    print( e );
530                    m_preStack.pop();
531
532                    // print a newline after the closing braces
533                    // to avoid breaking any subsequent wiki markup that follows.
534                    m_out.print( "}}}\n" );
535                }
536                else if( n.equals( "code" ) || n.equals( "tt" ) )
537                {
538                    m_out.print( "{{" );
539                    m_preStack.push();
540                    print( e );
541                    m_preStack.pop();
542                    m_out.print( "}}" );
543                    // NOTE: don't print a newline after the closing brackets because if the Text is inside
544                    // a table or list, it would break it if there was a subsequent row or list item.
545                }
546                else if( n.equals( "img" ) )
547                {
548                    if( !isIgnorableWikiMarkupLink( e ) )
549                    {
550                        m_out.print( "[" );
551                        print( trimLink( e.getAttributeValue( "src" ) ) );
552                        m_out.print( "]" );
553                    }
554                }
555                else if( n.equals( "form" ) )
556                {
557                    // remove the hidden input where name="formname" since a new one will be generated again when the xhtml is rendered.
558                    Element formName = getXPathElement( e, "INPUT[@name='formname']" );
559                    if( formName != null )
560                    {
561                        formName.detach();
562                    }
563
564                    String name = e.getAttributeValue( "name" );
565
566                    m_out.print( "\n[{FormOpen" );
567
568                    if( name != null )
569                    {
570                        m_out.print( " form='" + name + "'" );
571                    }
572
573                    m_out.print( "}]\n" );
574
575                    print( e );
576                    m_out.print( "\n[{FormClose}]\n" );
577                }
578                else if( n.equals( "input" ) )
579                {
580                    String type = e.getAttributeValue( "type" );
581                    String name = e.getAttributeValue( "name" );
582                    String value = e.getAttributeValue( "value" );
583                    String checked = e.getAttributeValue( "checked" );
584
585                    m_out.print( "[{FormInput" );
586
587                    if( type != null )
588                    {
589                        m_out.print( " type='" + type + "'" );
590                    }
591                    if( name != null )
592                    {
593                        // remove the "nbf_" that was prepended since new one will be generated again when the xhtml is rendered.
594                        if( name.startsWith( "nbf_" ) )
595                        {
596                            name = name.substring( 4, name.length( ));
597                        }
598                        m_out.print( " name='" + name + "'" );
599                    }
600                    if( value != null && !value.equals( "" ) )
601                    {
602                        m_out.print( " value='" + value + "'" );
603                    }
604                    if( checked != null )
605                    {
606                        m_out.print( " checked='" + checked + "'" );
607                    }
608
609                    m_out.print( "}]" );
610
611                    print( e );
612                }
613                else if( n.equals( "textarea" ) )
614                {
615                    String name = e.getAttributeValue( "name" );
616                    String rows = e.getAttributeValue( "rows" );
617                    String cols = e.getAttributeValue( "cols" );
618
619                    m_out.print( "[{FormTextarea" );
620
621                    if( name != null )
622                    {
623                        if( name.startsWith( "nbf_" ) )
624                        {
625                            name = name.substring( 4, name.length( ));
626                        }
627                        m_out.print( " name='" + name + "'" );
628                    }
629                    if( rows != null )
630                    {
631                        m_out.print( " rows='" + rows + "'" );
632                    }
633                    if( cols != null )
634                    {
635                        m_out.print( " cols='" + cols + "'" );
636                    }
637
638                    m_out.print( "}]" );
639                    print( e );
640                }
641                else if( n.equals( "select" ) )
642                {
643                    String name = e.getAttributeValue( "name" );
644
645                    m_out.print( "[{FormSelect" );
646
647                    if( name != null )
648                    {
649                        if( name.startsWith( "nbf_" ) )
650                        {
651                            name = name.substring( 4, name.length( ));
652                        }
653                        m_out.print( " name='" + name + "'" );
654                    }
655
656                    m_out.print( " value='" );
657                    print( e );
658                    m_out.print( "'}]" );
659                }
660                else if( n.equals( "option" ) )
661                {
662                    // If this <option> element isn't the second child element within the parent <select>
663                    // element, then we need to print a semicolon as a separator. (The first child element
664                    // is expected to be a newline character which is at index of 0).
665                    if( base.indexOf( e ) != 1 )
666                    {
667                        m_out.print( ";" );
668                    }
669
670                    Attribute selected = e.getAttribute( "selected" );
671                    if( selected !=  null )
672                    {
673                        m_out.print( "*" );
674                    }
675
676                    String value = e.getAttributeValue( "value" );
677                    if( value != null )
678                    {
679                        m_out.print( value );
680                    }
681                    else
682                    {
683                        print( e );
684                    }
685                }
686                else
687                {
688                    print( e );
689                }
690            }
691            else
692            {
693                print( c );
694            }
695        }
696    }
697
698    private void printImage( Element base )
699    {
700        Element child = getXPathElement( base, "TBODY/TR/TD/*" );
701        if( child == null )
702        {
703            child = base;
704        }
705        Element img;
706        String href;
707        Map<Object,Object> map = new ForgetNullValuesLinkedHashMap<>();
708        if( child.getName().equals( "A" ) )
709        {
710            img = child.getChild( "IMG" );
711            href = child.getAttributeValue( "href" );
712        }
713        else
714        {
715            img = child;
716            href = null;
717        }
718        if( img == null )
719        {
720            return;
721        }
722        String src = trimLink( img.getAttributeValue( "src" ) );
723        if( src == null )
724        {
725            return;
726        }
727        map.put( "align", base.getAttributeValue( "align" ) );
728        map.put( "height", img.getAttributeValue( "height" ) );
729        map.put( "width", img.getAttributeValue( "width" ) );
730        map.put( "alt", img.getAttributeValue( "alt" ) );
731        map.put( "caption", emptyToNull( ( Element )XPathFactory.instance().compile(  "CAPTION" ).evaluateFirst( base ) ) );
732        map.put( "link", href );
733        map.put( "border", img.getAttributeValue( "border" ) );
734        map.put( "style", base.getAttributeValue( "style" ) );
735        if( map.size() > 0 )
736        {
737            m_out.print( "[{Image src='" + src + "'" );
738            for( Iterator i = map.entrySet().iterator(); i.hasNext(); )
739            {
740                Map.Entry entry = (Map.Entry)i.next();
741                if( !entry.getValue().equals( "" ) )
742                {
743                    m_out.print( " " + entry.getKey() + "='" + entry.getValue() + "'" );
744                }
745            }
746            m_out.print( "}]" );
747        }
748        else
749        {
750            m_out.print( "[" + src + "]" );
751        }
752    }
753
754    Element getXPathElement( final Element base, final String expression ) {
755        final List< ? > nodes = XPathFactory.instance().compile( expression ).evaluate( base );
756        if( nodes == null || nodes.size() == 0 ) {
757            return null;
758        } else {
759            return ( Element )nodes.get( 0 );
760        }
761    }
762
763    private String emptyToNull( final Element e ) {
764        if( e == null ) {
765            return null;
766        }
767        final String s = e.getText();
768        return s == null ? null : ( s.replaceAll( "\\s", "" ).length() == 0 ? null : s );
769    }
770
771    private String propsToStyleString( final Map< Object, Object >  styleProps ) {
772        StringBuilder style = new StringBuilder();
773        for( Iterator< Map.Entry< Object, Object > > i = styleProps.entrySet().iterator(); i.hasNext(); ) {
774            Map.Entry< Object, Object > entry = i.next();
775            style.append( " " ).append( entry.getKey() ).append( ": " ).append( entry.getValue() ).append( ";" );
776        }
777        return style.toString();
778    }
779
780    private boolean isIgnorableWikiMarkupLink( final Element a ) {
781        final String ref = a.getAttributeValue( "href" );
782        final String clazz = a.getAttributeValue( "class" );
783        return ( ref != null && ref.startsWith( m_config.getPageInfoJsp() ) )
784            || ( clazz != null && clazz.trim().equalsIgnoreCase( m_config.getOutlink() ) );
785    }
786
787    /**
788     * Checks if the link points to an undefined page.
789     */
790    private boolean isUndefinedPageLink( Element a)
791    {
792        String classVal = a.getAttributeValue( "class" );
793
794        return "createpage".equals( classVal );
795    }
796
797    /**
798     *  Returns a Map containing the valid augmented wiki link attributes.
799     */
800    private Map<String,String> getAugmentedWikiLinkAttributes( Element a )
801    {
802        Map<String,String> attributesMap = new HashMap<>();
803
804        String id = a.getAttributeValue( "id" );
805        if( id != null && !id.equals( "" ) )
806        {
807            attributesMap.put( "id", id.replaceAll( "'", "\"" ) );
808        }
809
810        String cssClass = a.getAttributeValue( "class" );
811        if( cssClass != null && !cssClass.equals( "" )
812            && !cssClass.matches( "wikipage|createpage|external|interwiki|attachment" ) )
813        {
814            attributesMap.put( "class", cssClass.replaceAll( "'", "\"" ) );
815        }
816
817        String style = a.getAttributeValue( "style" );
818        if( style != null && !style.equals( "" ) )
819        {
820            attributesMap.put( "style", style.replaceAll( "'", "\"" ) );
821        }
822
823        String title = a.getAttributeValue( "title" );
824        if( title != null && !title.equals( "" ) )
825        {
826            attributesMap.put( "title", title.replaceAll( "'", "\"" ) );
827        }
828
829        String lang = a.getAttributeValue( "lang" );
830        if( lang != null && !lang.equals( "" ) )
831        {
832            attributesMap.put( "lang", lang.replaceAll( "'", "\"" ) );
833        }
834
835        String dir = a.getAttributeValue( "dir" );
836        if( dir != null && !dir.equals( "" ) )
837        {
838            attributesMap.put( "dir", dir.replaceAll( "'", "\"" ) );
839        }
840
841        String charset = a.getAttributeValue( "charset" );
842        if( charset != null && !charset.equals("") )
843        {
844            attributesMap.put( "charset", charset.replaceAll( "'", "\"" ) );
845        }
846
847        String type = a.getAttributeValue( "type" );
848        if( type != null && !type.equals( "" ) )
849        {
850            attributesMap.put( "type", type.replaceAll( "'", "\"" ) );
851        }
852
853        String hreflang = a.getAttributeValue( "hreflang" );
854        if( hreflang != null && !hreflang.equals( "" ) )
855        {
856            attributesMap.put( "hreflang", hreflang.replaceAll( "'", "\"" ) );
857        }
858
859        String rel = a.getAttributeValue( "rel" );
860        if( rel != null && !rel.equals( "" ) )
861        {
862            attributesMap.put( "rel", rel.replaceAll( "'", "\"" ) );
863        }
864
865        String rev = a.getAttributeValue( "rev" );
866        if( rev != null && !rev.equals( "" ) )
867        {
868            attributesMap.put( "rev", rev.replaceAll( "'", "\"" ) );
869        }
870
871        String accesskey = a.getAttributeValue( "accesskey" );
872        if( accesskey != null && !accesskey.equals( "" ) )
873        {
874            attributesMap.put( "accesskey", accesskey.replaceAll( "'", "\"" ) );
875        }
876
877        String tabindex = a.getAttributeValue( "tabindex" );
878        if( tabindex != null && !tabindex.equals( "" ) )
879        {
880            attributesMap.put( "tabindex", tabindex.replaceAll( "'", "\"" ) );
881        }
882
883        String target = a.getAttributeValue( "target" );
884        if( target != null && !target.equals( "" ) )
885        {
886            attributesMap.put( "target", target.replaceAll( "'", "\"" ) );
887        }
888
889        return attributesMap;
890    }
891
892    /**
893     * Converts the entries in the map to a string for use in a wiki link.
894     */
895    private String augmentedWikiLinkMapToString( Map attributesMap )
896    {
897        StringBuilder sb = new StringBuilder();
898
899        for ( Iterator itr = attributesMap.entrySet().iterator(); itr.hasNext(); )
900        {
901            Map.Entry entry = (Map.Entry)itr.next();
902            String attributeName = (String)entry.getKey();
903            String attributeValue = (String)entry.getValue();
904
905            sb.append( " " + attributeName + "='" + attributeValue + "'" );
906        }
907
908        return sb.toString().trim();
909    }
910
911    private Map< Object, Object > getStylePropertiesLowerCase( Element base ) throws IOException
912    {
913        String n = base.getName().toLowerCase();
914
915        //"font-weight: bold; font-style: italic;"
916        String style = base.getAttributeValue( "style" );
917        if( style == null )
918        {
919            style = "";
920        }
921
922        if( n.equals( "p" ) || n.equals( "div" ) )
923        {
924            String align = base.getAttributeValue( "align" );
925            if( align != null )
926            {
927                // only add the value of the align attribute if the text-align style didn't already exist.
928                if( style.indexOf( "text-align" ) == -1 )
929                {
930                    style = style + ";text-align:" + align + ";";
931                }
932            }
933        }
934
935
936
937        if( n.equals( "font" ) )
938        {
939            String color = base.getAttributeValue( "color" );
940            String face = base.getAttributeValue( "face" );
941            String size = base.getAttributeValue( "size" );
942            if( color != null )
943            {
944                style = style + "color:" + color + ";";
945            }
946            if( face != null )
947            {
948                style = style + "font-family:" + face + ";";
949            }
950            if( size != null )
951            {
952                if( size.equals( "1" ) )
953                {
954                    style = style + "font-size:xx-small;";
955                }
956                else if( size.equals( "2" ) )
957                {
958                    style = style + "font-size:x-small;";
959                }
960                else if( size.equals( "3" ) )
961                {
962                    style = style + "font-size:small;";
963                }
964                else if( size.equals( "4" ) )
965                {
966                    style = style + "font-size:medium;";
967                }
968                else if( size.equals( "5" ) )
969                {
970                    style = style + "font-size:large;";
971                }
972                else if( size.equals( "6" ) )
973                {
974                    style = style + "font-size:x-large;";
975                }
976                else if( size.equals( "7" ) )
977                {
978                    style = style + "font-size:xx-large;";
979                }
980            }
981        }
982
983        if( style.equals( "" ) )
984        {
985            return null;
986        }
987
988        style = style.replace( ';', '\n' ).toLowerCase();
989        LinkedHashMap< Object, Object > m = new LinkedHashMap<>();
990        new PersistentMapDecorator( m ).load( new ByteArrayInputStream( style.getBytes() ) );
991        return m;
992    }
993
994    private String trimLink( String ref )
995    {
996        if( ref == null )
997        {
998            return null;
999        }
1000        try
1001        {
1002            ref = URLDecoder.decode( ref, UTF8 );
1003            ref = ref.trim();
1004            if( ref.startsWith( m_config.getAttachPage() ) )
1005            {
1006                ref = ref.substring( m_config.getAttachPage().length() );
1007            }
1008            if( ref.startsWith( m_config.getWikiJspPage() ) )
1009            {
1010                ref = ref.substring( m_config.getWikiJspPage().length() );
1011
1012                // Handle links with section anchors.
1013                // For example, we need to translate the html string "TargetPage#section-TargetPage-Heading2"
1014                // to this wiki string "TargetPage#Heading2".
1015                ref = ref.replaceFirst( ".+#section-(.+)-(.+)", "$1#$2" );
1016            }
1017            if( ref.startsWith( m_config.getEditJspPage() ) )
1018            {
1019                ref = ref.substring( m_config.getEditJspPage().length() );
1020            }
1021            if( m_config.getPageName() != null )
1022            {
1023                if( ref.startsWith( m_config.getPageName() ) )
1024                {
1025                    ref = ref.substring( m_config.getPageName().length() );
1026                }
1027            }
1028        }
1029        catch ( UnsupportedEncodingException e )
1030        {
1031            // Shouldn't happen...
1032        }
1033        return ref;
1034    }
1035
1036    // FIXME: These should probably be better used with java.util.Stack
1037
1038    private static class LiStack
1039    {
1040
1041        private StringBuffer m_li = new StringBuffer();
1042
1043        public void push( String c )
1044        {
1045            m_li.append( c );
1046        }
1047
1048        public void pop()
1049        {
1050            m_li = m_li.deleteCharAt( m_li.length()-1 );
1051            // m_li = m_li.substring( 0, m_li.length() - 1 );
1052        }
1053
1054        @Override
1055        public String toString()
1056        {
1057            return m_li.toString();
1058        }
1059
1060    }
1061
1062    private class PreStack
1063    {
1064
1065        private int m_pre = 0;
1066
1067        public boolean isPreMode()
1068        {
1069            return m_pre > 0;
1070        }
1071
1072        public void push()
1073        {
1074            m_pre++;
1075            m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
1076        }
1077
1078        public void pop()
1079        {
1080            m_pre--;
1081            m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
1082        }
1083
1084    }
1085
1086}