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: ChildBlocksBuilder.html 979316 2016-02-02 21:51:43Z hboutemy $
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 if ( insideMonospaced )
093                    {
094                        text.append( c );
095                    }
096                    else
097                    {
098                        text = addTextBlockIfNecessary( blocks, specialBlocks, text );
099                        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}