View Javadoc
1   package org.apache.maven.doxia.module.confluence.parser;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.List;
24  
25  import org.codehaus.plexus.util.StringUtils;
26  
27  /**
28   * Re-usable builder that can be used to generate paragraph and list item text from a string containing all the content
29   * and wiki formatting. This class is intentionally stateful, but cheap to create, so create one as needed and keep it
30   * on the stack to preserve stateless behaviour in the caller.
31   *
32   * @author Dave Syer
33   * @version $Id: ChildBlocksBuilder.java 1726451 2016-01-23 20:25:03Z rfscholte $
34   * @since 1.1
35   */
36  public class ChildBlocksBuilder
37  {
38      private boolean insideBold = false;
39  
40      private boolean insideItalic = false;
41  
42      private boolean insideLink = false;
43  
44      private boolean insideLinethrough = false;
45  
46      private boolean insideUnderline = false;
47  
48      private boolean insideSub = false;
49  
50      private boolean insideSup = false;
51  
52      private List<Block> blocks = new ArrayList<Block>();
53  
54      private StringBuilder text = new StringBuilder();
55  
56      private String input;
57  
58      private boolean insideMonospaced;
59  
60      /**
61       * <p>Constructor for ChildBlocksBuilder.</p>
62       *
63       * @param input the input.
64       */
65      public ChildBlocksBuilder( String input )
66      {
67          this.input = input;
68      }
69  
70      /**
71       * Utility method to convert marked up content into blocks for rendering.
72       *
73       * @return a list of Blocks that can be used to render it
74       */
75      public List<Block> getBlocks()
76      {
77          List<Block> specialBlocks = new ArrayList<Block>();
78  
79          for ( int i = 0; i < input.length(); i++ )
80          {
81              char c = input.charAt( i );
82  
83              switch ( c )
84              {
85                  case '*':
86                      if ( insideBold )
87                      {
88                          insideBold = false;
89                          specialBlocks = getList( new BoldBlock( getChildren( text, specialBlocks ) ), specialBlocks );
90                          text = new StringBuilder();
91                      }
92                      else if ( insideMonospaced )
93                      {
94                          text.append( c );
95                      }
96                      else
97                      {
98                          text = addTextBlockIfNecessary( blocks, specialBlocks, text );
99                          insideBold = true;
100                     }
101 
102                     break;
103                 case '_':
104                     if ( insideItalic )
105                     {
106                         insideItalic = false;
107                         specialBlocks = getList( new ItalicBlock( getChildren( text, specialBlocks ) ), specialBlocks );
108                         text = new StringBuilder();
109                     }
110                     else if ( insideLink || insideMonospaced )
111                     {
112                         text.append( c );
113                     }
114                     else
115                     {
116                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
117                         insideItalic = true;
118                     }
119 
120                     break;
121                 case '-':
122                     if ( insideLinethrough )
123                     {
124                         insideLinethrough = false;
125                         blocks.add( new LinethroughBlock( text.toString() ) );
126                         text = new StringBuilder();
127                     }
128                     else if ( insideLink || insideMonospaced )
129                     {
130                         text.append( c );    
131                     }
132                     else
133                     {
134                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
135                         insideLinethrough = true;                            
136                     }
137                     break;
138                 case '+':
139                     if ( insideUnderline )
140                     {
141                         insideUnderline = false;
142                         blocks.add( new UnderlineBlock( text.toString() ) );
143                         text = new StringBuilder();
144                     }
145                     else if ( insideLink || insideMonospaced )
146                     {
147                         text.append( c );    
148                     }
149                     else
150                     {
151                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
152                         insideUnderline = true;                            
153                     }
154                     break;
155                 case '~':
156                     if ( insideSub )
157                     {
158                         insideSub = false;
159                         blocks.add( new SubBlock( text.toString() ) );
160                         text = new StringBuilder();
161                     }
162                     else if ( insideLink || insideMonospaced )
163                     {
164                         text.append( c );    
165                     }
166                     else
167                     {
168                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
169                         insideSub = true;                            
170                     }
171                     break;
172                 case '^':
173                     if ( insideSup )
174                     {
175                         insideSup = false;
176                         blocks.add( new SupBlock( text.toString() ) );
177                         text = new StringBuilder();
178                     }
179                     else if ( insideLink || insideMonospaced )
180                     {
181                         text.append( c );    
182                     }
183                     else
184                     {
185                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
186                         insideSup = true;                            
187                     }
188                     break;
189                 case '[':
190                     if ( insideMonospaced )
191                     {
192                         text.append( c );
193                     }
194                     else
195                     {
196                         insideLink = true;
197                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
198                     }
199                     break;
200                 case ']':
201                     if ( insideLink )
202                     {
203                         boolean addHTMLSuffix = false;
204                         String link = text.toString();
205 
206                         if ( !link.endsWith( ".html" ) )
207                         {
208                             if ( !link.contains( "http" ) )
209                             {
210                                 // relative path: see DOXIA-298
211                                 addHTMLSuffix = true;
212                             }
213                         }
214                         if ( link.contains( "|" ) )
215                         {
216                             String[] pieces = StringUtils.split( text.toString(), "|" );
217                             
218                             if ( pieces[1].startsWith( "^" ) )
219                             {
220                                 // use the "file attachment" ^ syntax to force verbatim link: needed to allow actually
221                                 // linking to some non-html resources
222                                 pieces[1] = pieces[1].substring( 1 ); // now just get rid of the lead ^
223                                 addHTMLSuffix = false; // force verbatim link to support attaching files/resources (not
224                                                        // just .html files)
225                             }
226 
227                             if ( addHTMLSuffix )
228                             {
229                                 if ( !pieces[1].contains( "#" ) )
230                                 {
231                                     pieces[1] = pieces[1].concat( ".html" );
232                                 }
233                                 else
234                                 {
235                                     if ( !pieces[1].startsWith( "#" ) )
236                                     {
237                                         String[] temp = pieces[1].split( "#" );
238                                         pieces[1] = temp[0] + ".html#" + temp[1];
239                                     }
240                                 }
241                             }
242 
243                             blocks.add( new LinkBlock( pieces[1], pieces[0] ) );
244                         }
245                         else
246                         {
247                             String value = link;
248 
249                             if ( link.startsWith( "#" ) )
250                             {
251                                 value = link.substring( 1 );
252                             }
253                             else if ( link.startsWith( "^" ) )
254                             {
255                                 link = link.substring( 1 );  // chop off the lead ^ from link and from value
256                                 value = link;
257                                 addHTMLSuffix = false; // force verbatim link to support attaching files/resources (not
258                                                        // just .html files)
259                             }
260 
261                             if ( addHTMLSuffix )
262                             {
263                                 if ( !link.contains( "#" ) )
264                                 {
265                                     link = link.concat( ".html" );
266                                 }
267                                 else
268                                 {
269                                     if ( !link.startsWith( "#" ) )
270                                     {
271                                         String[] temp = link.split( "#" );
272                                         link = temp[0] + ".html#" + temp[1];
273                                     }
274                                 }
275                             }
276 
277                             blocks.add( new LinkBlock( link, value ) );
278                         }
279 
280                         text = new StringBuilder();
281                         insideLink = false;
282                     }
283                     else if ( insideMonospaced )
284                     {
285                         text.append( c );
286                     }
287 
288                     break;
289                 case '{':
290                     if ( insideMonospaced )
291                     {
292                         text.append( c );
293                     }
294                     else
295                     {
296                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
297 
298                         if ( nextChar( input, i ) == '{' ) // it's monospaced
299                         {
300                             i++;
301                             insideMonospaced = true;
302                         }
303                     }
304                     // else it's a confluence macro...
305 
306                     break;
307                 case '}':
308                     if ( nextChar( input, i ) == '}' )
309                     {
310                         i++;
311                         insideMonospaced = false;
312                         specialBlocks = getList( new MonospaceBlock( getChildren( text, specialBlocks ) ),
313                                                  specialBlocks );
314                         text = new StringBuilder();
315                     }
316                     else if ( insideMonospaced )
317                     {
318                         text.append( c );
319                     }
320                     else
321                     {
322                         String name = text.toString();
323                         if ( name.startsWith( "anchor:" ) )
324                         {
325                             blocks.add( new AnchorBlock( name.substring( "anchor:".length() ) ) );
326                         }
327                         else
328                         {
329                             blocks.add( new TextBlock( "{" + name + "}" ) );
330                         }
331                         text = new StringBuilder();
332                     }
333 
334                     break;
335                 case '\\':
336                     if ( insideMonospaced )
337                     {
338                         text.append( c );
339                     }
340                     else if ( nextChar( input, i ) == '\\' )
341                     {
342                         i++;
343                         text = addTextBlockIfNecessary( blocks, specialBlocks, text );
344                         blocks.add( new LinebreakBlock() );
345                     }
346                     else
347                     {
348                         // DOXIA-467 single trailing backward slash, double is considered linebreak
349                         if ( i == input.length() - 1 )
350                         {
351                             text.append( '\\' );
352                         }
353                         else
354                         {
355                             text.append( input.charAt( ++i ) );
356                         }
357                     }
358 
359                     break;
360                 default:
361                     text.append( c );
362             }
363 
364             if ( !specialBlocks.isEmpty() )
365             {
366                 if ( !insideItalic && !insideBold && !insideMonospaced )
367                 {
368                     blocks.addAll( specialBlocks );
369                     specialBlocks.clear();
370                 }
371             }
372 
373         }
374 
375         if ( text.length() > 0 )
376         {
377             blocks.add( new TextBlock( text.toString() ) );
378         }
379 
380         return blocks;
381     }
382 
383     private List<Block> getList( Block block, List<Block> currentBlocks )
384     {
385         List<Block> list = new ArrayList<Block>();
386 
387         if ( insideBold || insideItalic || insideMonospaced )
388         {
389             list.addAll( currentBlocks );
390         }
391 
392         list.add( block );
393 
394         return list;
395     }
396 
397     private List<Block> getChildren( StringBuilder buffer, List<Block> currentBlocks )
398     {
399         String txt = buffer.toString().trim();
400 
401         if ( currentBlocks.isEmpty() && StringUtils.isEmpty( txt ) )
402         {
403             return new ArrayList<Block>();
404         }
405 
406         ArrayList<Block> list = new ArrayList<Block>();
407 
408         if ( !insideBold && !insideItalic && !insideMonospaced )
409         {
410             list.addAll( currentBlocks );
411         }
412 
413         if ( StringUtils.isEmpty( txt ) )
414         {
415             return list;
416         }
417 
418         list.add( new TextBlock( txt ) );
419 
420         return list;
421     }
422 
423     private static char nextChar( String input, int i )
424     {
425         return input.length() > i + 1 ? input.charAt( i + 1 ) : '\0';
426     }
427 
428     private StringBuilder addTextBlockIfNecessary( List<Block> blcks, List<Block> specialBlocks, StringBuilder txt )
429     {
430         if ( txt.length() == 0 )
431         {
432             return txt;
433         }
434 
435         TextBlock textBlock = new TextBlock( txt.toString() );
436 
437         if ( !insideBold && !insideItalic && !insideMonospaced )
438         {
439             blcks.add( textBlock );
440         }
441         else
442         {
443             specialBlocks.add( textBlock );
444         }
445 
446         return new StringBuilder();
447     }
448 
449 }