001    /**
002     * Copyright (c) 2000-2012 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
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    /**
035     * @author Iliyan Peychev
036     */
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, "&nbsp; &nbsp;");
185                            lines[i] = StringUtil.replace(
186                                    lines[i], StringPool.DOUBLE_SPACE, "&nbsp; ");
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?://|/)[-;/?:@&=+$,_.!~*'()%0-9a-z]{1,512}$",
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    }