Mercurial Hosting > luan
annotate src/goodjava/bbcode/BBCode.java @ 1704:19df8abc9805
add Table.is_list
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Fri, 01 Jul 2022 15:53:03 -0600 |
parents | 3a61451f8130 |
children | a6e27c8e7ef4 |
rev | line source |
---|---|
1702 | 1 package goodjava.bbcode; |
585 | 2 |
637
6ea90dc10375
bbcode parser now takes a quoter function
Franklin Schmidt <fschmidt@gmail.com>
parents:
588
diff
changeset
|
3 import java.util.List; |
6ea90dc10375
bbcode parser now takes a quoter function
Franklin Schmidt <fschmidt@gmail.com>
parents:
588
diff
changeset
|
4 import java.util.ArrayList; |
1703 | 5 import java.util.regex.Matcher; |
6 import java.util.regex.Pattern; | |
1402
27efb1fcbcb5
move luan.lib to goodjava
Franklin Schmidt <fschmidt@gmail.com>
parents:
1335
diff
changeset
|
7 import goodjava.parser.Parser; |
637
6ea90dc10375
bbcode parser now takes a quoter function
Franklin Schmidt <fschmidt@gmail.com>
parents:
588
diff
changeset
|
8 |
585 | 9 |
10 public final class BBCode { | |
11 | |
1702 | 12 public enum Target { HTML, TEXT } |
585 | 13 |
1702 | 14 public interface Quoter { |
15 public String quote(Target target,String text,String param); | |
585 | 16 } |
17 | |
1702 | 18 public static final Quoter defaultQuoter = new Quoter() { |
19 public String quote(Target target,String text,String param) { | |
20 StringBuilder sb = new StringBuilder(); | |
21 sb.append( "<blockquote>" ); | |
22 if( param != null ) { | |
23 sb.append( htmlEncode(param) ); | |
24 sb.append( " wrote:\n" ); | |
25 } | |
26 sb.append( text ); | |
27 sb.append( "</blockquote>" ); | |
28 return sb.toString(); | |
29 } | |
30 }; | |
585 | 31 |
1702 | 32 public Target target = Target.HTML; |
33 public Quoter quoter = defaultQuoter; | |
34 private final Parser parser; | |
35 | |
36 public BBCode(String text) { | |
585 | 37 this.parser = new Parser(text); |
38 } | |
39 | |
1702 | 40 public String parse() { |
585 | 41 StringBuilder sb = new StringBuilder(); |
1247 | 42 StringBuilder text = new StringBuilder(); |
585 | 43 while( !parser.endOfInput() ) { |
44 String block = parseBlock(); | |
1247 | 45 if( block != null ) { |
46 sb.append( textToString(text) ); | |
585 | 47 sb.append(block); |
1247 | 48 } else { |
49 text.append( parser.currentChar() ); | |
585 | 50 parser.anyChar(); |
51 } | |
52 } | |
1247 | 53 sb.append( textToString(text) ); |
585 | 54 return sb.toString(); |
55 } | |
56 | |
1702 | 57 private String parseWellFormed() { |
585 | 58 StringBuilder sb = new StringBuilder(); |
1247 | 59 StringBuilder text = new StringBuilder(); |
585 | 60 while( !parser.endOfInput() ) { |
61 String block = parseBlock(); | |
62 if( block != null ) { | |
1247 | 63 sb.append( textToString(text) ); |
585 | 64 sb.append(block); |
65 continue; | |
66 } | |
67 if( couldBeTag() ) | |
68 break; | |
1247 | 69 text.append( parser.currentChar() ); |
585 | 70 parser.anyChar(); |
71 } | |
1247 | 72 sb.append( textToString(text) ); |
585 | 73 return sb.toString(); |
74 } | |
75 | |
1702 | 76 private String textToString(StringBuilder text) { |
1247 | 77 String s = text.toString(); |
78 text.setLength(0); | |
1702 | 79 if( target == Target.HTML ) |
80 s = htmlEncode(s); | |
1247 | 81 return s; |
82 } | |
83 | |
585 | 84 private boolean couldBeTag() { |
85 if( parser.currentChar() != '[' ) | |
86 return false; | |
87 return parser.testIgnoreCase("[b]") | |
88 || parser.testIgnoreCase("[/b]") | |
89 || parser.testIgnoreCase("[i]") | |
90 || parser.testIgnoreCase("[/i]") | |
91 || parser.testIgnoreCase("[u]") | |
92 || parser.testIgnoreCase("[/u]") | |
93 || parser.testIgnoreCase("[url]") | |
94 || parser.testIgnoreCase("[url=") | |
95 || parser.testIgnoreCase("[/url]") | |
96 || parser.testIgnoreCase("[code]") | |
97 || parser.testIgnoreCase("[/code]") | |
98 || parser.testIgnoreCase("[img]") | |
99 || parser.testIgnoreCase("[/img]") | |
100 || parser.testIgnoreCase("[color=") | |
101 || parser.testIgnoreCase("[/color]") | |
102 || parser.testIgnoreCase("[size=") | |
103 || parser.testIgnoreCase("[/size]") | |
1703 | 104 || parser.testIgnoreCase("[video]") |
105 || parser.testIgnoreCase("[/video]") | |
585 | 106 || parser.testIgnoreCase("[quote]") |
107 || parser.testIgnoreCase("[quote=") | |
108 || parser.testIgnoreCase("[/quote]") | |
109 ; | |
110 } | |
111 | |
1702 | 112 private String parseBlock() { |
585 | 113 if( parser.currentChar() != '[' ) |
114 return null; | |
115 String s; | |
116 s = parseB(); if(s!=null) return s; | |
117 s = parseI(); if(s!=null) return s; | |
118 s = parseU(); if(s!=null) return s; | |
119 s = parseUrl1(); if(s!=null) return s; | |
120 s = parseUrl2(); if(s!=null) return s; | |
121 s = parseCode(); if(s!=null) return s; | |
122 s = parseImg(); if(s!=null) return s; | |
123 s = parseColor(); if(s!=null) return s; | |
124 s = parseSize(); if(s!=null) return s; | |
1703 | 125 s = parseVideo(); if(s!=null) return s; |
585 | 126 s = parseQuote1(); if(s!=null) return s; |
127 s = parseQuote2(); if(s!=null) return s; | |
128 return null; | |
129 } | |
130 | |
1702 | 131 private String parseB() { |
585 | 132 parser.begin(); |
133 if( !parser.matchIgnoreCase("[b]") ) | |
134 return parser.failure(null); | |
135 String content = parseWellFormed(); | |
136 if( !parser.matchIgnoreCase("[/b]") ) | |
137 return parser.failure(null); | |
1702 | 138 String rtn = target==Target.HTML ? "<b>"+content+"</b>" : content; |
585 | 139 return parser.success(rtn); |
140 } | |
141 | |
1702 | 142 private String parseI() { |
585 | 143 parser.begin(); |
144 if( !parser.matchIgnoreCase("[i]") ) | |
145 return parser.failure(null); | |
146 String content = parseWellFormed(); | |
147 if( !parser.matchIgnoreCase("[/i]") ) | |
148 return parser.failure(null); | |
1702 | 149 String rtn = target==Target.HTML ? "<i>"+content+"</i>" : content; |
585 | 150 return parser.success(rtn); |
151 } | |
152 | |
1702 | 153 private String parseU() { |
585 | 154 parser.begin(); |
155 if( !parser.matchIgnoreCase("[u]") ) | |
156 return parser.failure(null); | |
157 String content = parseWellFormed(); | |
158 if( !parser.matchIgnoreCase("[/u]") ) | |
159 return parser.failure(null); | |
1702 | 160 String rtn = target==Target.HTML ? "<u>"+content+"</u>" : content; |
585 | 161 return parser.success(rtn); |
162 } | |
163 | |
164 private String parseUrl1() { | |
165 parser.begin(); | |
166 if( !parser.matchIgnoreCase("[url]") ) | |
167 return parser.failure(null); | |
168 String url = parseRealUrl(); | |
169 if( !parser.matchIgnoreCase("[/url]") ) | |
170 return parser.failure(null); | |
1703 | 171 String rtn = target==Target.HTML ? "<a href='" + url + "'>" + url + "</a>" : url; |
585 | 172 return parser.success(rtn); |
173 } | |
174 | |
1702 | 175 private String parseUrl2() { |
585 | 176 parser.begin(); |
177 if( !parser.matchIgnoreCase("[url=") ) | |
178 return parser.failure(null); | |
179 String url = parseRealUrl(); | |
180 if( !parser.match(']') ) | |
181 return parser.failure(null); | |
182 String content = parseWellFormed(); | |
183 if( !parser.matchIgnoreCase("[/url]") ) | |
184 return parser.failure(null); | |
1703 | 185 String rtn = target==Target.HTML ? "<a href='" + url + "'>" + content + "</a>" : content; |
585 | 186 return parser.success(rtn); |
187 } | |
188 | |
189 private String parseRealUrl() { | |
190 parser.begin(); | |
191 while( parser.match(' ') ); | |
192 int start = parser.currentIndex(); | |
193 if( !parser.matchIgnoreCase("http") ) | |
194 return parser.failure(null); | |
195 parser.matchIgnoreCase("s"); | |
196 if( !parser.matchIgnoreCase("://") ) | |
197 return parser.failure(null); | |
198 while( parser.noneOf(" []'") ); | |
199 String url = parser.textFrom(start); | |
200 while( parser.match(' ') ); | |
201 return parser.success(url); | |
202 } | |
203 | |
204 private String parseCode() { | |
205 parser.begin(); | |
206 if( !parser.matchIgnoreCase("[code]") ) | |
207 return parser.failure(null); | |
208 int start = parser.currentIndex(); | |
209 while( !parser.testIgnoreCase("[/code]") ) { | |
210 if( !parser.anyChar() ) | |
211 return parser.failure(null); | |
212 } | |
213 String content = parser.textFrom(start); | |
214 if( !parser.matchIgnoreCase("[/code]") ) throw new RuntimeException(); | |
1702 | 215 String rtn = target==Target.HTML ? "<code>"+content+"</code>" : content; |
585 | 216 return parser.success(rtn); |
217 } | |
218 | |
219 private String parseImg() { | |
220 parser.begin(); | |
221 if( !parser.matchIgnoreCase("[img]") ) | |
222 return parser.failure(null); | |
223 String url = parseRealUrl(); | |
224 if( !parser.matchIgnoreCase("[/img]") ) | |
225 return parser.failure(null); | |
1703 | 226 String rtn = target==Target.HTML ? "<img src='" + url + "'>" : ""; |
585 | 227 return parser.success(rtn); |
228 } | |
229 | |
1702 | 230 private String parseColor() { |
585 | 231 parser.begin(); |
232 if( !parser.matchIgnoreCase("[color=") ) | |
233 return parser.failure(null); | |
234 int start = parser.currentIndex(); | |
235 parser.match('#'); | |
236 while( parser.inCharRange('0','9') | |
237 || parser.inCharRange('a','z') | |
238 || parser.inCharRange('A','Z') | |
239 ); | |
240 String color = parser.textFrom(start); | |
241 if( !parser.match(']') ) | |
242 return parser.failure(null); | |
243 String content = parseWellFormed(); | |
244 if( !parser.matchIgnoreCase("[/color]") ) | |
245 return parser.failure(null); | |
1702 | 246 String rtn = target==Target.HTML ? "<span style='color: "+color+"'>"+content+"</span>" : content; |
585 | 247 return parser.success(rtn); |
248 } | |
249 | |
1702 | 250 private String parseSize() { |
585 | 251 parser.begin(); |
252 if( !parser.matchIgnoreCase("[size=") ) | |
253 return parser.failure(null); | |
254 int start = parser.currentIndex(); | |
255 while( parser.match('.') || parser.inCharRange('0','9') ); | |
256 String size = parser.textFrom(start); | |
257 if( !parser.match(']') ) | |
258 return parser.failure(null); | |
259 String content = parseWellFormed(); | |
260 if( !parser.matchIgnoreCase("[/size]") ) | |
261 return parser.failure(null); | |
1702 | 262 String rtn = target==Target.HTML ? "<span style='font-size: "+size+"em'>"+content+"</span>" : content; |
585 | 263 return parser.success(rtn); |
264 } | |
265 | |
1703 | 266 private static final Pattern youtubePtn1 = Pattern.compile("https://youtu.be/([a-zA-Z0-9_-]+)(?:\\?t=([0-9]+))?"); |
267 private static final Pattern youtubePtn2 = Pattern.compile("https://www.youtube.com/watch?v=([a-zA-Z0-9_-]+)(?:\\?t=([0-9]+)s)?"); | |
268 private static final Pattern bitchutePtn = Pattern.compile("https://www.bitchute.com/video/([a-zA-Z0-9]+)/"); | |
269 | |
270 private static Matcher find(Pattern ptn,String s) { | |
271 Matcher m = ptn.matcher(s); | |
272 return m.find() ? m : null; | |
273 } | |
274 | |
275 private String parseVideo() { | |
585 | 276 parser.begin(); |
1703 | 277 if( !parser.matchIgnoreCase("[video]") ) |
278 return parser.failure(null); | |
279 String url = parseRealUrl(); | |
280 if( !parser.matchIgnoreCase("[/video]") ) | |
585 | 281 return parser.failure(null); |
1703 | 282 if( target==Target.TEXT ) |
283 return parser.success( "" ); | |
284 Matcher m; | |
285 m = find(youtubePtn1,url); | |
286 if( m == null ) | |
287 m = find(youtubePtn2,url); | |
288 if( m != null ) { | |
289 StringBuilder sb = new StringBuilder(); | |
290 sb.append( "<iframe width='420' height='315' src='https://www.youtube.com/embed/" ); | |
291 sb.append( m.group(1) ); | |
292 String t = m.group(2); | |
293 if( t != null ) { | |
294 sb.append( "?start=" ); | |
295 sb.append( t ); | |
296 } | |
297 sb.append( "' frameborder='0' allowfullscreen></iframe>" ); | |
298 return parser.success( sb.toString() ); | |
299 } | |
300 m = find(bitchutePtn,url); | |
301 if( m != null ) { | |
302 StringBuilder sb = new StringBuilder(); | |
303 sb.append( "<iframe width='420' height='315' scrolling='no' frameborder='0' style='border: none;' src='https://www.bitchute.com/embed/" ); | |
304 sb.append( m.group(1) ); | |
305 sb.append( "/'></iframe>" ); | |
306 return parser.success( sb.toString() ); | |
307 } | |
308 return parser.success( "<a href='" + url + "'>" + url + "</a>" ); | |
585 | 309 } |
310 | |
1703 | 311 |
1702 | 312 private String parseQuote1() { |
585 | 313 parser.begin(); |
314 if( !parser.matchIgnoreCase("[quote]") ) | |
315 return parser.failure(null); | |
316 String content = parseWellFormed(); | |
317 if( !parser.matchIgnoreCase("[/quote]") ) | |
318 return parser.failure(null); | |
1702 | 319 String rtn = quoter.quote(target,content,null); |
585 | 320 return parser.success(rtn); |
321 } | |
322 | |
1702 | 323 private String parseQuote2() { |
585 | 324 parser.begin(); |
325 if( !parser.matchIgnoreCase("[quote=") ) | |
326 return parser.failure(null); | |
637
6ea90dc10375
bbcode parser now takes a quoter function
Franklin Schmidt <fschmidt@gmail.com>
parents:
588
diff
changeset
|
327 List args = new ArrayList(); |
585 | 328 int start = parser.currentIndex(); |
1702 | 329 while( parser.noneOf("[]") ); |
585 | 330 String name = parser.textFrom(start).trim(); |
331 if( !parser.match(']') ) | |
332 return parser.failure(null); | |
333 String content = parseWellFormed(); | |
334 if( !parser.matchIgnoreCase("[/quote]") ) | |
335 return parser.failure(null); | |
1702 | 336 String rtn = quoter.quote(target,content,name); |
637
6ea90dc10375
bbcode parser now takes a quoter function
Franklin Schmidt <fschmidt@gmail.com>
parents:
588
diff
changeset
|
337 return parser.success(rtn); |
585 | 338 } |
339 | |
1702 | 340 public static String htmlEncode(String s) { |
341 final char[] a = s.toCharArray(); | |
342 StringBuilder buf = new StringBuilder(); | |
343 for( char c : a ) { | |
344 switch(c) { | |
345 case '&': | |
346 buf.append("&"); | |
347 break; | |
348 case '<': | |
349 buf.append("<"); | |
350 break; | |
351 case '>': | |
352 buf.append(">"); | |
353 break; | |
354 case '"': | |
355 buf.append("""); | |
356 break; | |
357 default: | |
358 buf.append(c); | |
359 } | |
360 } | |
361 return buf.toString(); | |
362 } | |
363 | |
585 | 364 } |