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.Element;
034import org.jdom2.JDOMException;
035import org.jdom2.Text;
036import 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 */
042public 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        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 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        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        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}