View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.util.parser;
35  
36  import java.util.ArrayDeque;
37  import java.util.Collections;
38  import java.util.Deque;
39  import java.util.HashSet;
40  import java.util.LinkedList;
41  import java.util.List;
42  import java.util.Set;
43  import java.util.regex.Matcher;
44  import java.util.regex.Pattern;
45  
46  import fr.paris.lutece.portal.business.editor.ParserComplexElement;
47  import fr.paris.lutece.portal.business.editor.ParserElement;
48  
49  /**
50   *
51   * This class provides parser utils for BBCODE parsing
52   *
53   */
54  public final class BbcodeUtil
55  {
56      private static final String CR_LF = "(?:\r\n|\r|\n)?";
57  
58      /**
59       * Instantiates a new bbcode util.
60       */
61      private BbcodeUtil( )
62      {
63      }
64  
65      /**
66       * Parse BBCODE text and return HTML text
67       * 
68       * @param value
69       *            the value of the text
70       * @param listParserElement
71       *            the list of simple parser element using for parsing
72       * @param listParserComplexElement
73       *            the list of complex parser element using for parsing
74       * @return HTML Text
75       */
76      public static String parse( String value, List<ParserElement> listParserElement, List<ParserComplexElement> listParserComplexElement )
77      {
78          StringBuilder buffer = new StringBuilder( value );
79  
80          // complex Element
81          if ( listParserComplexElement != null )
82          {
83              for ( ParserComplexElement element : listParserComplexElement )
84              {
85                  processNestedTags( buffer, element.getTagName( ), element.getOpenSubstWithParam( ), element.getCloseSubstWithParam( ),
86                          element.getOpenSubstWithoutParam( ), element.getCloseSubstWithoutParam( ), element.getInternalSubst( ),
87                          element.isProcessInternalTags( ), element.isAcceptParam( ), element.isRequiresQuotedParam( ) );
88              }
89          }
90  
91          String str = buffer.toString( );
92  
93          // SimpleElement
94          if ( listParserElement != null )
95          {
96              for ( ParserElement element : listParserElement )
97              {
98                  str = str.replaceAll( element.getCode( ), element.getValue( ) );
99              }
100         }
101 
102         return str;
103     }
104 
105     /**
106      *
107      * Method using for parsing complex element
108      * 
109      * @param buffer
110      *            value to parse
111      * @param tagName
112      *            tagName
113      * @param openSubstWithParam
114      *            openSubstWithParam
115      * @param closeSubstWithParam
116      *            closeSubstWithParam
117      * @param openSubstWithoutParam
118      *            openSubstWithoutParam
119      * @param closeSubstWithoutParam
120      *            closeSubstWithoutParam
121      * @param internalSubst
122      *            internalSubst
123      * @param processInternalTags
124      *            processInternalTags
125      * @param acceptParam
126      *            acceptParam
127      * @param requiresQuotedParam
128      *            requiresQuotedParam
129      */
130     private static void processNestedTags( StringBuilder buffer, String tagName, String openSubstWithParam, String closeSubstWithParam,
131             String openSubstWithoutParam, String closeSubstWithoutParam, String internalSubst, boolean processInternalTags, boolean acceptParam,
132             boolean requiresQuotedParam )
133     {
134         String str = buffer.toString( );
135 
136         Deque<MutableCharSequence> openStack = new ArrayDeque<>( );
137         Set<MutableCharSequence> subsOpen = new HashSet<>( );
138         Set<MutableCharSequence> subsClose = new HashSet<>( );
139         Set<MutableCharSequence> subsInternal = new HashSet<>( );
140 
141         String openTag = CR_LF + "\\[" + tagName + getOpenTag( acceptParam, requiresQuotedParam ) + "\\]" + CR_LF;
142         String closeTag = CR_LF + "\\[/" + tagName + "\\]" + CR_LF;
143         String internTag = CR_LF + "\\[\\*\\]" + CR_LF;
144 
145         String patternString = "(" + openTag + ")|(" + closeTag + ")";
146 
147         if ( processInternalTags )
148         {
149             patternString += ( "|(" + internTag + ")" );
150         }
151 
152         Pattern tagsPattern = Pattern.compile( patternString );
153         Matcher matcher = tagsPattern.matcher( str );
154 
155         int openTagGroup;
156         int paramGroup;
157         int closeTagGroup;
158         int internalTagGroup;
159 
160         if ( acceptParam )
161         {
162             openTagGroup = 1;
163             paramGroup = 2;
164             closeTagGroup = 3;
165             internalTagGroup = 4;
166         }
167         else
168         {
169             openTagGroup = 1;
170             paramGroup = -1; // INFO
171             closeTagGroup = 2;
172             internalTagGroup = 3;
173         }
174 
175         while ( matcher.find( ) )
176         {
177             int length = matcher.end( ) - matcher.start( );
178             MutableCharSequence matchedSeq = new MutableCharSequence( str, matcher.start( ), length );
179 
180             // test opening tags
181             if ( matcher.group( openTagGroup ) != null )
182             {
183                 if ( acceptParam && ( matcher.group( paramGroup ) != null ) )
184                 {
185                     matchedSeq._strParam = matcher.group( paramGroup );
186                 }
187 
188                 openStack.push( matchedSeq );
189 
190                 // test closing tags
191             }
192             else
193                 if ( ( matcher.group( closeTagGroup ) != null ) && !openStack.isEmpty( ) )
194                 {
195                     MutableCharSequence openSeq = openStack.pop( );
196 
197                     if ( acceptParam )
198                     {
199                         matchedSeq._strParam = openSeq._strParam;
200                     }
201 
202                     subsOpen.add( openSeq );
203                     subsClose.add( matchedSeq );
204 
205                     // test internal tags
206                 }
207                 else
208                     if ( processInternalTags && ( matcher.group( internalTagGroup ) != null ) && ( !openStack.isEmpty( ) ) )
209                     {
210                         subsInternal.add( matchedSeq );
211                     }
212         }
213 
214         LinkedList<MutableCharSequence> subst = new LinkedList<>( );
215         subst.addAll( subsOpen );
216         subst.addAll( subsClose );
217         subst.addAll( subsInternal );
218 
219         Collections.sort( subst, ( Object o1, Object o2 ) -> {
220             MutableCharSequence s1 = (MutableCharSequence) o1;
221             MutableCharSequence s2 = (MutableCharSequence) o2;
222 
223             return -( s1._nStart - s2._nStart );
224         } );
225 
226         buffer.delete( 0, buffer.length( ) );
227 
228         int start = 0;
229 
230         while ( !subst.isEmpty( ) )
231         {
232             MutableCharSequence seq = subst.removeLast( );
233             buffer.append( str.substring( start, seq._nStart ) );
234 
235             if ( subsClose.contains( seq ) )
236             {
237                 if ( seq._strParam != null )
238                 {
239                     buffer.append( closeSubstWithParam );
240                 }
241                 else
242                 {
243                     buffer.append( closeSubstWithoutParam );
244                 }
245             }
246             else
247                 if ( subsInternal.contains( seq ) )
248                 {
249                     buffer.append( internalSubst );
250                 }
251                 else
252                     if ( subsOpen.contains( seq ) )
253                     {
254                         Matcher m = Pattern.compile( openTag ).matcher( str.substring( seq._nStart, seq._nStart + seq._bLength ) );
255 
256                         if ( m.matches( ) )
257                         {
258                             if ( acceptParam && ( seq._strParam != null ) )
259                             {
260                                 buffer.append( //
261                                         openSubstWithParam.replaceAll( "\\{BBCODE_PARAM\\}", seq._strParam ) );
262                             }
263                             else
264                             {
265                                 buffer.append( openSubstWithoutParam );
266                             }
267                         }
268                     }
269 
270             start = seq._nStart + seq._bLength;
271         }
272 
273         buffer.append( str.substring( start ) );
274     }
275 
276     /**
277      *
278      * Inner class MutableCharSequence
279      *
280      */
281     static class MutableCharSequence implements CharSequence
282     {
283         /** _cBase */
284         private CharSequence _cBase;
285 
286         /** _nStart */
287         private int _nStart;
288 
289         /** _bLength */
290         private int _bLength;
291 
292         /** */
293         private String _strParam;
294 
295         /**
296          * MutableCharSequence
297          */
298         public MutableCharSequence( )
299         {
300         }
301 
302         /**
303          * @param base
304          *            base
305          * @param start
306          *            start
307          * @param length
308          *            length
309          */
310         public MutableCharSequence( CharSequence base, int start, int length )
311         {
312             reset( base, start, length );
313         }
314 
315         /**
316          * @return see {@link java.lang.CharSequence#length()}
317          */
318         @Override
319         public int length( )
320         {
321             return _bLength;
322         }
323 
324         /**
325          * @param index
326          *            index
327          * @return see {@link java.lang.CharSequence#charAt(int)}
328          */
329         @Override
330         public char charAt( int index )
331         {
332             return _cBase.charAt( _nStart + index );
333         }
334 
335         /**
336          * @param pStart
337          *            pStart
338          * @param end
339          *            end
340          * @return see {@link java.lang.CharSequence#subSequence(int, int)}
341          */
342         @Override
343         public CharSequence subSequence( int pStart, int end )
344         {
345             return new MutableCharSequence( _cBase, _nStart + pStart, _nStart + ( end - pStart ) );
346         }
347 
348         /**
349          * return CharSequence
350          * 
351          * @param pBase
352          *            pBase
353          * @param pStart
354          *            pStart
355          * @param pLength
356          *            pLength
357          * @return CharSequence
358          */
359         public CharSequence reset( CharSequence pBase, int pStart, int pLength )
360         {
361             _cBase = pBase;
362             _nStart = pStart;
363             _bLength = pLength;
364 
365             return this;
366         }
367 
368         /**
369          *
370          * @return see {@link java.lang.Object#toString()}
371          */
372         @Override
373         public String toString( )
374         {
375             StringBuilder sb = new StringBuilder( );
376 
377             for ( int i = _nStart; i < ( _nStart + _bLength ); i++ )
378             {
379                 sb.append( _cBase.charAt( i ) );
380             }
381 
382             return sb.toString( );
383         }
384     }
385 
386     private static String getOpenTag( boolean acceptParam, boolean requiresQuotedParam )
387     {
388         String tag = "";
389         if ( acceptParam )
390         {
391             if ( requiresQuotedParam )
392             {
393                 tag = "(?:=\"(.*?)\")?";
394             }
395             else
396             {
397                 tag = "(?:=\"?(.*?)\"?)?";
398             }
399         }
400         return tag;
401     }
402 }