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     */
019    package org.apache.wiki.htmltowiki;
020    
021    import java.io.ByteArrayInputStream;
022    import java.io.IOException;
023    import java.io.PrintWriter;
024    import java.io.UnsupportedEncodingException;
025    import java.net.URLDecoder;
026    import java.util.HashMap;
027    import java.util.Iterator;
028    import java.util.LinkedHashMap;
029    import java.util.Map;
030    
031    import org.apache.commons.lang.StringEscapeUtils;
032    import org.jdom2.Element;
033    import org.jdom2.Attribute;
034    import org.jdom2.JDOMException;
035    import org.jdom2.Text;
036    import org.jdom2.xpath.XPath;
037    
038    /**
039     * Converting XHtml to Wiki Markup.  This is the class which does all of the heavy loading.
040     *
041     */
042    public class XHtmlElementToWikiTranslator
043    {
044        private static final String UTF8 = "UTF-8";
045    
046        private XHtmlToWikiConfig m_config;
047    
048        private WhitespaceTrimWriter m_outTimmer;
049    
050        private PrintWriter m_out;
051    
052        private LiStack m_liStack = new LiStack();
053    
054        private PreStack m_preStack = new PreStack();
055    
056        /**
057         *  Create a new translator using the default config.
058         *  
059         *  @param base The base element from which to start translating.
060         *  @throws IOException If reading of the DOM tree fails.
061         *  @throws JDOMException If the DOM tree is faulty.
062         */
063        public XHtmlElementToWikiTranslator( Element base ) throws IOException, JDOMException
064        {
065            this( base, new XHtmlToWikiConfig() );
066        }
067    
068        /**
069         *  Create a new translator using the specified config.
070         *  
071         *  @param base The base element from which to start translating.
072         *  @param config The config to use.
073         *  @throws IOException If reading of the DOM tree fails.
074         *  @throws JDOMException If the DOM tree is faulty.
075         */
076        public XHtmlElementToWikiTranslator( Element base, XHtmlToWikiConfig config ) throws IOException, JDOMException
077        {
078            this.m_config = config;
079            m_outTimmer = new WhitespaceTrimWriter();
080            m_out = new PrintWriter( m_outTimmer );
081            print( base );
082        }
083    
084        /**
085         *  FIXME: I have no idea what this does...
086         * 
087         *  @return Something.
088         */
089        public String getWikiString()
090        {
091            return m_outTimmer.toString();
092        }
093    
094        private void print( String s )
095        {
096            s = StringEscapeUtils.unescapeHtml( s );
097            m_out.print( s );
098        }
099    
100        private void print( Object element ) throws IOException, JDOMException
101        {
102            if( element instanceof Text )
103            {
104                Text t = (Text)element;
105                String s = t.getText();
106                if( m_preStack.isPreMode() )
107                {
108                    m_out.print( s );
109                }
110                else
111                {
112                    // remove all "line terminator" characters
113                    s = s.replaceAll( "[\\r\\n\\f\\u0085\\u2028\\u2029]", "" );
114                    m_out.print( s );
115                }
116            }
117            else if( element instanceof Element )
118            {
119                Element base = (Element)element;
120                String n = base.getName().toLowerCase();
121                if( "imageplugin".equals( base.getAttributeValue( "class" ) ) )
122                {
123                    printImage( base );
124                }
125                else if( "wikiform".equals( base.getAttributeValue( "class" ) ) )
126                {
127                    // only print the children if the div's class="wikiform", but not the div itself.
128                    printChildren( base );
129                }
130                else
131                {
132                    boolean bold = false;
133                    boolean italic = false;
134                    boolean monospace = false;
135                    String cssSpecial = null;
136                    String cssClass = base.getAttributeValue( "class" );
137    
138                    // accomodate a FCKeditor bug with Firefox: when a link is removed, it becomes <span class="wikipage">text</span>.
139                    boolean ignoredCssClass = cssClass != null && cssClass.matches( "wikipage|createpage|external|interwiki|attachment" );
140    
141                    Map styleProps = null;
142    
143                    // Only get the styles if it's not a link element. Styles for link elements are
144                    // handled as an AugmentedWikiLink instead.
145                    if( !n.equals( "a" ) )
146                    {
147                        styleProps = getStylePropertiesLowerCase( base );
148                    }
149    
150                    if( styleProps != null )
151                    {
152                        String fontFamily = (String)styleProps.get( "font-family" );
153                        String whiteSpace = (String)styleProps.get( "white-space" );
154                        if( fontFamily != null && ( fontFamily.indexOf( "monospace" ) >= 0
155                                && whiteSpace != null && whiteSpace.indexOf( "pre" ) >= 0  ) )
156                        {
157                            styleProps.remove( "font-family" );
158                            styleProps.remove( "white-space" );
159                            monospace = true;
160                        }
161    
162                        String weight = (String)styleProps.remove( "font-weight" );
163                        String style = (String)styleProps.remove( "font-style" );
164    
165                        if( n.equals( "p" ) )
166                        {
167                            // change it so we can print out the css styles for <p>
168                            n = "div";
169                        }
170    
171                        italic = "oblique".equals( style ) || "italic".equals( style );
172                        bold = "bold".equals( weight ) || "bolder".equals( weight );
173                        if( !styleProps.isEmpty() )
174                        {
175                            cssSpecial = propsToStyleString( styleProps );
176                        }
177                    }
178                    if( cssClass != null && !ignoredCssClass )
179                    {
180                        if( n.equals( "div" ) )
181                        {
182                            m_out.print( "\n%%" + cssClass + " \n" );
183                        }
184                        else if( n.equals( "span" ) )
185                        {
186                            m_out.print( "%%" + cssClass + " " );
187                        }
188                    }
189                    if( bold )
190                    {
191                        m_out.print( "__" );
192                    }
193                    if( italic )
194                    {
195                        m_out.print( "''" );
196                    }
197                    if( monospace )
198                    {
199                        m_out.print( "{{{" );
200                        m_preStack.push();
201                    }
202                    if( cssSpecial != null )
203                    {
204                        if( n.equals( "div" ) )
205                        {
206                            m_out.print( "\n%%(" + cssSpecial + " )\n" );
207                        }
208                        else
209                        {
210                            m_out.print( "%%(" + cssSpecial + " )" );
211                        }
212                    }
213                    printChildren( base );
214                    if( cssSpecial != null )
215                    {
216                        if( n.equals( "div" ) )
217                        {
218                            m_out.print( "\n%%\n" );
219                        }
220                        else
221                        {
222                            m_out.print( "%%" );
223                        }
224                    }
225                    if( monospace )
226                    {
227                        m_preStack.pop();
228                        m_out.print( "}}}" );
229                    }
230                    if( italic )
231                    {
232                        m_out.print( "''" );
233                    }
234                    if( bold )
235                    {
236                        m_out.print( "__" );
237                    }
238                    if( cssClass != null && !ignoredCssClass )
239                    {
240                        if( n.equals( "div" ) )
241                        {
242                            m_out.print( "\n%%\n" );
243                        }
244                        else if( n.equals( "span" ) )
245                        {
246                            m_out.print( "%%" );
247                        }
248                    }
249                }
250            }
251        }
252    
253        private void printChildren( Element base ) throws IOException, JDOMException
254        {
255            for( Iterator i = base.getContent().iterator(); i.hasNext(); )
256            {
257                Object c = i.next();
258                if( c instanceof Element )
259                {
260                    Element e = (Element)c;
261                    String n = e.getName().toLowerCase();
262                    if( n.equals( "h1" ) )
263                    {
264                        m_out.print( "\n!!! " );
265                        print( e );
266                        m_out.println();
267                    }
268                    else if( n.equals( "h2" ) )
269                    {
270                        m_out.print( "\n!!! " );
271                        print( e );
272                        m_out.println();
273                    }
274                    else if( n.equals( "h3" ) )
275                    {
276                        m_out.print( "\n!! " );
277                        print( e );
278                        m_out.println();
279                    }
280                    else if( n.equals( "h4" ) )
281                    {
282                        m_out.print( "\n! " );
283                        print( e );
284                        m_out.println();
285                    }
286                    else if( n.equals( "p" ) )
287                    {
288                        if( e.getContentSize() != 0 ) // we don't want to print empty elements: <p></p>
289                        {
290                            m_out.println();
291                            print( e );
292                            m_out.println();
293                        }
294                    }
295                    else if( n.equals( "br" ) )
296                    {
297                        if( m_preStack.isPreMode() )
298                        {
299                            m_out.println();
300                        }
301                        else
302                        {
303                            String parentElementName = base.getName().toLowerCase();
304    
305                            //
306                            // To beautify the generated wiki markup, we print a newline character after a linebreak.
307                            // It's only safe to do this when the parent element is a <p> or <div>; when the parent
308                            // element is a table cell or list item, a newline character would break the markup.
309                            // We also check that this isn't being done inside a plugin body.
310                            //
311                            if( parentElementName.matches( "p|div" ) 
312                                && !base.getText().matches( "(?s).*\\[\\{.*\\}\\].*" ) )
313                            {
314                                m_out.print( " \\\\\n" );
315                            }
316                            else
317                            {
318                                m_out.print( " \\\\" );
319                            }
320                        }
321                        print( e );
322                    }
323                    else if( n.equals( "hr" ) )
324                    {
325                        m_out.println();
326                        print( "----" );
327                        print( e );
328                        m_out.println();
329                    }
330                    else if( n.equals( "table" ) )
331                    {
332                        if( !m_outTimmer.isCurrentlyOnLineBegin() )
333                        {
334                            m_out.println();
335                        }
336                        print( e );
337                    }
338                    else if( n.equals( "tr" ) )
339                    {
340                        print( e );
341                        m_out.println();
342                    }
343                    else if( n.equals( "td" ) )
344                    {
345                        m_out.print( "| " );
346                        print( e );
347                        if( !m_preStack.isPreMode() )
348                        {
349                            print( " " );
350                        }
351                    }
352                    else if( n.equals( "th" ) )
353                    {
354                        m_out.print( "|| " );
355                        print( e );
356                        if( !m_preStack.isPreMode() )
357                        {
358                            print( " " );
359                        }
360                    }
361                    else if( n.equals( "a" ) )
362                    {
363                        if( !isIgnorableWikiMarkupLink( e ) )
364                        {
365                            if( e.getChild( "IMG" ) != null )
366                            {
367                                printImage( e );
368                            }
369                            else
370                            {
371                                String ref = e.getAttributeValue( "href" );
372                                if( ref == null )
373                                {
374                                    if( isUndefinedPageLink( e ) )
375                                    {
376                                        m_out.print( "[" );
377                                        print( e );
378                                        m_out.print( "]" );
379                                    }
380                                    else
381                                    {
382                                        print( e );
383                                    }
384                                }
385                                else
386                                {
387                                    ref = trimLink( ref );
388                                    if( ref != null )
389                                    {
390                                        if( ref.startsWith( "#" ) ) // This is a link to a footnote.
391                                        {
392                                            // convert "#ref-PageName-1" to just "1"
393                                            String href = ref.replaceFirst( "#ref-.+-(\\d+)", "$1" );
394                                            
395                                            // remove the brackets around "[1]"
396                                            String textValue = e.getValue().substring( 1, (e.getValue().length() - 1) );
397                                            
398                                            if( href.equals( textValue ) ){ // handles the simplest case. Example: [1]
399                                                print( e );
400                                            }                                        
401                                            else{ // handles the case where the link text is different from the href. Example: [something|1]
402                                                m_out.print( "[" + textValue + "|" + href + "]" );
403                                            }
404                                        }
405                                        else
406                                        {
407                                            Map augmentedWikiLinkAttributes = getAugmentedWikiLinkAttributes( e );
408    
409                                            m_out.print( "[" );
410                                            print( e );
411                                            if( !e.getTextTrim().equalsIgnoreCase( ref ) )
412                                            {
413                                                m_out.print( "|" );
414                                                print( ref );
415    
416                                                if( !augmentedWikiLinkAttributes.isEmpty() )
417                                                {
418                                                    m_out.print( "|" );
419    
420                                                    String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
421                                                    m_out.print( augmentedWikiLink );
422                                                }
423                                            }
424                                            else if( !augmentedWikiLinkAttributes.isEmpty() )
425                                            {
426                                                // If the ref has the same value as the text and also if there
427                                                // are attributes, then just print: [ref|ref|attributes] .
428                                                m_out.print( "|" + ref + "|" );
429                                                String augmentedWikiLink = augmentedWikiLinkMapToString( augmentedWikiLinkAttributes );
430                                                m_out.print( augmentedWikiLink );
431                                            }
432    
433                                            m_out.print( "]" );
434                                        }
435                                    }
436                                }
437                            }
438                        }
439                    }
440                    else if( n.equals( "b" ) || n.equals("strong") )
441                    {
442                        m_out.print( "__" );
443                        print( e );
444                        m_out.print( "__" );
445                    }
446                    else if( n.equals( "i" ) || n.equals("em") || n.equals( "address" ) )
447                    {
448                        m_out.print( "''" );
449                        print( e );
450                        m_out.print( "''" );
451                    }
452                    else if( n.equals( "u" ) )
453                    {
454                        m_out.print( "%%( text-decoration:underline; )" );
455                        print( e );
456                        m_out.print( "%%" );
457                    }
458                    else if( n.equals( "strike" ) )
459                    {
460                        m_out.print( "%%strike " );
461                        print( e );
462                        m_out.print( "%%" );
463                        // NOTE: don't print a space before or after the double percents because that can break words into two.
464                        // For example: %%(color:red)ABC%%%%(color:green)DEF%% is different from %%(color:red)ABC%% %%(color:green)DEF%%
465                    }
466                    else if( n.equals( "sup" ) )
467                    {
468                        m_out.print( "%%sup " );
469                        print( e );
470                        m_out.print( "%%" );
471                    }
472                    else if( n.equals( "sub" ) )
473                    {
474                        m_out.print( "%%sub " );
475                        print( e );
476                        m_out.print( "%%" );
477                    }
478                    else if( n.equals("dl") )
479                    {
480                        m_out.print( "\n" );
481                        print( e );
482    
483                        // print a newline after the definition list. If we don't,
484                        // it may cause problems for the subsequent element.
485                        m_out.print( "\n" );
486                    }
487                    else if( n.equals("dt") )
488                    {
489                        m_out.print( ";" );
490                        print( e );
491                    }
492                    else if( n.equals("dd") )
493                    {
494                        m_out.print( ":" );
495                        print( e );
496                    }
497                    else if( n.equals( "ul" ) )
498                    {
499                        m_out.println();
500                        m_liStack.push( "*" );
501                        print( e );
502                        m_liStack.pop();
503                    }
504                    else if( n.equals( "ol" ) )
505                    {
506                        m_out.println();
507                        m_liStack.push( "#" );
508                        print( e );
509                        m_liStack.pop();
510                    }
511                    else if( n.equals( "li" ) )
512                    {
513                        m_out.print( m_liStack + " " );
514                        print( e );
515    
516                        // The following line assumes that the XHTML has been "pretty-printed"
517                        // (newlines separate child elements from their parents).
518                        boolean lastListItem = base.indexOf( e ) == ( base.getContentSize() - 2 );
519                        boolean sublistItem = m_liStack.toString().length() > 1;
520    
521                        // only print a newline if this <li> element is not the last item within a sublist.
522                        if( !sublistItem || !lastListItem )
523                        {
524                            m_out.println();
525                        }
526                    }
527                    else if( n.equals( "pre" ) )
528                    {
529                        m_out.print( "\n{{{" ); // start JSPWiki "code blocks" on its own line
530                        m_preStack.push();
531                        print( e );
532                        m_preStack.pop();
533    
534                        // print a newline after the closing braces
535                        // to avoid breaking any subsequent wiki markup that follows.
536                        m_out.print( "}}}\n" );
537                    }
538                    else if( n.equals( "code" ) || n.equals( "tt" ) )
539                    {
540                        m_out.print( "{{" );
541                        m_preStack.push();
542                        print( e );
543                        m_preStack.pop();
544                        m_out.print( "}}" );
545                        // NOTE: don't print a newline after the closing brackets because if the Text is inside
546                        // a table or list, it would break it if there was a subsequent row or list item.
547                    }
548                    else if( n.equals( "img" ) )
549                    {
550                        if( !isIgnorableWikiMarkupLink( e ) )
551                        {
552                            m_out.print( "[" );
553                            print( trimLink( e.getAttributeValue( "src" ) ) );
554                            m_out.print( "]" );
555                        }
556                    }
557                    else if( n.equals( "form" ) )
558                    {
559                        // remove the hidden input where name="formname" since a new one will be generated again when the xhtml is rendered.
560                        Element formName = (Element)XPath.selectSingleNode( e, "INPUT[@name='formname']" );
561                        if( formName != null )
562                        {
563                            formName.detach();
564                        }
565    
566                        String name = e.getAttributeValue( "name" );
567    
568                        m_out.print( "\n[{FormOpen" );
569    
570                        if( name != null )
571                        {
572                            m_out.print( " form='" + name + "'" );
573                        }
574    
575                        m_out.print( "}]\n" );
576    
577                        print( e );
578                        m_out.print( "\n[{FormClose}]\n" );
579                    }
580                    else if( n.equals( "input" ) )
581                    {
582                        String type = e.getAttributeValue( "type" );
583                        String name = e.getAttributeValue( "name" );
584                        String value = e.getAttributeValue( "value" );
585                        String checked = e.getAttributeValue( "checked" );
586    
587                        m_out.print( "[{FormInput" );
588    
589                        if( type != null )
590                        {
591                            m_out.print( " type='" + type + "'" );
592                        }
593                        if( name != null )
594                        {
595                            // remove the "nbf_" that was prepended since new one will be generated again when the xhtml is rendered.
596                            if( name.startsWith( "nbf_" ) )
597                            {
598                                name = name.substring( 4, name.length( ));
599                            }
600                            m_out.print( " name='" + name + "'" );
601                        }
602                        if( value != null && !value.equals( "" ) )
603                        {
604                            m_out.print( " value='" + value + "'" );
605                        }
606                        if( checked != null )
607                        {
608                            m_out.print( " checked='" + checked + "'" );
609                        }
610    
611                        m_out.print( "}]" );
612    
613                        print( e );
614                    }
615                    else if( n.equals( "textarea" ) )
616                    {
617                        String name = e.getAttributeValue( "name" );
618                        String rows = e.getAttributeValue( "rows" );
619                        String cols = e.getAttributeValue( "cols" );
620    
621                        m_out.print( "[{FormTextarea" );
622    
623                        if( name != null )
624                        {
625                            if( name.startsWith( "nbf_" ) )
626                            {
627                                name = name.substring( 4, name.length( ));
628                            }
629                            m_out.print( " name='" + name + "'" );
630                        }
631                        if( rows != null )
632                        {
633                            m_out.print( " rows='" + rows + "'" );
634                        }
635                        if( cols != null )
636                        {
637                            m_out.print( " cols='" + cols + "'" );
638                        }
639    
640                        m_out.print( "}]" );
641                        print( e );
642                    }
643                    else if( n.equals( "select" ) )
644                    {
645                        String name = e.getAttributeValue( "name" );
646    
647                        m_out.print( "[{FormSelect" );
648    
649                        if( name != null )
650                        {
651                            if( name.startsWith( "nbf_" ) )
652                            {
653                                name = name.substring( 4, name.length( ));
654                            }
655                            m_out.print( " name='" + name + "'" );
656                        }
657    
658                        m_out.print( " value='" );
659                        print( e );
660                        m_out.print( "'}]" );
661                    }
662                    else if( n.equals( "option" ) )
663                    {
664                        // If this <option> element isn't the second child element within the parent <select>
665                        // element, then we need to print a semicolon as a separator. (The first child element
666                        // is expected to be a newline character which is at index of 0).
667                        if( base.indexOf( e ) != 1 )
668                        {
669                            m_out.print( ";" );
670                        }
671    
672                        Attribute selected = e.getAttribute( "selected" );
673                        if( selected !=  null )
674                        {
675                            m_out.print( "*" );
676                        }
677    
678                        String value = e.getAttributeValue( "value" );
679                        if( value != null )
680                        {
681                            m_out.print( value );
682                        }
683                        else
684                        {
685                            print( e );
686                        }
687                    }
688                    else
689                    {
690                        print( e );
691                    }
692                }
693                else
694                {
695                    print( c );
696                }
697            }
698        }
699    
700        @SuppressWarnings("unchecked")
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            StringBuffer style = new StringBuffer();
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 classVal != null && classVal.equals( "createpage" );
789        }
790    
791        /**
792         *  Returns a Map containing the valid augmented wiki link attributes.
793         */
794        private Map getAugmentedWikiLinkAttributes( Element a )
795        {
796            Map<String,String> attributesMap = new HashMap<String,String>();
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            StringBuffer sb = new StringBuffer();
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            public String toString()
1049            {
1050                return m_li.toString();
1051            }
1052    
1053        }
1054    
1055        private class PreStack
1056        {
1057    
1058            private int m_pre = 0;
1059    
1060            public boolean isPreMode()
1061            {
1062                return m_pre > 0;
1063            }
1064    
1065            public void push()
1066            {
1067                m_pre++;
1068                m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
1069            }
1070    
1071            public void pop()
1072            {
1073                m_pre--;
1074                m_outTimmer.setWhitespaceTrimMode( !isPreMode() );
1075            }
1076    
1077        }
1078    
1079    }