PageRenderTime 33ms CodeModel.GetById 15ms app.highlight 14ms RepoModel.GetById 1ms app.codeStats 1ms

/hudson-core/src/main/java/hudson/MarkupText.java

http://github.com/hudson/hudson
Java | 334 lines | 170 code | 44 blank | 120 comment | 23 complexity | cc9960cf9b36b9560bbab8358fc65017 MD5 | raw file
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5 * 
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 * 
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 * 
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24package hudson;
 25
 26import java.util.ArrayList;
 27import java.util.Collections;
 28import java.util.List;
 29import java.util.regex.Matcher;
 30import java.util.regex.Pattern;
 31
 32/**
 33 * Mutable representation of string with HTML mark up.
 34 *
 35 * <p>
 36 * This class is used to put mark up on plain text.
 37 * See <a href="https://github.com/hudson/hudson/blob/master/hudson-core/src/test/java/hudson/MarkupTextTest.java">
 38 * the test code</a> for a typical usage and its result.
 39 *
 40 * @author Kohsuke Kawaguchi
 41 * @since 1.70
 42 */
 43public class MarkupText extends AbstractMarkupText {
 44    private final String text;
 45
 46    /**
 47     * Added mark up tags.
 48     */
 49    private final List<Tag> tags = new ArrayList<Tag>();
 50
 51    /**
 52     * Represents one mark up inserted into text.
 53     */
 54    private static final class Tag implements Comparable<Tag> {
 55        /**
 56         * Char position of this tag in {@link MarkupText#text}.
 57         * This tag is placed in front of the character of this index.
 58         */
 59        private final int pos;
 60        private final String markup;
 61
 62
 63        public Tag(int pos, String markup) {
 64            this.pos = pos;
 65            this.markup = markup;
 66        }
 67
 68        public int compareTo(Tag that) {
 69            return this.pos-that.pos;
 70        }
 71    }
 72
 73    /**
 74     * Represents a substring of a {@link MarkupText}.
 75     */
 76    public final class SubText extends AbstractMarkupText {
 77        private final int start,end;
 78        private final int[] groups;
 79
 80        public SubText(Matcher m, int textOffset) {
 81            start = m.start() + textOffset;
 82            end   = m.end() + textOffset;
 83
 84            int cnt = m.groupCount();
 85            groups = new int[cnt*2];
 86            for( int i=0; i<cnt; i++ ) {
 87                groups[i*2  ] = m.start(i+1) + textOffset;
 88                groups[i*2+1] = m.end(i+1) + textOffset;
 89            }
 90        }
 91
 92        public SubText(int start, int end) {
 93            this.start = start;
 94            this.end = end;
 95            groups = new int[0];
 96        }
 97
 98        @Override
 99        public SubText subText(int start, int end) {
100            return MarkupText.this.subText(this.start+start,
101                    end<0 ? this.end+1+end : this.start+end);
102        }
103
104        @Override
105        public String getText() {
106            return text.substring(start,end);
107        }
108
109        @Override
110        public void addMarkup(int startPos, int endPos, String startTag, String endTag) {
111            MarkupText.this.addMarkup(startPos+start,  endPos+start, startTag, endTag);
112        }
113
114        /**
115         * Surrounds this subtext with the specified start tag and the end tag.
116         *
117         * <p>
118         * Start/end tag text can contain special tokens "$0", "$1", ...
119         * and they will be replaced by their {@link #group(int) group match}.
120         * "\$" can be used to escape characters.
121         */
122        public void surroundWith(String startTag, String endTag) {
123            addMarkup(0,length(),replace(startTag),replace(endTag));
124        }
125
126        /**
127         * Works like {@link #surroundWith(String, String)} except
128         * that the token replacement is not performed on parameters.
129         */
130        public void surroundWithLiteral(String startTag, String endTag) {
131            addMarkup(0,length(),startTag,endTag);
132        }
133
134        /**
135         * Surrounds this subtext with &lt;a>...&lt;/a>. 
136         */
137        public void href(String url) {
138            addHyperlink(0,length(),url);
139        }
140
141        /**
142         * Gets the start index of the captured group within {@link MarkupText#getText()}.
143         *
144         * @param groupIndex
145         *      0 means the start of the whole subtext. 1, 2, ... are
146         *      groups captured by '(...)' in the regexp.
147         */
148        public int start(int groupIndex) {
149            if(groupIndex==0)    return start;
150            return groups[groupIndex*2-2];
151        }
152
153        /**
154         * Gets the start index of this subtext within {@link MarkupText#getText()}.
155         */
156        public int start() {
157            return start;
158        }
159
160        /**
161         * Gets the end index of the captured group within {@link MarkupText#getText()}.
162         */
163        public int end(int groupIndex) {
164            if(groupIndex==0)    return end;
165            return groups[groupIndex*2-1];
166        }
167
168        /**
169         * Gets the end index of this subtext within {@link MarkupText#getText()}.
170         */
171        public int end() {
172            return end;
173        }
174
175        /**
176         * Gets the text that represents the captured group.
177         */
178        public String group(int groupIndex) {
179            if(start(groupIndex)==-1)
180                return null;
181            return text.substring(start(groupIndex),end(groupIndex));
182        }
183
184        /**
185         * How many captured groups are in this subtext.
186         * @since 1.357
187         */
188        public int groupCount() {
189            return groups.length / 2;
190        }
191
192        /**
193         * Replaces the group tokens like "$0", "$1", and etc with their actual matches.
194         */
195        public String replace(String s) {
196            StringBuffer buf = new StringBuffer();
197
198            for( int i=0; i<s.length(); i++) {
199                char ch = s.charAt(i);
200                if (ch == '\\') {// escape char
201                    i++;
202                    buf.append(s.charAt(i));
203                } else if (ch == '$') {// replace by group
204                    i++;
205
206                    ch = s.charAt(i);
207                    // get the group number
208                    int groupId = ch - '0';
209                    if (groupId < 0 || groupId > 9) {
210                    	buf.append('$').append(ch);
211                    } else {
212                    	// add the group text
213                    	String group = group(groupId);
214                    	if (group != null) 
215                    		buf.append(group);
216                    }
217
218                } else {
219                    // other chars
220                    buf.append(ch);
221                }
222            }
223
224            return buf.toString();
225        }
226
227        @Override
228        protected SubText createSubText(Matcher m) {
229            return new SubText(m,start);
230        }
231    }
232
233    /**
234     *
235     * @param text
236     *      Plain text. This shouldn't include any markup nor escape. Those are done later in {@link #toString(boolean)}.
237     */
238    public MarkupText(String text) {
239        this.text = text;
240    }
241
242    @Override
243    public String getText() {
244        return text;
245    }
246
247    /**
248     * Returns a subtext.
249     *
250     * @param end
251     *      If negative, -N means "trim the last N-1 chars". That is, (s,-1) is the same as (s,length)
252     */
253    public SubText subText(int start, int end) {
254        return new SubText(start, end<0 ? text.length()+1+end : end);
255    }
256
257    @Override
258    public void addMarkup( int startPos, int endPos, String startTag, String endTag ) {
259        rangeCheck(startPos);
260        rangeCheck(endPos);
261        if(startPos>endPos) throw new IndexOutOfBoundsException();
262
263        // when multiple tags are added to the same range, we want them to show up like
264        // <b><i>abc</i></b>, not <b><i>abc</b></i>. Also, we'd like <b>abc</b><i>def</i>,
265        // not <b>abc<i></b>def</i>. Do this by inserting them to different places.
266        tags.add(new Tag(startPos, startTag));
267        tags.add(0,new Tag(endPos,endTag));
268    }
269
270    public void addMarkup(int pos, String tag) {
271        rangeCheck(pos);
272        tags.add(new Tag(pos,tag));
273    }
274
275    private void rangeCheck(int pos) {
276        if(pos<0 || pos>text.length())
277            throw new IndexOutOfBoundsException();
278    }
279
280    /**
281     * Returns the fully marked-up text.
282     *
283     * @deprecated as of 1.350.
284     *      Use {@link #toString(boolean)} to be explicit about the escape mode.
285     */
286    @Override
287    public String toString() {
288        return toString(false);
289    }
290
291    /**
292     * Returns the fully marked-up text.
293     *
294     * @param preEscape
295     *      If true, the escaping is for the &lt;PRE> context. This leave SP and CR/LF intact.
296     *      If false, the escape is for the normal HTML, thus SP becomes &amp;nbsp; and CR/LF becomes &lt;BR>
297     */
298    public String toString(boolean preEscape) {
299        if(tags.isEmpty())
300            return preEscape? Util.xmlEscape(text) : Util.escape(text);  // the most common case
301
302        Collections.sort(tags);
303
304        StringBuilder buf = new StringBuilder();
305        int copied = 0; // # of chars already copied from text to buf
306
307        for (Tag tag : tags) {
308            if (copied<tag.pos) {
309                String portion = text.substring(copied, tag.pos);
310                buf.append(preEscape ? Util.xmlEscape(portion) : Util.escape(portion));
311                copied = tag.pos;
312            }
313            buf.append(tag.markup);
314        }
315        if (copied<text.length()) {
316            String portion = text.substring(copied, text.length());
317            buf.append(preEscape ? Util.xmlEscape(portion) : Util.escape(portion));
318        }
319
320        return buf.toString();
321    }
322
323    // perhaps this method doesn't need to be here to remain binary compatible with past versions,
324    // but having this seems to be safer.
325    @Override
326    public List<SubText> findTokens(Pattern pattern) {
327        return super.findTokens(pattern);
328    }
329
330    @Override
331    protected SubText createSubText(Matcher m) {
332        return new SubText(m,0);
333    }
334}