001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it 
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011package com.ardor3d.extension.ui.text.parser;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.Comparator;
016import java.util.Iterator;
017import java.util.LinkedList;
018import java.util.List;
019import java.util.StringTokenizer;
020
021import com.ardor3d.extension.ui.text.StyleConstants;
022import com.ardor3d.extension.ui.text.StyleSpan;
023import com.ardor3d.math.ColorRGBA;
024import com.ardor3d.math.type.ReadOnlyColorRGBA;
025
026public class ForumLikeMarkupParser implements StyleParser {
027
028    Comparator<StyleSpan> endSorter = new Comparator<StyleSpan>() {
029        @Override
030        public int compare(final StyleSpan o1, final StyleSpan o2) {
031            return o1.getSpanStart() + o1.getSpanLength() - (o2.getSpanStart() + o2.getSpanLength());
032        }
033    };
034
035    @Override
036    public String parseStyleSpans(final String text, final List<StyleSpan> store) {
037        final StringBuilder rVal = new StringBuilder("");
038        int index = 0;
039        TagStatus tagStatus = TagStatus.NONE;
040        String currTagText = "";
041        final LinkedList<StyleSpan> buildingSpans = new LinkedList<>();
042        final StringTokenizer st = new StringTokenizer(text, "[]\\", true);
043        String token;
044        while (st.hasMoreTokens()) {
045            token = st.nextToken();
046            // escape char
047            if (tagStatus == TagStatus.NONE && "\\".equals(token)) {
048                if (st.hasMoreTokens()) {
049                    token = st.nextToken();
050                    if ("[".equals(token) || "]".equals(token)) {
051                        rVal.append(token);
052                        index++;
053                        continue;
054                    } else {
055                        rVal.append('\\');
056                        rVal.append(token);
057                        index += token.length() + 1;
058                        continue;
059                    }
060                } else {
061                    rVal.append('\\');
062                    index++;
063                    continue;
064                }
065            }
066
067            // start token
068            else if (tagStatus == TagStatus.NONE && "[".equals(token)) {
069                tagStatus = TagStatus.START_TAG;
070                continue;
071            }
072
073            else if (tagStatus == TagStatus.START_TAG) {
074                currTagText = token;
075                tagStatus = TagStatus.IN_TAG;
076                continue;
077            }
078
079            // end token
080            else if (tagStatus == TagStatus.IN_TAG && "]".equals(token)) {
081                tagStatus = TagStatus.NONE;
082                // interpret tag:
083                // BOLD
084                if ("b".equalsIgnoreCase(currTagText)) {
085                    // start a new bold span
086                    buildingSpans.add(new StyleSpan(StyleConstants.KEY_BOLD, Boolean.TRUE, index, 0));
087                } else if ("/b".equalsIgnoreCase(currTagText)) {
088                    // find last BOLD entry and add length
089                    endSpan(StyleConstants.KEY_BOLD, store, index, buildingSpans);
090                }
091
092                // ITALICS
093                else if ("i".equalsIgnoreCase(currTagText)) {
094                    // start a new italics span
095                    buildingSpans.add(new StyleSpan(StyleConstants.KEY_ITALICS, Boolean.TRUE, index, 0));
096                } else if ("/i".equalsIgnoreCase(currTagText)) {
097                    // find last ITALICS entry and add length
098                    endSpan(StyleConstants.KEY_ITALICS, store, index, buildingSpans);
099                }
100
101                // COLOR
102                else if (currTagText.toLowerCase().startsWith("c=")) {
103                    // start a new color span
104                    try {
105                        // parse a color
106                        final String c = currTagText.substring(2);
107                        buildingSpans.add(new StyleSpan(StyleConstants.KEY_COLOR, ColorRGBA.parseColor(c, null), index,
108                                0));
109                    } catch (final Exception e) {
110                        e.printStackTrace();
111                    }
112                } else if ("/c".equalsIgnoreCase(currTagText)) {
113                    // find last BOLD entry and add length
114                    endSpan(StyleConstants.KEY_COLOR, store, index, buildingSpans);
115                }
116
117                // SIZE
118                else if (currTagText.toLowerCase().startsWith("size=")) {
119                    // start a new size span
120                    try {
121                        // parse a size
122                        final int i = Integer.parseInt(currTagText.substring(5));
123                        buildingSpans.add(new StyleSpan(StyleConstants.KEY_SIZE, i, index, 0));
124                    } catch (final Exception e) {
125                        e.printStackTrace();
126                    }
127                } else if ("/size".equalsIgnoreCase(currTagText)) {
128                    // find last SIZE entry and add length
129                    endSpan(StyleConstants.KEY_SIZE, store, index, buildingSpans);
130                }
131
132                // FAMILY
133                else if (currTagText.toLowerCase().startsWith("f=")) {
134                    // start a new family span
135                    final String family = currTagText.substring(2);
136                    buildingSpans.add(new StyleSpan(StyleConstants.KEY_FAMILY, family, index, 0));
137                } else if ("/f".equalsIgnoreCase(currTagText)) {
138                    // find last FAMILY entry and add length
139                    endSpan(StyleConstants.KEY_FAMILY, store, index, buildingSpans);
140                } else {
141                    // not really a tag, so put it back.
142                    rVal.append('[');
143                    rVal.append(currTagText);
144                    rVal.append(']');
145                    tagStatus = TagStatus.NONE;
146                }
147
148                currTagText = "";
149                continue;
150            }
151
152            // anything else
153            rVal.append(token);
154            index += token.length();
155        }
156
157        // close any remaining open tags
158        while (!buildingSpans.isEmpty()) {
159            final StyleSpan span = buildingSpans.getLast();
160            endSpan(span.getStyle(), store, index, buildingSpans);
161        }
162
163        // return plain text
164        return rVal.toString();
165    }
166
167    private void endSpan(final String key, final List<StyleSpan> store, final int index,
168            final LinkedList<StyleSpan> buildingSpans) {
169        for (final Iterator<StyleSpan> it = buildingSpans.descendingIterator(); it.hasNext();) {
170            final StyleSpan next = it.next();
171            if (key.equals(next.getStyle())) {
172                next.setSpanLength(index - next.getSpanStart());
173                store.add(next);
174                it.remove();
175                break;
176            }
177        }
178    }
179
180    @Override
181    public String addStyleMarkup(final String plainText, final List<StyleSpan> spans) {
182        if (spans.isEmpty()) {
183            return plainText;
184        }
185
186        // list of spans, sorted by start index
187        final List<StyleSpan> starts = new ArrayList<>();
188        starts.addAll(spans);
189        Collections.sort(starts);
190
191        // list of spans, to be sorted by end index
192        final List<StyleSpan> ends = new LinkedList<>();
193
194        final StringBuilder builder = new StringBuilder();
195
196        // go through all chars and add starts and ends
197        for (int index = 0, max = plainText.length(); index < max; index++) {
198            // close markup
199            while (!ends.isEmpty()) {
200                final StyleSpan span = ends.get(0);
201                if (span.getSpanStart() + span.getSpanLength() == index) {
202                    builder.append(getMarkup(span, true));
203                    ends.remove(0);
204                } else {
205                    break;
206                }
207            }
208
209            // add starts
210            while (!starts.isEmpty()) {
211                final StyleSpan span = starts.get(0);
212                if (span.getSpanStart() == index) {
213                    builder.append(getMarkup(span, false));
214                    ends.add(span);
215                    starts.remove(0);
216                    Collections.sort(ends, endSorter);
217                } else {
218                    break;
219                }
220            }
221
222            builder.append(plainText.charAt(index));
223        }
224
225        // close any remaining markup:
226        while (!ends.isEmpty()) {
227            final StyleSpan span = ends.get(0);
228            builder.append(getMarkup(span, true));
229            ends.remove(0);
230        }
231
232        return builder.toString();
233    }
234
235    private String getMarkup(final StyleSpan span, final boolean end) {
236        if (StyleConstants.KEY_BOLD.equalsIgnoreCase(span.getStyle())) {
237            return end ? "[/b]" : "[b]";
238        } else if (StyleConstants.KEY_ITALICS.equalsIgnoreCase(span.getStyle())) {
239            return end ? "[/i]" : "[i]";
240        } else if (StyleConstants.KEY_FAMILY.equalsIgnoreCase(span.getStyle())) {
241            return end ? "[/f]" : "[f=" + span.getValue() + "]";
242        } else if (StyleConstants.KEY_SIZE.equalsIgnoreCase(span.getStyle())) {
243            return end ? "[/size]" : "[size=" + span.getValue() + "]";
244        } else if (StyleConstants.KEY_COLOR.equalsIgnoreCase(span.getStyle()) && span.getValue() instanceof ColorRGBA) {
245            return end ? "[/c]" : "[c=" + ((ReadOnlyColorRGBA) span.getValue()).asHexRRGGBBAA() + "]";
246        }
247
248        return "";
249    }
250
251    enum TagStatus {
252        NONE, START_TAG, IN_TAG
253    }
254}