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}