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 } |
