001package org.apache.maven.doxia.module.confluence.parser; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.util.ArrayList; 023import java.util.List; 024 025import org.codehaus.plexus.util.StringUtils; 026 027/** 028 * Re-usable builder that can be used to generate paragraph and list item text from a string containing all the content 029 * and wiki formatting. This class is intentionally stateful, but cheap to create, so create one as needed and keep it 030 * on the stack to preserve stateless behaviour in the caller. 031 * 032 * @author Dave Syer 033 * @version $Id$ 034 * @since 1.1 035 */ 036public class ChildBlocksBuilder 037{ 038 private boolean insideBold = false; 039 040 private boolean insideItalic = false; 041 042 private boolean insideLink = false; 043 044 private boolean insideLinethrough = false; 045 046 private boolean insideUnderline = false; 047 048 private boolean insideSub = false; 049 050 private boolean insideSup = false; 051 052 private List<Block> blocks = new ArrayList<Block>(); 053 054 private StringBuilder text = new StringBuilder(); 055 056 private String input; 057 058 private boolean insideMonospaced; 059 060 /** 061 * <p>Constructor for ChildBlocksBuilder.</p> 062 * 063 * @param input the input. 064 */ 065 public ChildBlocksBuilder( String input ) 066 { 067 this.input = input; 068 } 069 070 /** 071 * Utility method to convert marked up content into blocks for rendering. 072 * 073 * @return a list of Blocks that can be used to render it 074 */ 075 public List<Block> getBlocks() 076 { 077 List<Block> specialBlocks = new ArrayList<Block>(); 078 079 for ( int i = 0; i < input.length(); i++ ) 080 { 081 char c = input.charAt( i ); 082 083 switch ( c ) 084 { 085 case '*': 086 if ( insideBold ) 087 { 088 insideBold = false; 089 specialBlocks = getList( new BoldBlock( getChildren( text, specialBlocks ) ), specialBlocks ); 090 text = new StringBuilder(); 091 } 092 else 093 { 094 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 095 insideBold = true; 096 } 097 098 break; 099 case '_': 100 if ( insideItalic ) 101 { 102 insideItalic = false; 103 specialBlocks = getList( new ItalicBlock( getChildren( text, specialBlocks ) ), specialBlocks ); 104 text = new StringBuilder(); 105 } 106 else if ( insideLink ) 107 { 108 text.append( '_' ); 109 } 110 else 111 { 112 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 113 insideItalic = true; 114 } 115 116 break; 117 case '-': 118 if ( insideLinethrough ) 119 { 120 insideLinethrough = false; 121 blocks.add( new LinethroughBlock( text.toString() ) ); 122 text = new StringBuilder(); 123 } 124 else if ( insideLink ) 125 { 126 text.append( c ); 127 } 128 else 129 { 130 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 131 insideLinethrough = true; 132 } 133 break; 134 case '+': 135 if ( insideUnderline ) 136 { 137 insideUnderline = false; 138 blocks.add( new UnderlineBlock( text.toString() ) ); 139 text = new StringBuilder(); 140 } 141 else if ( insideLink ) 142 { 143 text.append( c ); 144 } 145 else 146 { 147 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 148 insideUnderline = true; 149 } 150 break; 151 case '~': 152 if ( insideSub ) 153 { 154 insideSub = false; 155 blocks.add( new SubBlock( text.toString() ) ); 156 text = new StringBuilder(); 157 } 158 else if ( insideLink ) 159 { 160 text.append( c ); 161 } 162 else 163 { 164 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 165 insideSub = true; 166 } 167 break; 168 case '^': 169 if ( insideSup ) 170 { 171 insideSup = false; 172 blocks.add( new SupBlock( text.toString() ) ); 173 text = new StringBuilder(); 174 } 175 else if ( insideLink ) 176 { 177 text.append( c ); 178 } 179 else 180 { 181 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 182 insideSup = true; 183 } 184 break; 185 case '[': 186 insideLink = true; 187 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 188 break; 189 case ']': 190 if ( insideLink ) 191 { 192 boolean addHTMLSuffix = false; 193 String link = text.toString(); 194 195 if ( !link.endsWith( ".html" ) ) 196 { 197 if ( !link.contains( "http" ) ) 198 { 199 // relative path: see DOXIA-298 200 addHTMLSuffix = true; 201 } 202 } 203 if ( link.contains( "|" ) ) 204 { 205 String[] pieces = StringUtils.split( text.toString(), "|" ); 206 207 if ( pieces[1].startsWith( "^" ) ) 208 { 209 // use the "file attachment" ^ syntax to force verbatim link: needed to allow actually 210 // linking to some non-html resources 211 pieces[1] = pieces[1].substring( 1 ); // now just get rid of the lead ^ 212 addHTMLSuffix = false; // force verbatim link to support attaching files/resources (not 213 // just .html files) 214 } 215 216 if ( addHTMLSuffix ) 217 { 218 if ( !pieces[1].contains( "#" ) ) 219 { 220 pieces[1] = pieces[1].concat( ".html" ); 221 } 222 else 223 { 224 if ( !pieces[1].startsWith( "#" ) ) 225 { 226 String[] temp = pieces[1].split( "#" ); 227 pieces[1] = temp[0] + ".html#" + temp[1]; 228 } 229 } 230 } 231 232 blocks.add( new LinkBlock( pieces[1], pieces[0] ) ); 233 } 234 else 235 { 236 String value = link; 237 238 if ( link.startsWith( "#" ) ) 239 { 240 value = link.substring( 1 ); 241 } 242 else if ( link.startsWith( "^" ) ) 243 { 244 link = link.substring( 1 ); // chop off the lead ^ from link and from value 245 value = link; 246 addHTMLSuffix = 247 false; // force verbatim link to support attaching files/resources (not just .html files) 248 } 249 250 if ( addHTMLSuffix ) 251 { 252 if ( !link.contains( "#" ) ) 253 { 254 link = link.concat( ".html" ); 255 } 256 else 257 { 258 if ( !link.startsWith( "#" ) ) 259 { 260 String[] temp = link.split( "#" ); 261 link = temp[0] + ".html#" + temp[1]; 262 } 263 } 264 } 265 266 blocks.add( new LinkBlock( link, value ) ); 267 } 268 269 text = new StringBuilder(); 270 insideLink = false; 271 } 272 273 break; 274 case '{': 275 276 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 277 278 if ( nextChar( input, i ) == '{' ) // it's monospaced 279 { 280 i++; 281 insideMonospaced = true; 282 } 283 // else it's a confluence macro... 284 285 break; 286 case '}': 287 288 // System.out.println( "line = " + line ); 289 290 if ( nextChar( input, i ) == '}' ) 291 { 292 i++; 293 insideMonospaced = false; 294 specialBlocks = getList( new MonospaceBlock( getChildren( text, specialBlocks ) ), 295 specialBlocks ); 296 text = new StringBuilder(); 297 } 298 else 299 { 300 String name = text.toString(); 301 if ( name.startsWith( "anchor:" ) ) 302 { 303 blocks.add( new AnchorBlock( name.substring( "anchor:".length() ) ) ); 304 } 305 else 306 { 307 blocks.add( new TextBlock( "{" + name + "}" ) ); 308 } 309 text = new StringBuilder(); 310 } 311 312 break; 313 case '\\': 314 if ( insideMonospaced ) 315 { 316 text.append( c ); 317 } 318 else if ( nextChar( input, i ) == '\\' ) 319 { 320 i++; 321 text = addTextBlockIfNecessary( blocks, specialBlocks, text ); 322 blocks.add( new LinebreakBlock() ); 323 } 324 else 325 { 326 // DOXIA-467 single trailing backward slash, double is considered linebreak 327 if ( i == input.length() - 1 ) 328 { 329 text.append( '\\' ); 330 } 331 else 332 { 333 text.append( input.charAt( ++i ) ); 334 } 335 } 336 337 break; 338 default: 339 text.append( c ); 340 } 341 342 if ( !specialBlocks.isEmpty() ) 343 { 344 if ( !insideItalic && !insideBold && !insideMonospaced ) 345 { 346 blocks.addAll( specialBlocks ); 347 specialBlocks.clear(); 348 } 349 } 350 351 } 352 353 if ( text.length() > 0 ) 354 { 355 blocks.add( new TextBlock( text.toString() ) ); 356 } 357 358 return blocks; 359 } 360 361 private List<Block> getList( Block block, List<Block> currentBlocks ) 362 { 363 List<Block> list = new ArrayList<Block>(); 364 365 if ( insideBold || insideItalic || insideMonospaced ) 366 { 367 list.addAll( currentBlocks ); 368 } 369 370 list.add( block ); 371 372 return list; 373 } 374 375 private List<Block> getChildren( StringBuilder buffer, List<Block> currentBlocks ) 376 { 377 String txt = buffer.toString().trim(); 378 379 if ( currentBlocks.isEmpty() && StringUtils.isEmpty( txt ) ) 380 { 381 return new ArrayList<Block>(); 382 } 383 384 ArrayList<Block> list = new ArrayList<Block>(); 385 386 if ( !insideBold && !insideItalic && !insideMonospaced ) 387 { 388 list.addAll( currentBlocks ); 389 } 390 391 if ( StringUtils.isEmpty( txt ) ) 392 { 393 return list; 394 } 395 396 list.add( new TextBlock( txt ) ); 397 398 return list; 399 } 400 401 private static char nextChar( String input, int i ) 402 { 403 return input.length() > i + 1 ? input.charAt( i + 1 ) : '\0'; 404 } 405 406 private StringBuilder addTextBlockIfNecessary( List<Block> blcks, List<Block> specialBlocks, StringBuilder txt ) 407 { 408 if ( txt.length() == 0 ) 409 { 410 return txt; 411 } 412 413 TextBlock textBlock = new TextBlock( txt.toString() ); 414 415 if ( !insideBold && !insideItalic && !insideMonospaced ) 416 { 417 blcks.add( textBlock ); 418 } 419 else 420 { 421 specialBlocks.add( textBlock ); 422 } 423 424 return new StringBuilder(); 425 } 426 427}