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}