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