001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.common.string;
018
019import com.google.common.base.Splitter;
020import com.google.common.html.HtmlEscapers;
021import java.util.ArrayDeque;
022import java.util.Deque;
023import java.util.HashMap;
024import java.util.Map;
025
026public class MarkupUtils {
027    
028    private MarkupUtils() {
029        super();
030    }
031    
032    private static class MarkupUtilsHolder {
033        static MarkupUtils instance = new MarkupUtils();
034    }
035    
036    public static MarkupUtils getInstance() {
037        return MarkupUtilsHolder.instance;
038    }
039    
040    private static class MarkupTransformation {
041        String markup;
042        String startHtml;
043        String endHtml;
044        String requiredIn;
045        
046        MarkupTransformation(Map<String, MarkupTransformation> transformations, String markup, String startHtml, String endHtml, String requiredIn) {
047            this.markup = markup;
048            this.startHtml = startHtml;
049            this.endHtml = endHtml;
050            this.requiredIn = requiredIn;
051            
052            transformations.put(markup, this);
053        }
054    };
055    
056    private final static Map<String, MarkupTransformation> transformations;
057    
058    static {
059        transformations = new HashMap<>();
060        
061        // TODO: Fill in additional tags from http://learningforlife.fsu.edu/webmaster/references/xhtml/tags/index.cfm
062        new MarkupTransformation(transformations, "b", "<b>", "</b>", null);
063        new MarkupTransformation(transformations, "i", "<i>", "</i>", null);
064        new MarkupTransformation(transformations, "u", "<u>", "</u>", null);
065        new MarkupTransformation(transformations, "s", "<s>", "</s>", null);
066        new MarkupTransformation(transformations, "ul", "<ul>", "</ul>", null);
067        new MarkupTransformation(transformations, "ol", "<ol>", "</ol>", null);
068        new MarkupTransformation(transformations, "li", "<li>", "</li>", "ul:ol");
069        new MarkupTransformation(transformations, "blockquote", "<blockquote>", "</blockquote>", null);
070        new MarkupTransformation(transformations, "pre", "<pre>", "</pre>", null);
071        new MarkupTransformation(transformations, "quote", "<hr /><blockquote>", "</blockquote><hr />", null);
072        new MarkupTransformation(transformations, "br", "<br />", null, null);
073        new MarkupTransformation(transformations, "red", "<span style=\"color: red\">", "</span>", null);
074        new MarkupTransformation(transformations, "green", "<span style=\"color: green\">", "</span>", null);
075        new MarkupTransformation(transformations, "blue", "<span style=\"color: blue\">", "</span>", null);
076        new MarkupTransformation(transformations, "orange", "<span style=\"color: orange\">", "</span>", null);
077        new MarkupTransformation(transformations, "black", "<span style=\"color: black\">", "</span>", null);
078        new MarkupTransformation(transformations, "white", "<span style=\"color: white\">", "</span>", null);
079        new MarkupTransformation(transformations, "yellow", "<span style=\"color: yellow\">", "</span>", null);
080        new MarkupTransformation(transformations, "purple", "<span style=\"color: purple\">", "</span>", null);
081        new MarkupTransformation(transformations, "table", "<table>", "</table>", null);
082        new MarkupTransformation(transformations, "tr", "<tr>", "</tr>", "table");
083        new MarkupTransformation(transformations, "td", "<td>", "</td>", "tr");
084    }
085    
086    public String filter(final String originalContent) {
087        String intermediateContent = HtmlEscapers.htmlEscaper().escape(originalContent);
088        StringBuilder finalContent = new StringBuilder(intermediateContent.length());
089        StringBuilder markupBuilder = null;
090        boolean endMarkup = false;
091        Deque<MarkupTransformation> activeMarkup = new ArrayDeque<>();
092        
093        for(int ch : StringUtils.getInstance().codePoints(intermediateContent)) {
094            switch(ch) {
095                case '[':
096                    markupBuilder = new StringBuilder();
097                    endMarkup = false;
098                    break;
099                case ']':
100                    MarkupTransformation mt = transformations.get(markupBuilder.toString());
101                    
102                    if(mt != null) {
103                        if(endMarkup) {
104                            if(mt == activeMarkup.peek()) {
105                                activeMarkup.pop();
106                                finalContent.append(mt.endHtml);
107                            }
108                        } else {
109                            boolean pushIt = true;
110                            
111                            if(mt.requiredIn != null) {
112                                MarkupTransformation lastMt = activeMarkup.peek();
113                                
114                                pushIt = false;
115                                
116                                if(lastMt != null) {
117                                    String []endingMarkup = Splitter.on(':').trimResults().omitEmptyStrings().splitToList(mt.requiredIn).toArray(new String[0]);
118                                    
119                                    for(int i = 0; i < endingMarkup.length; i++) {
120                                        if(endingMarkup[i].equals(lastMt.markup)) {
121                                            pushIt = true;
122                                            break;
123                                        }
124                                    }
125                                }
126                            }
127                            
128                            if(pushIt) {
129                                if(mt.endHtml != null) {
130                                    activeMarkup.push(mt);
131                                }
132                                
133                                finalContent.append(mt.startHtml);
134                            }
135                        }
136                    }
137                    
138                    markupBuilder = null;
139                    break;
140                default:
141                    if(markupBuilder == null) {
142                        switch(ch) {
143                            case '\t':
144                                finalContent.append("&nbsp;&nbsp;&nbsp;&nbsp;");
145                                break;
146                            case '\r':
147                                // Drop it.
148                                break;
149                            case '\n':
150                                finalContent.append("<br />");
151                                break;
152                            default:
153                                finalContent.appendCodePoint(ch);
154                                break;
155                        }
156                    } else {
157                        if(ch == '/' && markupBuilder.length() == 0) {
158                            endMarkup = true;
159                        } else if(Character.isLetter(ch)) {
160                            markupBuilder.appendCodePoint(Character.toLowerCase(ch));
161                            break;
162                        }
163                    }
164                    break;
165            }
166        }
167        
168        while(activeMarkup.peek() != null) {
169            finalContent.append(activeMarkup.pop().endHtml);
170        }
171        
172        return finalContent.toString();
173    }
174    
175}