001/*
002 * Copyright (C) 2009-2017 the original author(s).
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 */
016package org.fusesource.jansi;
017
018import java.io.IOException;
019import java.util.Locale;
020
021import org.fusesource.jansi.Ansi.Attribute;
022import org.fusesource.jansi.Ansi.Color;
023
024/**
025 * Renders ANSI color escape-codes in strings by parsing out some special syntax to pick up the correct fluff to use.
026 *
027 * The syntax for embedded ANSI codes is:
028 *
029 * <pre>
030 *   <tt>@|</tt><em>code</em>(<tt>,</tt><em>code</em>)* <em>text</em><tt>|@</tt>
031 * </pre>
032 *
033 * Examples:
034 *
035 * <pre>
036 *   <tt>@|bold Hello|@</tt>
037 * </pre>
038 *
039 * <pre>
040 *   <tt>@|bold,red Warning!|@</tt>
041 * </pre>
042 *
043 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
044 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
045 * @since 1.1
046 */
047public class AnsiRenderer {
048
049    public static final String BEGIN_TOKEN = "@|";
050
051    public static final String END_TOKEN = "|@";
052
053    public static final String CODE_TEXT_SEPARATOR = " ";
054
055    public static final String CODE_LIST_SEPARATOR = ",";
056
057    private static final int BEGIN_TOKEN_LEN = 2;
058
059    private static final int END_TOKEN_LEN = 2;
060
061    public static String render(final String input) throws IllegalArgumentException {
062        try {
063            return render(input, new StringBuilder()).toString();
064        } catch (IOException e) {
065            // Cannot happen because StringBuilder does not throw IOException
066            throw new IllegalArgumentException(e);
067        }
068    }
069
070    /**
071     * Renders the given input to the target Appendable.
072     * 
073     * @param input
074     *            source to render
075     * @param target
076     *            render onto this target Appendable.
077     * @return the given Appendable
078     * @throws IOException
079     *             If an I/O error occurs
080     */
081    public static Appendable render(final String input, Appendable target) throws IOException {
082
083        int i = 0;
084        int j, k;
085
086        while (true) {
087            j = input.indexOf(BEGIN_TOKEN, i);
088            if (j == -1) {
089                if (i == 0) {
090                    target.append(input);
091                    return target;
092                }
093                target.append(input.substring(i, input.length()));
094                return target;
095            }
096            target.append(input.substring(i, j));
097            k = input.indexOf(END_TOKEN, j);
098
099            if (k == -1) {
100                target.append(input);
101                return target;
102            }
103            j += BEGIN_TOKEN_LEN;
104            String spec = input.substring(j, k);
105
106            String[] items = spec.split(CODE_TEXT_SEPARATOR, 2);
107            if (items.length == 1) {
108                target.append(input);
109                return target;
110            }
111            String replacement = render(items[1], items[0].split(CODE_LIST_SEPARATOR));
112
113            target.append(replacement);
114
115            i = k + END_TOKEN_LEN;
116        }
117    }
118
119    public static String render(final String text, final String... codes) {
120        return render(Ansi.ansi(), codes)
121                .a(text).reset().toString();
122    }
123
124    /**
125     * Renders {@link Code} names as an ANSI escape string.
126     * @param codes The code names to render
127     * @return an ANSI escape string.
128     */
129    public static String renderCodes(final String... codes) {
130        return render(Ansi.ansi(), codes).toString();
131    }
132
133    /**
134     * Renders {@link Code} names as an ANSI escape string.
135     * @param codes A space separated list of code names to render
136     * @return an ANSI escape string.
137     */
138    public static String renderCodes(final String codes) {
139        return renderCodes(codes.split("\\s"));
140    }
141
142    private static Ansi render(Ansi ansi, String... names) {
143        for (String name : names) {
144            render(ansi, name);
145        }
146        return ansi;
147    }
148
149    private static Ansi render(Ansi ansi, String name) {
150        Code code = Code.valueOf(name.toUpperCase(Locale.ENGLISH));
151        if (code.isColor()) {
152            if (code.isBackground()) {
153                ansi.bg(code.getColor());
154            } else {
155                ansi.fg(code.getColor());
156            }
157        } else if (code.isAttribute()) {
158            ansi.a(code.getAttribute());
159        }
160        return ansi;
161    }
162
163    public static boolean test(final String text) {
164        return text != null && text.contains(BEGIN_TOKEN);
165    }
166
167    public enum Code {
168        //
169        // TODO: Find a better way to keep Code in sync with Color/Attribute/Erase
170        //
171
172        // Colors
173        BLACK(Color.BLACK),
174        RED(Color.RED),
175        GREEN(Color.GREEN),
176        YELLOW(Color.YELLOW),
177        BLUE(Color.BLUE),
178        MAGENTA(Color.MAGENTA),
179        CYAN(Color.CYAN),
180        WHITE(Color.WHITE),
181
182        // Foreground Colors
183        FG_BLACK(Color.BLACK, false),
184        FG_RED(Color.RED, false),
185        FG_GREEN(Color.GREEN, false),
186        FG_YELLOW(Color.YELLOW, false),
187        FG_BLUE(Color.BLUE, false),
188        FG_MAGENTA(Color.MAGENTA, false),
189        FG_CYAN(Color.CYAN, false),
190        FG_WHITE(Color.WHITE, false),
191
192        // Background Colors
193        BG_BLACK(Color.BLACK, true),
194        BG_RED(Color.RED, true),
195        BG_GREEN(Color.GREEN, true),
196        BG_YELLOW(Color.YELLOW, true),
197        BG_BLUE(Color.BLUE, true),
198        BG_MAGENTA(Color.MAGENTA, true),
199        BG_CYAN(Color.CYAN, true),
200        BG_WHITE(Color.WHITE, true),
201
202        // Attributes
203        RESET(Attribute.RESET),
204        INTENSITY_BOLD(Attribute.INTENSITY_BOLD),
205        INTENSITY_FAINT(Attribute.INTENSITY_FAINT),
206        ITALIC(Attribute.ITALIC),
207        UNDERLINE(Attribute.UNDERLINE),
208        BLINK_SLOW(Attribute.BLINK_SLOW),
209        BLINK_FAST(Attribute.BLINK_FAST),
210        BLINK_OFF(Attribute.BLINK_OFF),
211        NEGATIVE_ON(Attribute.NEGATIVE_ON),
212        NEGATIVE_OFF(Attribute.NEGATIVE_OFF),
213        CONCEAL_ON(Attribute.CONCEAL_ON),
214        CONCEAL_OFF(Attribute.CONCEAL_OFF),
215        UNDERLINE_DOUBLE(Attribute.UNDERLINE_DOUBLE),
216        UNDERLINE_OFF(Attribute.UNDERLINE_OFF),
217
218        // Aliases
219        BOLD(Attribute.INTENSITY_BOLD),
220        FAINT(Attribute.INTENSITY_FAINT),;
221
222        @SuppressWarnings("unchecked")
223        private final Enum n;
224
225        private final boolean background;
226
227        @SuppressWarnings("unchecked")
228        Code(final Enum n, boolean background) {
229            this.n = n;
230            this.background = background;
231        }
232
233        @SuppressWarnings("unchecked")
234        Code(final Enum n) {
235            this(n, false);
236        }
237
238        public boolean isColor() {
239            return n instanceof Ansi.Color;
240        }
241
242        public Ansi.Color getColor() {
243            return (Ansi.Color) n;
244        }
245
246        public boolean isAttribute() {
247            return n instanceof Attribute;
248        }
249
250        public Attribute getAttribute() {
251            return (Attribute) n;
252        }
253
254        public boolean isBackground() {
255            return background;
256        }
257    }
258
259    private AnsiRenderer() {
260    }
261
262}