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