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