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