Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 92b9747f authored by Siyamed Sinir's avatar Siyamed Sinir Committed by Android (Google) Code Review
Browse files

Merge "Sort the result of SpannableStringBuilder.getSpans"

parents 8c32981f fa05ba0b
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -1826,8 +1826,12 @@ public abstract class Layout {
            return ArrayUtils.emptyArray(type);
        }

        if(text instanceof SpannableStringBuilder) {
            return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
        } else {
            return text.getSpans(start, end, type);
        }
    }

    private char getEllipsisChar(TextUtils.TruncateAt method) {
        return (method == TextUtils.TruncateAt.END_SMALL) ?
+181 −23
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.text;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.style.ParagraphStyle;
import android.util.Log;

import com.android.internal.util.ArrayUtils;
@@ -66,11 +67,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        TextUtils.getChars(text, start, end, mText, 0);

        mSpanCount = 0;
        mSpanInsertCount = 0;
        mSpans = EmptyArray.OBJECT;
        mSpanStarts = EmptyArray.INT;
        mSpanEnds = EmptyArray.INT;
        mSpanFlags = EmptyArray.INT;
        mSpanMax = EmptyArray.INT;
        mSpanOrder = EmptyArray.INT;
        mPrioSortBuffer = EmptyArray.INT;
        mOrderSortBuffer = EmptyArray.INT;

        if (text instanceof Spanned) {
            Spanned sp = (Spanned) text;
@@ -234,6 +239,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
    // Documentation from interface
    public void clear() {
        replace(0, length(), "", 0, 0);
        mSpanInsertCount = 0;
    }

    // Documentation from interface
@@ -256,6 +262,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        if (mIndexOfSpan != null) {
            mIndexOfSpan.clear();
        }
        mSpanInsertCount = 0;
    }

    // Documentation from interface
@@ -485,6 +492,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
        System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
        System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
        System.arraycopy(mSpanOrder, i + 1, mSpanOrder, i, count);

        mSpanCount--;

@@ -712,9 +720,6 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
                end += mGapLength;
        }

        int count = mSpanCount;
        Object[] spans = mSpans;

        if (mIndexOfSpan != null) {
            Integer index = mIndexOfSpan.get(what);
            if (index != null) {
@@ -744,8 +749,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start);
        mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end);
        mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags);
        mSpanOrder = GrowingArrayUtils.append(mSpanOrder, mSpanCount, mSpanInsertCount);
        invalidateIndex(mSpanCount);
        mSpanCount++;
        mSpanInsertCount++;
        // Make sure there is enough room for empty interior nodes.
        // This magic formula computes the size of the smallest perfect binary
        // tree no smaller than mSpanCount.
@@ -837,6 +844,25 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
     */
    @SuppressWarnings("unchecked")
    public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind) {
        return getSpans(queryStart, queryEnd, kind, true);
    }

    /**
     * Return an array of the spans of the specified type that overlap
     * the specified range of the buffer.  The kind may be Object.class to get
     * a list of all the spans regardless of type.
     *
     * @param queryStart Start index.
     * @param queryEnd End index.
     * @param kind Class type to search for.
     * @param sort If true the results are sorted by the insertion order.
     * @param <T>
     * @return Array of the spans. Empty array if no results are found.
     *
     * @hide
     */
    public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind,
                                 boolean sort) {
        if (kind == null) return (T[]) ArrayUtils.emptyArray(Object.class);
        if (mSpanCount == 0) return ArrayUtils.emptyArray(kind);
        int count = countSpans(queryStart, queryEnd, kind, treeRoot());
@@ -846,7 +872,13 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable

        // Safe conversion, but requires a suppressWarning
        T[] ret = (T[]) Array.newInstance(kind, count);
        getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, 0);
        if (sort) {
            mPrioSortBuffer = checkSortBuffer(mPrioSortBuffer, count);
            mOrderSortBuffer = checkSortBuffer(mOrderSortBuffer, count);
        }
        getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, mPrioSortBuffer,
                mOrderSortBuffer, 0, sort);
        if (sort) sort(ret, mPrioSortBuffer, mOrderSortBuffer);
        return ret;
    }

@@ -876,7 +908,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
                if (spanEnd >= queryStart &&
                    (spanStart == spanEnd || queryStart == queryEnd ||
                        (spanStart != queryEnd && spanEnd != queryStart)) &&
                        kind.isInstance(mSpans[i])) {
                        (Object.class == kind || kind.isInstance(mSpans[i]))) {
                    count++;
                }
                if ((i & 1) != 0) {
@@ -887,9 +919,25 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        return count;
    }

    /**
     * Fills the result array with the spans found under the current interval tree node.
     *
     * @param queryStart Start index for the interval query.
     * @param queryEnd End index for the interval query.
     * @param kind Class type to search for.
     * @param i Index of the current tree node.
     * @param ret Array to be filled with results.
     * @param priority Buffer to keep record of the priorities of spans found.
     * @param insertionOrder Buffer to keep record of the insertion orders of spans found.
     * @param count The number of found spans.
     * @param sort Flag to fill the priority and insertion order buffers. If false then
     *             the spans with priority flag will be sorted in the result array.
     * @param <T>
     * @return The total number of spans found.
     */
    @SuppressWarnings("unchecked")
    private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
            int i, T[] ret, int count) {
            int i, T[] ret, int[] priority, int[] insertionOrder, int count, boolean sort) {
        if ((i & 1) != 0) {
            // internal tree node
            int left = leftChild(i);
@@ -898,7 +946,8 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
                spanMax -= mGapLength;
            }
            if (spanMax >= queryStart) {
                count = getSpansRec(queryStart, queryEnd, kind, left, ret, count);
                count = getSpansRec(queryStart, queryEnd, kind, left, ret, priority,
                        insertionOrder, count, sort);
            }
        }
        if (i >= mSpanCount) return count;
@@ -914,35 +963,136 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
            if (spanEnd >= queryStart &&
                    (spanStart == spanEnd || queryStart == queryEnd ||
                        (spanStart != queryEnd && spanEnd != queryStart)) &&
                        kind.isInstance(mSpans[i])) {
                int prio = mSpanFlags[i] & SPAN_PRIORITY;
                if (prio != 0) {
                    int j;

                    for (j = 0; j < count; j++) {
                        (Object.class == kind || kind.isInstance(mSpans[i]))) {
                int spanPriority = mSpanFlags[i] & SPAN_PRIORITY;
                if(sort) {
                    ret[count] = (T) mSpans[i];
                    priority[count] = spanPriority;
                    insertionOrder[count] = mSpanOrder[i];
                } else if (spanPriority != 0) {
                    //insertion sort for elements with priority
                    int j = 0;
                    for (; j < count; j++) {
                        int p = getSpanFlags(ret[j]) & SPAN_PRIORITY;

                        if (prio > p) {
                            break;
                        }
                        if (spanPriority > p) break;
                    }

                    System.arraycopy(ret, j, ret, j + 1, count - j);
                    // Safe conversion thanks to the isInstance test above
                    ret[j] = (T) mSpans[i];
                } else {
                    // Safe conversion thanks to the isInstance test above
                    ret[count] = (T) mSpans[i];
                }
                count++;
            }
            if (count < ret.length && (i & 1) != 0) {
                count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, count);
                count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, priority,
                        insertionOrder, count, sort);
            }
        }
        return count;
    }

    /**
     * Check the size of the buffer and grow if required.
     *
     * @param buffer Buffer to be checked.
     * @param size Required size.
     * @return Same buffer instance if the current size is greater than required size. Otherwise a
     * new instance is created and returned.
     */
    private final int[] checkSortBuffer(int[] buffer, int size) {
        if(size > buffer.length) {
            return ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(size));
        }
        return buffer;
    }

    /**
     * An iterative heap sort implementation. It will sort the spans using first their priority
     * then insertion order. A span with higher priority will be before a span with lower
     * priority. If priorities are the same, the spans will be sorted with insertion order. A
     * span with a lower insertion order will be before a span with a higher insertion order.
     *
     * @param array Span array to be sorted.
     * @param priority Priorities of the spans
     * @param insertionOrder Insertion orders of the spans
     * @param <T> Span object type.
     * @param <T>
     */
    private final <T> void sort(T[] array, int[] priority, int[] insertionOrder) {
        int size = array.length;
        for (int i = size / 2 - 1; i >= 0; i--) {
            siftDown(i, array, size, priority, insertionOrder);
        }

        for (int i = size - 1; i > 0; i--) {
            T v = array[0];
            int prio = priority[0];
            int insertOrder = insertionOrder[0];
            array[0] = array[i];
            priority[0] = priority[i];
            insertionOrder[0] = insertionOrder[i];
            siftDown(0, array, i, priority, insertionOrder);
            array[i] = v;
            priority[i] = prio;
            insertionOrder[i] = insertOrder;
        }
    }

    /**
     * Helper function for heap sort.
     *
     * @param index Index of the element to sift down.
     * @param array Span array to be sorted.
     * @param size Current heap size.
     * @param priority Priorities of the spans
     * @param insertionOrder Insertion orders of the spans
     * @param <T> Span object type.
     */
    private final <T> void siftDown(int index, T[] array, int size, int[] priority,
                                    int[] insertionOrder) {
        T v = array[index];
        int prio = priority[index];
        int insertOrder = insertionOrder[index];

        int left = 2 * index + 1;
        while (left < size) {
            if (left < size - 1 && compareSpans(left, left + 1, priority, insertionOrder) < 0) {
                left++;
            }
            if (compareSpans(index, left, priority, insertionOrder) >= 0) {
                break;
            }
            array[index] = array[left];
            priority[index] = priority[left];
            insertionOrder[index] = insertionOrder[left];
            index = left;
            left = 2 * index + 1;
        }
        array[index] = v;
        priority[index] = prio;
        insertionOrder[index] = insertOrder;
    }

    /**
     * Compare two span elements in an array. Comparison is based first on the priority flag of
     * the span, and then the insertion order of the span.
     *
     * @param left Index of the element to compare.
     * @param right Index of the other element to compare.
     * @param priority Priorities of the spans
     * @param insertionOrder Insertion orders of the spans
     * @return
     */
    private final int compareSpans(int left, int right, int[] priority,
                                       int[] insertionOrder) {
        int priority1 = priority[left];
        int priority2 = priority[right];
        if (priority1 == priority2) {
            return Integer.compare(insertionOrder[left], insertionOrder[right]);
        }
        // since high priority has to be before a lower priority, the arguments to compare are
        // opposite of the insertion order check.
        return Integer.compare(priority2, priority1);
    }

    /**
     * Return the next offset after <code>start</code> but less than or
     * equal to <code>limit</code> where a span of the specified type
@@ -1509,18 +1659,21 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
                int start = mSpanStarts[i];
                int end = mSpanEnds[i];
                int flags = mSpanFlags[i];
                int insertionOrder = mSpanOrder[i];
                int j = i;
                do {
                    mSpans[j] = mSpans[j - 1];
                    mSpanStarts[j] = mSpanStarts[j - 1];
                    mSpanEnds[j] = mSpanEnds[j - 1];
                    mSpanFlags[j] = mSpanFlags[j - 1];
                    mSpanOrder[j] = mSpanOrder[j - 1];
                    j--;
                } while (j > 0 && start < mSpanStarts[j - 1]);
                mSpans[j] = span;
                mSpanStarts[j] = start;
                mSpanEnds[j] = end;
                mSpanFlags[j] = flags;
                mSpanOrder[j] = insertionOrder;
                invalidateIndex(j);
            }
        }
@@ -1558,6 +1711,11 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
    private int[] mSpanEnds;
    private int[] mSpanMax;  // see calcMax() for an explanation of what this array stores
    private int[] mSpanFlags;
    private int[] mSpanOrder;  // store the order of span insertion
    private int mSpanInsertCount;  // counter for the span insertion
    private int[] mPrioSortBuffer;  // buffer used to sort getSpans result
    private int[] mOrderSortBuffer;  // buffer used to sort getSpans result

    private int mSpanCount;
    private IdentityHashMap<Object, Integer> mIndexOfSpan;
    private int mLowWaterMark;  // indices below this have not been touched
+87 −12
Original line number Diff line number Diff line
@@ -36,13 +36,28 @@ import java.lang.reflect.Array;
        mSpanData = EmptyArray.INT;

        if (source instanceof Spanned) {
            Spanned sp = (Spanned) source;
            Object[] spans = sp.getSpans(start, end, Object.class);
            if (source instanceof SpannableStringInternal) {
                copySpans((SpannableStringInternal) source, start, end);
            } else {
                copySpans((Spanned) source, start, end);
            }
        }
    }

    /**
     * Copies another {@link Spanned} object's spans between [start, end] into this object.
     *
     * @param src Source object to copy from.
     * @param start Start index in the source object.
     * @param end End index in the source object.
     */
    private final void copySpans(Spanned src, int start, int end) {
        Object[] spans = src.getSpans(start, end, Object.class);

        for (int i = 0; i < spans.length; i++) {
                int st = sp.getSpanStart(spans[i]);
                int en = sp.getSpanEnd(spans[i]);
                int fl = sp.getSpanFlags(spans[i]);
            int st = src.getSpanStart(spans[i]);
            int en = src.getSpanEnd(spans[i]);
            int fl = src.getSpanFlags(spans[i]);

            if (st < start)
                st = start;
@@ -52,6 +67,66 @@ import java.lang.reflect.Array;
            setSpan(spans[i], st - start, en - start, fl);
        }
    }

    /**
     * Copies a {@link SpannableStringInternal} object's spans between [start, end] into this
     * object.
     *
     * @param src Source object to copy from.
     * @param start Start index in the source object.
     * @param end End index in the source object.
     */
    private final void copySpans(SpannableStringInternal src, int start, int end) {
        if (start == 0 && end == src.length()) {
            mSpans = ArrayUtils.newUnpaddedObjectArray(src.mSpans.length);
            mSpanData = new int[src.mSpanData.length];
            mSpanCount = src.mSpanCount;
            System.arraycopy(src.mSpans, 0, mSpans, 0, src.mSpans.length);
            System.arraycopy(src.mSpanData, 0, mSpanData, 0, mSpanData.length);
        } else {
            int count = 0;
            int[] srcData = src.mSpanData;
            int limit = src.mSpanCount;
            for (int i = 0; i < limit; i++) {
                int spanStart = srcData[i * COLUMNS + START];
                int spanEnd = srcData[i * COLUMNS + END];
                if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
                count++;
            }

            if (count == 0) return;

            Object[] srcSpans = src.mSpans;
            mSpanCount = count;
            mSpans = ArrayUtils.newUnpaddedObjectArray(mSpanCount);
            mSpanData = new int[mSpanCount * COLUMNS];
            for (int i = 0, j = 0; i < limit; i++) {
                int spanStart = srcData[i * COLUMNS + START];
                int spanEnd = srcData[i * COLUMNS + END];
                if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
                if (spanStart < start) spanStart = start;
                if (spanEnd > end) spanEnd = end;

                mSpans[j] = srcSpans[i];
                mSpanData[j * COLUMNS + START] = spanStart - start;
                mSpanData[j * COLUMNS + END] = spanEnd - start;
                mSpanData[j * COLUMNS + FLAGS] = srcData[i * COLUMNS + FLAGS];
                j++;
            }
        }
    }

    /**
     * Checks if [spanStart, spanEnd] interval is excluded from [start, end].
     *
     * @return True if excluded, false if included.
     */
    private final boolean isOutOfCopyRange(int start, int end, int spanStart, int spanEnd) {
        if (spanStart > end || spanEnd < start) return true;
        if (spanStart != spanEnd && start != end) {
            if (spanStart == end || spanEnd == start) return true;
        }
        return false;
    }

    public final int length() {
@@ -234,7 +309,7 @@ import java.lang.reflect.Array;
            }

            // verify span class as late as possible, since it is expensive
            if (kind != null && !kind.isInstance(spans[i])) {
            if (kind != null && kind != Object.class && !kind.isInstance(spans[i])) {
                continue;
            }

+138 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import com.google.caliper.AfterExperiment;
import com.google.caliper.BeforeExperiment;
import com.google.caliper.Benchmark;
import com.google.caliper.Param;

public class SpannableStringBuilderBenchmark {

    @Param({"android.text.style.ImageSpan",
            "android.text.style.ParagraphStyle",
            "android.text.style.CharacterStyle",
            "java.lang.Object"})
    private String paramType;

    @Param({"1", "4", "16"})
    private String paramStringMult;

    private Class clazz;
    private SpannableStringBuilder builder;

    @BeforeExperiment
    protected void setUp() throws Exception {
        clazz = Class.forName(paramType);
        int strSize = Integer.valueOf(paramStringMult);
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < strSize; i++) {
            strBuilder.append(TEST_STRING);
        }
        builder = new SpannableStringBuilder(Html.fromHtml(strBuilder.toString()));
    }

    @AfterExperiment
    protected void tearDown() {
        builder.clear();
        builder = null;
    }

    @Benchmark
    public void timeGetSpans(int reps) throws Exception {
        for (int i = 0; i < reps; i++) {
            builder.getSpans(0, builder.length(), clazz);
        }
    }

    //contains 0 ImageSpans, 2 ParagraphSpans, 53 CharacterStyleSpans
    public static String TEST_STRING =
            "<p><span><a href=\"http://android.com\">some link</a></span></p>\n" +
            "<h1 style=\"margin: 0.0px 0.0px 10.0px 0.0px; line-height: 64.0px; font: 62.0px " +
                    "'Helvetica Neue Light'; color: #000000; \"><span>some title</span></h1>\n" +
            "<p><span>by <a href=\"http://android.com\"><span>some name</span></a>\n" +
            "  <a href=\"https://android.com\"><span>some text</span></a></span></p>\n" +
            "<p><span>some date</span></p>\n" +
            "<table cellspacing=\"0\" cellpadding=\"0\">\n" +
            "  <tbody><tr><td valign=\"bottom\">\n" +
            "        <p><span><blockquote>a paragraph</blockquote></span><br></p>\n" +
            "  </tbody></tr></td>\n" +
            "</table>\n" +
            "<h2 style=\"margin: 0.0px 0.0px 0.0px 0.0px; line-height: 38.0px; font: 26.0px " +
                    "'Helvetica Neue Light'; color: #262626; -webkit-text-stroke: #262626\">" +
                    "<span>some header two</span></h2>\n" +
            "<p><span>Lorem ipsum dolor concludaturque. </span></p>\n" +
            "<p><span></span><br></p>\n" +
            "<p><span>Vix te doctus</span></p>\n" +
            "<p><span><b>Error mel</b></span><span>, est ei. <a href=\"http://andorid.com\">" +
                    "<span>asda</span></a> ullamcorper eam.</span></p>\n" +
            "<p><span>adversarium <a href=\"http://android.com\"><span>efficiantur</span></a>, " +
                    "mea te.</span></p>\n" +
            "<p><span></span><br></p>\n" +
            "<h1>Testing display of HTML elements</h1>\n" +
            "<h2>2nd level heading</h2>\n" +
            "<p>test paragraph.</p>\n" +
            "<h3>3rd level heading</h3>\n" +
            "<p>test paragraph.</p>\n" +
            "<h4>4th level heading</h4>\n" +
            "<p>test paragraph.</p>\n" +
            "<h5>5th level heading</h5>\n" +
            "<p>test paragraph.</p>\n" +
            "<h6>6th level heading</h6>\n" +
            "<p>test paragraph.</p>\n" +
            "<h2>level elements</h2>\n" +
            "<p>a normap paragraph(<code>p</code> element).\n" +
            "  with some <strong>strong</strong>.</p>\n" +
            "<div>This is a <code>div</code> element. </div>\n" +
            "<blockquote><p>This is a block quotation with some <em>style</em></p></blockquote>\n" +
            "<address>an address element</address>\n" +
            "<h2>Text-level markup</h2>\n" +
            "<ul>\n" +
            "  <li> <abbr title=\"Cascading Style Sheets\">CSS</abbr> (an abbreviation;\n" +
            "    <code>abbr</code> markup used)\n" +
            "  <li> <acronym title=\"radio detecting and ranging\">radar</acronym>\n" +
            "  <li> <b>bolded</b>\n" +
            "  <li> <big>big thing</big>\n" +
            "  <li> <font size=6>large size</font>\n" +
            "  <li> <font face=Courier>Courier font</font>\n" +
            "  <li> <font color=red>red text</font>\n" +
            "  <li> <cite>Origin of Species</cite>\n" +
            "  <li> <code>a[i] = b[i] + c[i);</code>\n" +
            "  <li> some <del>deleted</del> text\n" +
            "  <li> an <dfn>octet</dfn> is an\n" +
            "  <li> this is <em>very</em> simple\n" +
            "  <li> <i lang=\"la\">Homo sapiens</i>\n" +
            "  <li> some <ins>inserted</ins> text\n" +
            "  <li> type <kbd>yes</kbd> when\n" +
            "  <li> <q>Hello!</q>\n" +
            "  <li> <q>She said <q>Hello!</q></q>\n" +
            "  <li> <samp>ccc</samp>\n" +
            "  <li> <small>important</small>\n" +
            "  <li> <strike>overstruck</strike>\n" +
            "  <li> <strong>this is highlighted text</strong>\n" +
            "  <li> <code>sub</code> and\n" +
            "    <code>sup</code> x<sub>1</sub> and H<sub>2</sub>O\n" +
            "    M<sup>lle</sup>, 1<sup>st</sup>, e<sup>x</sup>, sin<sup>2</sup> <i>x</i>,\n" +
            "    e<sup>x<sup>2</sup></sup> and f(x)<sup>g(x)<sup>a+b+c</sup></sup>\n" +
            "    (where 2 and a+b+c should appear as exponents of exponents).\n" +
            "  <li> <tt>text in monospace font</tt>\n" +
            "  <li> <u>underlined</u> text\n" +
            "  <li> <code>cat</code> <var>filename</var> displays the\n" +
            "    the <var>filename</var>.\n" +
            "</ul>\n";

}
+61 −0

File added.

Preview size limit exceeded, changes collapsed.