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 1542771 2013-11-17 17:50:31Z hboutemy $
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
93 {
94 text = addTextBlockIfNecessary( blocks, specialBlocks, text );
95 insideBold = true;
96 }
97
98 break;
99 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 }