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 outside;");
044 _listStyles.put("A", "list-style: upper-alpha outside;");
045 _listStyles.put("1", "list-style: decimal outside;");
046 _listStyles.put("i", "list-style: lower-roman outside;");
047 _listStyles.put("I", "list-style: upper-roman outside;");
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 outside;\"";
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 ((marker.getValue() + 1) < bbCodeItems.size()) {
426 if (data.matches("\\A\r?\n\\z")) {
427 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
428
429 if (bbCodeItem != null) {
430 String value = bbCodeItem.getValue();
431
432 if (_excludeNewLineTypes.containsKey(value)) {
433 int type = bbCodeItem.getType();
434
435 int excludeNewLineType = _excludeNewLineTypes.get(
436 value);
437
438 if ((type & excludeNewLineType) > 0) {
439 data = StringPool.BLANK;
440 }
441 }
442 }
443 }
444 else if (data.matches("(?s).*\r?\n\\z")) {
445 bbCodeItem = bbCodeItems.get(marker.getValue() + 1);
446
447 if ((bbCodeItem != null) &&
448 (bbCodeItem.getType() == BBCodeParser.TYPE_TAG_END)) {
449
450 String value = bbCodeItem.getValue();
451
452 if (value.equals("*")) {
453 data = data.substring(0, data.length() - 1);
454 }
455 }
456 }
457 }
458
459 if (data.length() > 0) {
460 data = data.replaceAll("\r?\n", "<br />");
461 }
462
463 return data;
464 }
465
466 protected void handleQuote(
467 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
468
469 String quote = bbCodeItem.getAttribute();
470
471 if ((quote != null) && (quote.length() > 0)) {
472 sb.append("<div class=\"quote-title\">");
473 sb.append(escapeQuote(quote));
474 sb.append(":</div>");
475 }
476
477 sb.append("<div class=\"quote\"><div class=\"quote-content\">");
478
479 tags.push("</div></div>");
480 }
481
482 protected void handleSimpleTag(
483 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
484
485 handleSimpleTag(sb, tags, bbCodeItem.getValue());
486 }
487
488 protected void handleSimpleTag(
489 StringBundler sb, Stack<String> tags, String tag) {
490
491 sb.append("<");
492 sb.append(tag);
493 sb.append(">");
494
495 tags.push("</" + tag + ">");
496 }
497
498 protected void handleStrikeThrough(StringBundler sb, Stack<String> tags) {
499 handleSimpleTag(sb, tags, "strike");
500 }
501
502 protected void handleTable(StringBundler sb, Stack<String> tags) {
503 handleSimpleTag(sb, tags, "table");
504 }
505
506 protected void handleTableCell(StringBundler sb, Stack<String> tags) {
507 handleSimpleTag(sb, tags, "td");
508 }
509
510 protected void handleTableHeader(StringBundler sb, Stack<String> tags) {
511 handleSimpleTag(sb, tags, "th");
512 }
513
514 protected void handleTableRow(StringBundler sb, Stack<String> tags) {
515 handleSimpleTag(sb, tags, "tr");
516 }
517
518 protected void handleTagEnd(
519 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
520
521 String tag = bbCodeItem.getValue();
522
523 if (isValidTag(tag)) {
524 sb.append(tags.pop());
525 }
526 }
527
528 protected void handleTagStart(
529 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
530 IntegerWrapper marker, BBCodeItem bbCodeItem) {
531
532 String tag = bbCodeItem.getValue();
533
534 if (!isValidTag(tag)) {
535 return;
536 }
537
538 if (tag.equals("b")) {
539 handleBold(sb, tags);
540 }
541 else if (tag.equals("center") || tag.equals("justify") ||
542 tag.equals("left") || tag.equals("right")) {
543
544 handleTextAlign(sb, tags, bbCodeItem);
545 }
546 else if (tag.equals("code")) {
547 handleCode(sb, bbCodeItems, marker);
548 }
549 else if (tag.equals("color") || tag.equals("colour")) {
550 handleColor(sb, tags, bbCodeItem);
551 }
552 else if (tag.equals("email")) {
553 handleEmail(sb, bbCodeItems, tags, marker, bbCodeItem);
554 }
555 else if (tag.equals("font")) {
556 handleFontFamily(sb, tags, bbCodeItem);
557 }
558 else if (tag.equals("i")) {
559 handleItalic(sb, tags);
560 }
561 else if (tag.equals("img")) {
562 handleImage(sb, bbCodeItems, marker);
563 }
564 else if (tag.equals("li") || tag.equals("*")) {
565 handleListItem(sb, tags);
566 }
567 else if (tag.equals("list")) {
568 handleList(sb, tags, bbCodeItem);
569 }
570 else if (tag.equals("q") || tag.equals("quote")) {
571 handleQuote(sb, tags, bbCodeItem);
572 }
573 else if (tag.equals("s")) {
574 handleStrikeThrough(sb, tags);
575 }
576 else if (tag.equals("size")) {
577 handleFontSize(sb, tags, bbCodeItem);
578 }
579 else if (tag.equals("table")) {
580 handleTable(sb, tags);
581 }
582 else if (tag.equals("td")) {
583 handleTableCell(sb, tags);
584 }
585 else if (tag.equals("th")) {
586 handleTableHeader(sb, tags);
587 }
588 else if (tag.equals("tr")) {
589 handleTableRow(sb, tags);
590 }
591 else if (tag.equals("url")) {
592 handleURL(sb, bbCodeItems, tags, marker, bbCodeItem);
593 }
594 else {
595 handleSimpleTag(sb, tags, bbCodeItem);
596 }
597 }
598
599 protected void handleTextAlign(
600 StringBundler sb, Stack<String> tags, BBCodeItem bbCodeItem) {
601
602 sb.append("<p style=\"text-align: ");
603 sb.append(bbCodeItem.getValue());
604 sb.append("\">");
605
606 tags.push("</p>");
607 }
608
609 protected void handleURL(
610 StringBundler sb, List<BBCodeItem> bbCodeItems, Stack<String> tags,
611 IntegerWrapper marker, BBCodeItem bbCodeItem) {
612
613 sb.append("<a href=\"");
614
615 String href = bbCodeItem.getAttribute();
616
617 if (href == null) {
618 href = extractData(
619 bbCodeItems, marker, "url", BBCodeParser.TYPE_DATA, false);
620 }
621
622 Matcher matcher = _urlPattern.matcher(href);
623
624 if (matcher.matches()) {
625 sb.append(HtmlUtil.escapeHREF(href));
626 }
627
628 sb.append("\">");
629
630 tags.push("</a>");
631 }
632
633 protected boolean isValidTag(String tag) {
634 if ((tag != null) && (tag.length() > 0)) {
635 Matcher matcher = _tagPattern.matcher(tag);
636
637 return matcher.matches();
638 }
639
640 return false;
641 }
642
643 private static final String[][] _EMOTICONS = {
644 {"happy.gif", ":)", "happy"},
645 {"smile.gif", ":D", "smile"},
646 {"cool.gif", "B)", "cool"},
647 {"sad.gif", ":(", "sad"},
648 {"tongue.gif", ":P", "tongue"},
649 {"laugh.gif", ":lol:", "laugh"},
650 {"kiss.gif", ":#", "kiss"},
651 {"blush.gif", ":*)", "blush"},
652 {"bashful.gif", ":bashful:", "bashful"},
653 {"smug.gif", ":smug:", "smug"},
654 {"blink.gif", ":blink:", "blink"},
655 {"huh.gif", ":huh:", "huh"},
656 {"mellow.gif", ":mellow:", "mellow"},
657 {"unsure.gif", ":unsure:", "unsure"},
658 {"mad.gif", ":mad:", "mad"},
659 {"oh_my.gif", ":O", "oh-my-goodness"},
660 {"roll_eyes.gif", ":rolleyes:", "roll-eyes"},
661 {"angry.gif", ":angry:", "angry"},
662 {"suspicious.gif", "8o", "suspicious"},
663 {"big_grin.gif", ":grin:", "grin"},
664 {"in_love.gif", ":love:", "in-love"},
665 {"bored.gif", ":bored:", "bored"},
666 {"closed_eyes.gif", "-_-", "closed-eyes"},
667 {"cold.gif", ":cold:", "cold"},
668 {"sleep.gif", ":sleep:", "sleep"},
669 {"glare.gif", ":glare:", "glare"},
670 {"darth_vader.gif", ":vader:", "darth-vader"},
671 {"dry.gif", ":dry:", "dry"},
672 {"exclamation.gif", ":what:", "what"},
673 {"girl.gif", ":girl:", "girl"},
674 {"karate_kid.gif", ":kid:", "karate-kid"},
675 {"ninja.gif", ":ph34r:", "ninja"},
676 {"pac_man.gif", ":V", "pac-man"},
677 {"wacko.gif", ":wacko:", "wacko"},
678 {"wink.gif", ":wink:", "wink"},
679 {"wub.gif", ":wub:", "wub"}
680 };
681
682 private static Log _log = LogFactoryUtil.getLog(
683 HtmlBBCodeTranslatorImpl.class);
684
685 private Map<String, String> _bbCodeCharacters;
686 private BBCodeParser _bbCodeParser = new BBCodeParser();
687 private Pattern _bbCodePattern = Pattern.compile("[]&<>'\"`\\[()]");
688 private Pattern _colorPattern = Pattern.compile(
689 "^(:?aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple" +
690 "|red|silver|teal|white|yellow|#(?:[0-9a-f]{3})?[0-9a-f]{3})$",
691 Pattern.CASE_INSENSITIVE);
692 private String[] _emoticonDescriptions = new String[_EMOTICONS.length];
693 private String[] _emoticonFiles = new String[_EMOTICONS.length];
694 private String[] _emoticonSymbols = new String[_EMOTICONS.length];
695 private Map<String, Integer> _excludeNewLineTypes;
696 private int[] _fontSizes = {10, 12, 16, 18, 24, 32, 48};
697 private Pattern _imagePattern = Pattern.compile(
698 "^(?:https?:
699 Pattern.CASE_INSENSITIVE);
700 private Map<String, String> _listStyles;
701 private Pattern _tagPattern = Pattern.compile(
702 "^/?(?:b|center|code|colou?r|email|i|img|justify|left|pre|q|quote|" +
703 "right|\\*|s|size|table|tr|th|td|li|list|font|u|url)$",
704 Pattern.CASE_INSENSITIVE);
705 private Pattern _urlPattern = Pattern.compile(
706 "^[-;/?:@&=+$,_.!~*'()%0-9a-z#]{1,512}$", Pattern.CASE_INSENSITIVE);
707
708 }