    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.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.wiki.api.core.Engine;
    025import org.apache.wiki.htmltowiki.syntax.MarkupHelper;
    026import org.apache.wiki.util.ClassUtil;
    027import org.apache.wiki.util.XmlUtil;
    028import org.jdom2.Content;
    029import org.jdom2.Element;
    030import org.jdom2.JDOMException;
    031import org.jdom2.Verifier;
    032import org.jdom2.Text;
    033import org.jdom2.xpath.XPathFactory;
    034
    035import java.io.PrintWriter;
    036import java.util.ArrayDeque;
    037import java.util.Arrays;
    038import java.util.Deque;
    039import java.util.LinkedHashMap;
    040import java.util.Map;
    041import java.util.stream.Collectors;
    042
    043
    044/**
    045 * Converting XHtml to Wiki Markup.  This is the class which orchestrates all the heavy loading.
    046 */
    047public class XHtmlElementToWikiTranslator {
    048
    049    private static final Logger LOG = LogManager.getLogger( XHtmlElementToWikiTranslator.class );
    050    private static final String DEFAULT_SYNTAX_DECORATOR = "org.apache.wiki.htmltowiki.syntax.jspwiki.JSPWikiSyntaxDecorator";
    051
    052    private final Engine e;
    053    private final XHtmlToWikiConfig config;
    054    private final WhitespaceTrimWriter outTrimmer = new WhitespaceTrimWriter();
    055    private final SyntaxDecorator syntax;
    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 Engine e, final Element base ) throws JDOMException, ReflectiveOperationException {
    064        this( e, 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 Engine e, final Element base, final XHtmlToWikiConfig config ) throws JDOMException, ReflectiveOperationException {
    075        this.e = e;
    076        this.config = config;
    077        syntax = getSyntaxDecorator();
    078        final PrintWriter out = new PrintWriter( outTrimmer );
    079        final Deque< String > liStack = new ArrayDeque<>();
    080        final Deque< String > preStack = new PreDeque();
    081
    082        syntax.init( out, liStack, preStack, outTrimmer, config, this );
    083        translate( base );
    084    }
    085
    086    SyntaxDecorator getSyntaxDecorator() throws ReflectiveOperationException {
    087        String sdClass = e.getWikiProperties().getProperty( "jspwiki.syntax.decorator", DEFAULT_SYNTAX_DECORATOR );
    088        if( !ClassUtil.assignable( sdClass, SyntaxDecorator.class.getName() ) ) {
    089            LOG.warn( "{} does not subclass {} reverting to default syntax decorator.", sdClass, SyntaxDecorator.class.getName() );
    090            sdClass = DEFAULT_SYNTAX_DECORATOR;
    091        }
    092        LOG.debug( "Using {} as markup parser.", sdClass );
    093        return ClassUtil.buildInstance( sdClass );
    094    }
    095
    096    /**
    097     * Outputs parsed wikitext.
    098     *
    099     * @return parsed wikitext.
    100     */
    101    public String getWikiString() {
    102        return outTrimmer.toString();
    103    }
    104
    105    public void translate( final Content element ) throws JDOMException {
    106        if( element instanceof Text ) {
    107            translateText( ( Text ) element );
    108        } else if( element instanceof Element ) {
    109            final Element base = ( Element )element;
    110            final String n = base.getName().toLowerCase();
    111            final String reason = Verifier.checkElementName(n);
    112
    113            if (reason != null)  return; /* invalid element name */
    114
    115            if( "script".equals( n ) ) return; /* nono, not a good idea*/
    116
    117            if( "imageplugin".equals( base.getAttributeValue( "class" ) ) ) {
    118                translateImage( base );
    119            } else if( "wikiform".equals( base.getAttributeValue( "class" ) ) ) {
    120                // only print the children if the div's class="wikiform", but not the div itself.
    121                translateChildren( base );
    122            } else {
    123                translateParagraph( base );
    124            }
    125        }
    126    }
    127
    128    public void translateText( final Text element ) {
    129        syntax.text( element );
    130    }
    131
    132    public void translateImage( final Element base ) {
    133        Element child = XmlUtil.getXPathElement( base, "TBODY/TR/TD/*" );
    134        if( child == null ) {
    135            child = base;
    136        }
    137        final Element img;
    138        final String href;
    139        if( child.getName().equals( "A" ) ) {
    140            img = child.getChild( "IMG" );
    141            href = child.getAttributeValue( "href" );
    142        } else {
    143            img = child;
    144            href = null;
    145        }
    146        if( img == null ) {
    147            return;
    148        }
    149        final String src = config.trimLink( img.getAttributeValue( "src" ) );
    150        if( src == null ) {
    151            return;
    152        }
    153
    154        final Map< String, Object > imageAttrs = new LinkedHashMap<>();
    155        putIfNotEmpty( imageAttrs, "align", base.getAttributeValue( "align" ) );
    156        putIfNotEmpty( imageAttrs, "height", img.getAttributeValue( "height" ) );
    157        putIfNotEmpty( imageAttrs, "width", img.getAttributeValue( "width" ) );
    158        putIfNotEmpty( imageAttrs, "alt", img.getAttributeValue( "alt" ) );
    159        putIfNotEmpty( imageAttrs, "caption", emptyToNull( ( Element )XPathFactory.instance().compile(  "CAPTION" ).evaluateFirst( base ) ) );
    160        putIfNotEmpty( imageAttrs, "link", href );
    161        putIfNotEmpty( imageAttrs, "border", img.getAttributeValue( "border" ) );
    162        putIfNotEmpty( imageAttrs, "style", base.getAttributeValue( "style" ) );
    163        syntax.image( src, imageAttrs );
    164    }
    165
    166    private void putIfNotEmpty( final Map< String, Object > map, final String key, final Object value ) {
    167        if( value != null ) {
    168            map.put( key, value );
    169        }
    170    }
    171
    172    private String emptyToNull( final Element e ) {
    173        if( e == null ) {
    174            return null;
    175        }
    176        final String s = e.getText();
    177        return s == null ? null : ( s.replaceAll( "\\s", "" ).isEmpty() ? null : s );
    178    }
    179
    180    private Map< Object, Object > getStylePropertiesLowerCase( final Element base ) {
    181        final String n = base.getName().toLowerCase();
    182
    183        // "font-weight: bold; font-style: italic;"
    184        String style = base.getAttributeValue( "style" );
    185        if( style == null ) {
    186            style = "";
    187        }
    188
    189        if( n.equals( "p" ) || n.equals( "div" ) ) {
    190            final String align = base.getAttributeValue( "align" );
    191            if( align != null ) {
    192                // only add the value of the align attribute if the text-align style didn't already exist.
    193                if( !style.contains( "text-align" ) ) {
    194                    style += ";text-align:" + align + ";";
    195                }
    196            }
    197        }
    198
    199        if( n.equals( "font" ) ) {
    200            final String color = base.getAttributeValue( "color" );
    201            final String face = base.getAttributeValue( "face" );
    202            final String size = base.getAttributeValue( "size" );
    203            if( color != null ) {
    204                style = style + "color:" + color + ";";
    205            }
    206            if( face != null ) {
    207                style = style + "font-family:" + face + ";";
    208            }
    209            if( size != null ) {
    210                switch ( size ) {
    211                    case "1": style += "font-size:xx-small;"; break;
    212                    case "2": style += "font-size:x-small;"; break;
    213                    case "3": style += "font-size:small;"; break;
    214                    case "4": style += "font-size:medium;"; break;
    215                    case "5": style += "font-size:large;"; break;
    216                    case "6": style += "font-size:x-large;"; break;
    217                    case "7": style += "font-size:xx-large;"; break;
    218                }
    219            }
    220        }
    221
    222        if( style.equals( "" ) ) {
    223            return null;
    224        }
    225
    226        final Map< Object, Object > m = new LinkedHashMap<>();
    227        Arrays.stream( style.toLowerCase().split( ";" ) )
    228              .filter( StringUtils::isNotBlank )
    229              .forEach( prop -> m.put( prop.split( ":" )[ 0 ].trim(), prop.split( ":" )[ 1 ].trim() ) );
    230        return m;
    231    }
    232
    233    private String propsToStyleString( final Map< Object, Object >  styleProps ) {
    234        return styleProps.entrySet().stream().map(entry -> " " + entry.getKey() + ": " + entry.getValue() + ";").collect(Collectors.joining());
    235    }
    236
    237    public void translateChildren( final Element base ) throws JDOMException {
    238        for( final Content c : base.getContent() ) {
    239            if( c instanceof Element ) {
    240                final Element e = ( Element )c;
    241                final String n = e.getName().toLowerCase();
    242                switch( n ) {
    243                    case "h1": syntax.h1( e ); break;
    244                    case "h2": syntax.h2( e ); break;
    245                    case "h3": syntax.h3( e ); break;
    246                    case "h4": syntax.h4( e ); break;
    247                    case "p": syntax.p( e ); break;
    248                    case "br": syntax.br( base, e ); break;
    249                    case "hr": syntax.hr( e ); break;
    250                    case "table": syntax.table( e ); break;
    251                    case "tbody": syntax.tbody( e ); break;
    252                    case "tr": syntax.tr( e ); break;
    253                    case "td": syntax.td( e ); break;
    254                    case "thead": syntax.thead( e ); break;
    255                    case "th": syntax.th( e ); break;
    256                    case "a": translateA( e ); break;
    257                    case "b":
    258                    case "strong": syntax.strong( e ); break;
    259                    case "i":
    260                    case "em":
    261                    case "address": syntax.em( e ); break;
    262                    case "u": syntax.underline( e ); break;
    263                    case "strike": syntax.strike( e ); break;
    264                    case "sub": syntax.sub( e ); break;
    265                    case "sup": syntax.sup( e ); break;
    266                    case "dl": syntax.dl( e ); break;
    267                    case "dt": syntax.dt( e ); break;
    268                    case "dd": syntax.dd( e ); break;
    269                    case "ul": syntax.ul( e ); break;
    270                    case "ol": syntax.ol( e ); break;
    271                    case "li": syntax.li( base, e ); break;
    272                    case "pre": syntax.pre( e ); break;
    273                    case "code":
    274                    case "tt": syntax.code( e ); break;
    275                    case "img": syntax.img( e ); break;
    276                    case "form": syntax.form( e ); break;
    277                    case "input": syntax.input( e ); break;
    278                    case "textarea": syntax.textarea( e ); break;
    279                    case "select": syntax.select( e ); break;
    280                    case "option": syntax.option( base, e ); break;
    281                    default: translate( e ); break;
    282                }
    283            } else {
    284                translate( c );
    285            }
    286        }
    287    }
    288
    289    void translateA( final Element e ) throws JDOMException {
    290        if( config.isNotIgnorableWikiMarkupLink( e ) ) {
    291            if( e.getChild( "IMG" ) != null ) {
    292                translateImage( e );
    293            } else {
    294                final String ref = config.trimLink( e.getAttributeValue( "href" ) );
    295                if( ref == null ) {
    296                    if( MarkupHelper.isUndefinedPageLink( e ) ) {
    297                        syntax.aUndefined( e );
    298                    } else {
    299                        translate( e );
    300                    }
    301                } else if( MarkupHelper.isFootnoteLink( ref ) ) {
    302                    final String href = ref.replaceFirst( "#ref-.+-(\\d+)", "$1" ); // convert "#ref-PageName-1" to just "1"
    303                    final String textValue = e.getValue().substring( 1, ( e.getValue().length() - 1 ) ); // remove the brackets around "[1]"
    304                    syntax.aFootnote( textValue, href );
    305                } else {
    306                    syntax.a( e, ref );
    307                }
    308            }
    309        }
    310    }
    311
    312    public void translateParagraph( final Element base ) throws JDOMException {
    313        final ElementDecoratorData dto = buildElementDecoratorDataFrom( base );
    314        syntax.paragraph( dto );
    315    }
    316
    317    ElementDecoratorData buildElementDecoratorDataFrom( final Element base ) {
    318        String n = base.getName().toLowerCase();
    319        boolean bold = false;
    320        boolean italic = false;
    321        boolean monospace = false;
    322        String cssSpecial = null;
    323        final String cssClass = base.getAttributeValue( "class" );
    324
    325        // accomodate a FCKeditor bug with Firefox: when a link is removed, it becomes <span class="wikipage">text</span>.
    326        final boolean ignoredCssClass = cssClass != null && cssClass.matches( "wikipage|createpage|external|interwiki|attachment|inline-code" );
    327
    328        Map< Object, Object > styleProps = null;
    329
    330        // Only get the styles if it's not a link element. Styles for link elements are handled as an AugmentedWikiLink instead.
    331        if( !n.equals( "a" ) ) {
    332            styleProps = getStylePropertiesLowerCase( base );
    333        }
    334
    335        if( "inline-code".equals( cssClass ) ) {
    336            monospace = true;
    337        }
    338
    339        if( styleProps != null ) {
    340            final String weight = ( String ) styleProps.remove( "font-weight" );
    341            final String style = ( String ) styleProps.remove( "font-style" );
    342
    343            if ( n.equals( "p" ) ) {
    344                // change it, so we can print out the css styles for <p>
    345                n = "div";
    346            }
    347
    348            italic = "oblique".equals( style ) || "italic".equals( style );
    349            bold = "bold".equals( weight ) || "bolder".equals( weight );
    350            if ( !styleProps.isEmpty() ) {
    351                cssSpecial = propsToStyleString( styleProps );
    352            }
    353        }
    354
    355        final ElementDecoratorData dto = new ElementDecoratorData();
    356        dto.base = base;
    357        dto.bold = bold;
    358        dto.cssClass = cssClass;
    359        dto.cssSpecial = cssSpecial;
    360        dto.htmlBase = n;
    361        dto.ignoredCssClass = ignoredCssClass;
    362        dto.italic = italic;
    363        dto.monospace = monospace;
    364        return dto;
    365    }
    366
    367    private class PreDeque extends ArrayDeque< String > {
    368
    369        private static final long serialVersionUID = 2401529529970187649L;
    370
    371        @Override
    372        public void addFirst( final String item ) {
    373            super.addFirst( item );
    374            outTrimmer.setWhitespaceTrimMode( isEmpty() );
    375        }
    376
    377        @Override
    378        public String removeFirst() {
    379            final String pop = super.removeFirst();
    380            outTrimmer.setWhitespaceTrimMode( isEmpty() );
    381            return pop;
    382        }
    383
    384    }
    385
    386    /**
    387     * Simple data placeholder class to move decoration state between plain text syntax translation related classes.
    388     */
    389    public static class ElementDecoratorData {
    390
    391        /** don't allow instantiation outside enclosing class. */
    392        private ElementDecoratorData() {}
    393
    394        public Element base;
    395        public String htmlBase;
    396        public String cssClass;
    397        public String cssSpecial;
    398        public boolean monospace;
    399        public boolean bold;
    400        public boolean italic;
    401        public boolean ignoredCssClass;
    402    }
    403
    404}