001
014
015 package com.liferay.portal.parsers.bbcode;
016
017 import com.liferay.portal.kernel.log.Log;
018 import com.liferay.portal.kernel.log.LogFactoryUtil;
019 import com.liferay.portal.kernel.parsers.bbcode.BBCodeTranslator;
020 import com.liferay.portal.kernel.util.GetterUtil;
021 import com.liferay.portal.kernel.util.HtmlUtil;
022 import com.liferay.portal.kernel.util.IntegerWrapper;
023 import com.liferay.portal.kernel.util.StringBundler;
024 import com.liferay.portal.kernel.util.StringPool;
025 import com.liferay.portal.kernel.util.StringUtil;
026
027 import java.util.Collection;
028 import java.util.HashMap;
029 import java.util.List;
030 import java.util.Map;
031 import java.util.Stack;
032 import java.util.regex.Matcher;
033 import java.util.regex.Pattern;
034
035
038 public class HtmlBBCodeTranslatorImpl implements BBCodeTranslator {
039
040 public HtmlBBCodeTranslatorImpl() {
041 _listStyles = new HashMap<String, String>();
042
043 _listStyles.put("a", "list-style: lower-alpha inside;");
044 _listStyles.put("A", "list-style: upper-alpha inside;");
045 _listStyles.put("1", "list-style: decimal inside;");
046 _listStyles.put("i", "list-style: lower-roman inside;");
047 _listStyles.put("I", "list-style: upper-roman inside;");
048
049 _excludeNewLineTypes = new HashMap<String, Integer>();
050
051 _excludeNewLineTypes.put("*", BBCodeParser.TYPE_TAG_START_END);
052 _excludeNewLineTypes.put("li", BBCodeParser.TYPE_TAG_START_END);
053 _excludeNewLineTypes.put("table", BBCodeParser.TYPE_TAG_END);
054 _excludeNewLineTypes.put("td", BBCodeParser.TYPE_TAG_START_END);
055 _excludeNewLineTypes.put("th", BBCodeParser.TYPE_TAG_START_END);
056 _excludeNewLineTypes.put("tr", BBCodeParser.TYPE_TAG_START_END);
057
058 _bbCodeCharacters = new HashMap<String, String>();
059
060 _bbCodeCharacters.put("&", "&");
061 _bbCodeCharacters.put("<", "<");
062 _bbCodeCharacters.put(">", ">");
063 _bbCodeCharacters.put("\"", """);
064 _bbCodeCharacters.put("'", "'");
065 _bbCodeCharacters.put("/", "/");
066 _bbCodeCharacters.put("`", "`");
067 _bbCodeCharacters.put("[", "[");
068 _bbCodeCharacters.put("]", "]");
069 _bbCodeCharacters.put("(", "(");
070 _bbCodeCharacters.put(")", ")");
071
072 for (int i = 0; i < _EMOTICONS.length; i++) {
073 String[] emoticon = _EMOTICONS[i];
074
075 _emoticonDescriptions[i] = emoticon[2];
076 _emoticonFiles[i] = emoticon[0];
077 _emoticonSymbols[i] = emoticon[1];
078
079 String image = emoticon[0];
080
081 emoticon[0] =
082 "<img alt=\"emoticon\" src=\"@theme_images_path@/emoticons/" +
083 image + "\" >";
084 }
085 }
086
087 public String[] getEmoticonDescriptions() {
088 return _emoticonDescriptions;
089 }
090
091 public String[] getEmoticonFiles() {
092 return _emoticonFiles;
093 }
094
095 public String[][] getEmoticons() {
096 return _EMOTICONS;
097 }
098
099 public String[] getEmoticonSymbols() {
100 return _emoticonSymbols;
101 }
102
103 public String getHTML(String bbcode) {
104 try {
105 bbcode = parse(bbcode);
106 }
107 catch (Exception e) {
108 _log.error("Unable to parse: " + bbcode, e);
109
110 bbcode = HtmlUtil.escape(bbcode);
111 }
112
113 return bbcode;
114 }
115
116 public String parse(String text) {
117 StringBundler sb = new StringBundler();
118
119 List<BBCodeItem> bbCodeItems = _bbCodeParser.parse(text);
120 Stack<String> tags = new Stack<String>();
121 IntegerWrapper marker = new IntegerWrapper();
122
123 for (; marker.getValue() < bbCodeItems.size(); marker.increment()) {
124 BBCodeItem bbCodeItem = bbCodeItems.get(marker.getValue());
125
126 int type = bbCodeItem.getType();
127
128 if (type == BBCodeParser.TYPE_DATA) {
129 handleData(sb, bbCodeItems, tags, marker, bbCodeItem);
130 }
131 else if (type == BBCodeParser.TYPE_TAG_END) {
132 handleTagEnd(sb, tags, bbCodeItem);
133 }
134 else if (type == BBCodeParser.TYPE_TAG_START) {
135 handleTagStart(sb, bbCodeItems, tags, marker, bbCodeItem);
136 }
137 }
138
139 return sb.toString();
140 }
141
142 protected String escapeQuote(String quote) {
143 StringBuilder sb = new StringBuilder();
144
145 int index = 0;
146
147 Matcher matcher = _bbCodePattern.matcher(quote);
148
149 Collection<String> values = _bbCodeCharacters.values();
150
151 while (matcher.find()) {
152 String match = matcher.group();
153
154 int matchStartIndex = matcher.start();
155
156 int nextSemicolonIndex = quote.indexOf(
157 StringPool.SEMICOLON, matchStartIndex);
158
159 sb.append(quote.substring(index, matchStartIndex));
160
161 boolean entityFound = false;
162
163 if (nextSemicolonIndex >= 0) {
164 String value = quote.substring(
165 matchStartIndex, nextSemicolonIndex + 1);
166
167 if (values.contains(value)) {
168 sb.append(value);
169
170 index = matchStartIndex + value.length();
171
172 entityFound = true;
173 }
174 }
175
176 if (!entityFound) {
177 String escapedValue = _bbCodeCharacters.get(match);
178
179 sb.append(escapedValue);
180
181 index = matchStartIndex + match.length();
182 }
183 }
184
185 if (index < quote.length()) {
186 sb.append(quote.substring(index, quote.length()));
187 }
188
189 return sb.toString();
190 }
191
192 protected String extractData(
193 List<BBCodeItem> bbCodeItems, IntegerWrapper marker, String tag,
194 int type, boolean consume) {
195
196 StringBundler sb = new StringBundler();
197
198 int index = marker.getValue() + 1;
199
200 BBCodeItem bbCodeItem = null;
201
202 do {
203 bbCodeItem = bbCodeItems.get(index++);
204
205 if ((bbCodeItem.getType() & type) > 0) {
206 sb.append(bbCodeItem.getValue());
207 }
208
209 }
210 while ((bbCodeItem.getType() != BBCodeParser.TYPE_TAG_END) &&
211 !tag.equals(bbCodeItem.getValue()));
212
213 if (consume) {
214 marker.setValue(index - 1);
215 }
216
217 return sb.toString();
218 }
219
220 protected void handleBold(StringBundler sb, Stack<String> tags) {
221 handleSimpleTag(sb, tags, "strong");
222 }
223
224 protected void handleCode(
225 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
226
227 sb.append("<div class=\"code\">");
228
229 String code = extractData(
230 bbCodeItems, marker, "code", BBCodeParser.TYPE_DATA, true);
231
232 code = HtmlUtil.escape(code);
233 code = code.replaceAll(StringPool.TAB, StringPool.FOUR_SPACES);
234
235 String[] lines = code.split("\r?\n");
236
237 String digits = String.valueOf(lines.length + 1);
238
239 for (int i = 0; i < lines.length; i++) {
240 String index = String.valueOf(i + 1);
241
242 sb.append("<span class=\"code-lines\">");
243
244 for (int j = 0; j < digits.length() - index.length(); j++) {
245 sb.append(StringPool.NBSP);
246 }
247
248 lines[i] = StringUtil.replace(
249 lines[i], StringPool.THREE_SPACES, " ");
250 lines[i] = StringUtil.replace(
251 lines[i], StringPool.DOUBLE_SPACE, " ");
252
253 sb.append(index);
254 sb.append("</span>");
255 sb.append(lines[i]);
256
257 if (index.length() < lines.length) {
258 sb.append("<br />");
259 }
260 }
261
262 sb.append("</div>");
263 }
264
265 protected void handleColor(
266 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
267
268 sb.append("<span style=\"color: ");
269
270 String color = bbCodeItem.getAttribute();
271
272 if (color == null) {
273 color = "inherit";
274 }
275 else {
276 Matcher matcher = _colorPattern.matcher(color);
277
278 if (!matcher.matches()) {
279 color = "inherit";
280 }
281 }
282
283 sb.append(color);
284
285 sb.append("\">");
286
287 tags.push("</span>");
288 }
289
290 protected void handleData(
291 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
292 IntegerWrapper marker, BBCodeItem bbCodeItem) {
293
294 String value = HtmlUtil.escape(bbCodeItem.getValue());
295
296 value = handleNewLine(bbCodeItems, tags, marker, value);
297
298 for (int i = 0; i < _EMOTICONS.length; i++) {
299 String[] emoticon = _EMOTICONS[i];
300
301 value = StringUtil.replace(value, emoticon[1], emoticon[0]);
302 }
303
304 sb.append(value);
305 }
306
307 protected void handleEmail(
308 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
309 IntegerWrapper marker, BBCodeItem bbCodeItem) {
310
311 sb.append("<a href=\"");
312
313 String href = bbCodeItem.getAttribute();
314
315 if (href == null) {
316 href = extractData(
317 bbCodeItems, marker, "email", BBCodeParser.TYPE_DATA, false);
318 }
319
320 if (!href.startsWith("mailto:")) {
321 href = "mailto:" + href;
322 }
323
324 sb.append(HtmlUtil.escapeHREF(href));
325
326 sb.append("\">");
327
328 tags.push("</a>");
329 }
330
331 protected void handleFontFamily(
332 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
333
334 sb.append("<span style=\"font-family: ");
335 sb.append(HtmlUtil.escapeAttribute(bbCodeItem.getAttribute()));
336 sb.append("\">");
337
338 tags.push("</span>");
339 }
340
341 protected void handleFontSize(
342 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
343
344 sb.append("<span style=\"font-size: ");
345
346 int size = GetterUtil.getInteger(bbCodeItem.getAttribute());
347
348 if ((size >= 1) && (size <= _fontSizes.length)) {
349 sb.append(_fontSizes[size - 1]);
350 }
351 else {
352 sb.append(_fontSizes[1]);
353 }
354
355 sb.append("px\">");
356
357 tags.push("</span>");
358 }
359
360 protected void handleImage(
361 StringBundler sb, List<BBCodeItem> bbCodeItems, IntegerWrapper marker) {
362
363 sb.append("<img src=\"");
364
365 String src = extractData(
366 bbCodeItems, marker, "img", BBCodeParser.TYPE_DATA, true);
367
368 Matcher matcher = _imagePattern.matcher(src);
369
370 if (matcher.matches()) {
371 sb.append(HtmlUtil.escapeAttribute(src));
372 }
373
374 sb.append("\" />");
375 }
376
377 protected void handleItalic(StringBundler sb, Stack<String> tags) {
378 handleSimpleTag(sb, tags, "em");
379 }
380
381 protected void handleList(
382 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
383
384 String listStyle = null;
385
386 String tag = null;
387
388 String listAttribute = bbCodeItem.getAttribute();
389
390 if (listAttribute != null) {
391 listStyle = _listStyles.get(listAttribute);
392
393 tag = "ol";
394 }
395 else {
396 tag = "ul style=\"list-style: disc inside;\"";
397 }
398
399 if (listStyle == null) {
400 sb.append("<");
401 sb.append(tag);
402 sb.append(">");
403 }
404 else {
405 sb.append("<");
406 sb.append(tag);
407 sb.append(" style=\"");
408 sb.append(listStyle);
409 sb.append("\">");
410 }
411
412 tags.push("</" + tag + ">");
413 }
414
415 protected void handleListItem(StringBundler sb, Stack<String> tags) {
416 handleSimpleTag(sb, tags, "li");
417 }
418
419 protected String handleNewLine(
420 List<BBCodeItem> bbCodeItems, Stack<String> tags, IntegerWrapper marker,
421 String data) {
422
423 BBCodeItem bbCodeItem = null;
424
425 if (data.matches("\\A\r?\n\\z")) {
426 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
427
428 if (bbCodeItem != null) {
429 String value = bbCodeItem.getValue();
430
431 if (_excludeNewLineTypes.containsKey(value)) {
432 int type = bbCodeItem.getType();
433
434 int excludeNewLineType = _excludeNewLineTypes.get(value);
435
436 if ((type & excludeNewLineType) > 0) {
437 data = StringPool.BLANK;
438 }
439 }
440 }
441 }
442 else if (data.matches("(?s).*\r?\n\\z")) {
443 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
444
445 if ((bbCodeItem != null) &&
446 (bbCodeItem.getType() == BBCodeParser.TYPE_TAG_END)) {
447
448 String value = bbCodeItem.getValue();
449
450 if (value.equals("*")) {
451 data = data.substring(0, data.length() - 1);
452 }
453 }
454 }
455
456 if (data.length() > 0) {
457 data = data.replaceAll("\r?\n", "<br />");
458 }
459
460 return data;
461 }
462
463 protected void handleQuote(
464 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
465
466 String quote = bbCodeItem.getAttribute();
467
468 if ((quote != null) && (quote.length() > 0)) {
469 sb.append("<div class=\"quote-title\">");
470 sb.append(escapeQuote(quote));
471 sb.append(":</div>");
472 }
473
474 sb.append("<div class=\"quote\"><div class=\"quote-content\">");
475
476 tags.push("</div></div>");
477 }
478
479 protected void handleSimpleTag(
480 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
481
482 handleSimpleTag(sb, tags, bbCodeItem.getValue());
483 }
484
485 protected void handleSimpleTag(
486 StringBundler sb, Stack<String> tags, String tag) {
487
488 sb.append("<");
489 sb.append(tag);
490 sb.append(">");
491
492 tags.push("</" + tag + ">");
493 }
494
495 protected void handleStrikeThrough(StringBundler sb, Stack<String> tags) {
496 handleSimpleTag(sb, tags, "strike");
497 }
498
499 protected void handleTable(StringBundler sb, Stack<String> tags) {
500 handleSimpleTag(sb, tags, "table");
501 }
502
503 protected void handleTableCell(StringBundler sb, Stack<String> tags) {
504 handleSimpleTag(sb, tags, "td");
505 }
506
507 protected void handleTableHeader(StringBundler sb, Stack<String> tags) {
508 handleSimpleTag(sb, tags, "th");
509 }
510
511 protected void handleTableRow(StringBundler sb, Stack<String> tags) {
512 handleSimpleTag(sb, tags, "tr");
513 }
514
515 protected void handleTagEnd(
516 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
517
518 String tag = bbCodeItem.getValue();
519
520 if (isValidTag(tag)) {
521 sb.append(tags.pop());
522 }
523 }
524
525 protected void handleTagStart(
526 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
527 IntegerWrapper marker, BBCodeItem bbCodeItem) {
528
529 String tag = bbCodeItem.getValue();
530
531 if (!isValidTag(tag)) {
532 return;
533 }
534
535 if (tag.equals("b")) {
536 handleBold(sb, tags);
537 }
538 else if (tag.equals("center") || tag.equals("justify") ||
539 tag.equals("left") || tag.equals("right")) {
540
541 handleTextAlign(sb, tags, bbCodeItem);
542 }
543 else if (tag.equals("code")) {
544 handleCode(sb, bbCodeItems, marker);
545 }
546 else if (tag.equals("color") || tag.equals("colour")) {
547 handleColor(sb, tags, bbCodeItem);
548 }
549 else if (tag.equals("email")) {
550 handleEmail(sb, bbCodeItems, tags, marker, bbCodeItem);
551 }
552 else if (tag.equals("font")) {
553 handleFontFamily(sb, tags, bbCodeItem);
554 }
555 else if (tag.equals("i")) {
556 handleItalic(sb, tags);
557 }
558 else if (tag.equals("img")) {
559 handleImage(sb, bbCodeItems, marker);
560 }
561 else if (tag.equals("li") || tag.equals("*")) {
562 handleListItem(sb, tags);
563 }
564 else if (tag.equals("list")) {
565 handleList(sb, tags, bbCodeItem);
566 }
567 else if (tag.equals("q") || tag.equals("quote")) {
568 handleQuote(sb, tags, bbCodeItem);
569 }
570 else if (tag.equals("s")) {
571 handleStrikeThrough(sb, tags);
572 }
573 else if (tag.equals("size")) {
574 handleFontSize(sb, tags, bbCodeItem);
575 }
576 else if (tag.equals("table")) {
577 handleTable(sb, tags);
578 }
579 else if (tag.equals("td")) {
580 handleTableCell(sb, tags);
581 }
582 else if (tag.equals("th")) {
583 handleTableHeader(sb, tags);
584 }
585 else if (tag.equals("tr")) {
586 handleTableRow(sb, tags);
587 }
588 else if (tag.equals("url")) {
589 handleURL(sb, bbCodeItems, tags, marker, bbCodeItem);
590 }
591 else {
592 handleSimpleTag(sb, tags, bbCodeItem);
593 }
594 }
595
596 protected void handleTextAlign(
597 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
598
599 sb.append("<p style=\"text-align: ");
600 sb.append(bbCodeItem.getValue());
601 sb.append("\">");
602
603 tags.push("</p>");
604 }
605
606 protected void handleURL(
607 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
608 IntegerWrapper marker, BBCodeItem bbCodeItem) {
609
610 sb.append("<a href=\"");
611
612 String href = bbCodeItem.getAttribute();
613
614 if (href == null) {
615 href = extractData(
616 bbCodeItems, marker, "url", BBCodeParser.TYPE_DATA, false);
617 }
618
619 Matcher matcher = _urlPattern.matcher(href);
620
621 if (matcher.matches()) {
622 sb.append(HtmlUtil.escapeHREF(href));
623 }
624
625 sb.append("\">");
626
627 tags.push("</a>");
628 }
629
630 protected boolean isValidTag(String tag) {
631 if ((tag != null) && (tag.length() > 0)) {
632 Matcher matcher = _tagPattern.matcher(tag);
633
634 return matcher.matches();
635 }
636
637 return false;
638 }
639
640 private static final String[][] _EMOTICONS = {
641 {"happy.gif", ":)", "happy"},
642 {"smile.gif", ":D", "smile"},
643 {"cool.gif", "B)", "cool"},
644 {"sad.gif", ":(", "sad"},
645 {"tongue.gif", ":P", "tongue"},
646 {"laugh.gif", ":lol:", "laugh"},
647 {"kiss.gif", ":#", "kiss"},
648 {"blush.gif", ":*)", "blush"},
649 {"bashful.gif", ":bashful:", "bashful"},
650 {"smug.gif", ":smug:", "smug"},
651 {"blink.gif", ":blink:", "blink"},
652 {"huh.gif", ":huh:", "huh"},
653 {"mellow.gif", ":mellow:", "mellow"},
654 {"unsure.gif", ":unsure:", "unsure"},
655 {"mad.gif", ":mad:", "mad"},
656 {"oh_my.gif", ":O", "oh-my-goodness"},
657 {"roll_eyes.gif", ":rolleyes:", "roll-eyes"},
658 {"angry.gif", ":angry:", "angry"},
659 {"suspicious.gif", "8o", "suspicious"},
660 {"big_grin.gif", ":grin:", "grin"},
661 {"in_love.gif", ":love:", "in-love"},
662 {"bored.gif", ":bored:", "bored"},
663 {"closed_eyes.gif", "-_-", "closed-eyes"},
664 {"cold.gif", ":cold:", "cold"},
665 {"sleep.gif", ":sleep:", "sleep"},
666 {"glare.gif", ":glare:", "glare"},
667 {"darth_vader.gif", ":vader:", "darth-vader"},
668 {"dry.gif", ":dry:", "dry"},
669 {"exclamation.gif", ":what:", "what"},
670 {"girl.gif", ":girl:", "girl"},
671 {"karate_kid.gif", ":kid:", "karate-kid"},
672 {"ninja.gif", ":ph34r:", "ninja"},
673 {"pac_man.gif", ":V", "pac-man"},
674 {"wacko.gif", ":wacko:", "wacko"},
675 {"wink.gif", ":wink:", "wink"},
676 {"wub.gif", ":wub:", "wub"}
677 };
678
679 private static Log _log = LogFactoryUtil.getLog(
680 HtmlBBCodeTranslatorImpl.class);
681
682 private Map<String, String> _bbCodeCharacters;
683 private BBCodeParser _bbCodeParser = new BBCodeParser();
684 private Pattern _bbCodePattern = Pattern.compile("[]&<>'\"`\\[()]");
685 private Pattern _colorPattern = Pattern.compile(
686 "^(:?aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple" +
687 "|red|silver|teal|white|yellow|#(?:[0-9a-f]{3})?[0-9a-f]{3})$",
688 Pattern.CASE_INSENSITIVE);
689 private String[] _emoticonDescriptions = new String[_EMOTICONS.length];
690 private String[] _emoticonFiles = new String[_EMOTICONS.length];
691 private String[] _emoticonSymbols = new String[_EMOTICONS.length];
692 private Map<String, Integer> _excludeNewLineTypes;
693 private int[] _fontSizes = {10, 12, 16, 18, 24, 32, 48};
694 private Pattern _imagePattern = Pattern.compile(
695 "^(?:https?:
696 Pattern.CASE_INSENSITIVE);
697 private Map<String, String> _listStyles;
698 private Pattern _tagPattern = Pattern.compile(
699 "^/?(?:b|center|code|colou?r|email|i|img|justify|left|pre|q|quote|" +
700 "right|\\*|s|size|table|tr|th|td|li|list|font|u|url)$",
701 Pattern.CASE_INSENSITIVE);
702 private Pattern _urlPattern = Pattern.compile(
703 "^[-;/?:@&=+$,_.!~*'()%0-9a-z#]{1,512}$", Pattern.CASE_INSENSITIVE);
704
705 }