1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import com.android.internal.util.ArrayUtils;
20import org.ccil.cowan.tagsoup.HTMLSchema;
21import org.ccil.cowan.tagsoup.Parser;
22import org.xml.sax.Attributes;
23import org.xml.sax.ContentHandler;
24import org.xml.sax.InputSource;
25import org.xml.sax.Locator;
26import org.xml.sax.SAXException;
27import org.xml.sax.XMLReader;
28
29import android.app.ActivityThread;
30import android.app.Application;
31import android.content.res.ColorStateList;
32import android.content.res.Resources;
33import android.graphics.Color;
34import android.graphics.Typeface;
35import android.graphics.drawable.Drawable;
36import android.text.style.AbsoluteSizeSpan;
37import android.text.style.AlignmentSpan;
38import android.text.style.BackgroundColorSpan;
39import android.text.style.BulletSpan;
40import android.text.style.CharacterStyle;
41import android.text.style.ForegroundColorSpan;
42import android.text.style.ImageSpan;
43import android.text.style.ParagraphStyle;
44import android.text.style.QuoteSpan;
45import android.text.style.RelativeSizeSpan;
46import android.text.style.StrikethroughSpan;
47import android.text.style.StyleSpan;
48import android.text.style.SubscriptSpan;
49import android.text.style.SuperscriptSpan;
50import android.text.style.TextAppearanceSpan;
51import android.text.style.TypefaceSpan;
52import android.text.style.URLSpan;
53import android.text.style.UnderlineSpan;
54
55import java.io.IOException;
56import java.io.StringReader;
57import java.util.HashMap;
58import java.util.Locale;
59import java.util.Map;
60import java.util.regex.Matcher;
61import java.util.regex.Pattern;
62
63/**
64 * This class processes HTML strings into displayable styled text.
65 * Not all HTML tags are supported.
66 */
67public class Html {
68    /**
69     * Retrieves images for HTML <img> tags.
70     */
71    public static interface ImageGetter {
72        /**
73         * This method is called when the HTML parser encounters an
74         * &lt;img&gt; tag.  The <code>source</code> argument is the
75         * string from the "src" attribute; the return value should be
76         * a Drawable representation of the image or <code>null</code>
77         * for a generic replacement image.  Make sure you call
78         * setBounds() on your Drawable if it doesn't already have
79         * its bounds set.
80         */
81        public Drawable getDrawable(String source);
82    }
83
84    /**
85     * Is notified when HTML tags are encountered that the parser does
86     * not know how to interpret.
87     */
88    public static interface TagHandler {
89        /**
90         * This method will be called whenn the HTML parser encounters
91         * a tag that it does not know how to interpret.
92         */
93        public void handleTag(boolean opening, String tag,
94                                 Editable output, XMLReader xmlReader);
95    }
96
97    /**
98     * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n'
99     * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored.
100     */
101    public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000;
102
103    /**
104     * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a
105     * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be
106     * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element.
107     */
108    public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001;
109
110    /**
111     * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with
112     * one newline character by default.
113     */
114    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001;
115
116    /**
117     * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from
118     * other texts with one newline character by default.
119     */
120    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002;
121
122    /**
123     * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts
124     * with one newline character by default.
125     */
126    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004;
127
128    /**
129     * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts
130     * with one newline character by default.
131     */
132    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008;
133
134    /**
135     * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts
136     * with one newline character by default.
137     */
138    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010;
139
140    /**
141     * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other
142     * texts with one newline character by default.
143     */
144    public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020;
145
146    /**
147     * Flag indicating that CSS color values should be used instead of those defined in
148     * {@link Color}.
149     */
150    public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100;
151
152    /**
153     * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
154     * elements with blank lines (two newline characters) in between. This is the legacy behavior
155     * prior to N.
156     */
157    public static final int FROM_HTML_MODE_LEGACY = 0x00000000;
158
159    /**
160     * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
161     * elements with line breaks (single newline character) in between. This inverts the
162     * {@link Spanned} to HTML string conversion done with the option
163     * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}.
164     */
165    public static final int FROM_HTML_MODE_COMPACT =
166            FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH
167            | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING
168            | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM
169            | FROM_HTML_SEPARATOR_LINE_BREAK_LIST
170            | FROM_HTML_SEPARATOR_LINE_BREAK_DIV
171            | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE;
172
173    /**
174     * The bit which indicates if lines delimited by '\n' will be grouped into &lt;p&gt; elements.
175     */
176    private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001;
177
178    private Html() { }
179
180    /**
181     * Returns displayable styled text from the provided HTML string with the legacy flags
182     * {@link #FROM_HTML_MODE_LEGACY}.
183     *
184     * @deprecated use {@link #fromHtml(String, int)} instead.
185     */
186    @Deprecated
187    public static Spanned fromHtml(String source) {
188        return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null);
189    }
190
191    /**
192     * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
193     * HTML will display as a generic replacement image which your program can then go through and
194     * replace with real images.
195     *
196     * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
197     */
198    public static Spanned fromHtml(String source, int flags) {
199        return fromHtml(source, flags, null, null);
200    }
201
202    /**
203     * Lazy initialization holder for HTML parser. This class will
204     * a) be preloaded by the zygote, or b) not loaded until absolutely
205     * necessary.
206     */
207    private static class HtmlParser {
208        private static final HTMLSchema schema = new HTMLSchema();
209    }
210
211    /**
212     * Returns displayable styled text from the provided HTML string with the legacy flags
213     * {@link #FROM_HTML_MODE_LEGACY}.
214     *
215     * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead.
216     */
217    @Deprecated
218    public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
219        return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
220    }
221
222    /**
223     * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
224     * HTML will use the specified ImageGetter to request a representation of the image (use null
225     * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
226     * you don't want this).
227     *
228     * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
229     */
230    public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
231            TagHandler tagHandler) {
232        Parser parser = new Parser();
233        try {
234            parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
235        } catch (org.xml.sax.SAXNotRecognizedException e) {
236            // Should not happen.
237            throw new RuntimeException(e);
238        } catch (org.xml.sax.SAXNotSupportedException e) {
239            // Should not happen.
240            throw new RuntimeException(e);
241        }
242
243        HtmlToSpannedConverter converter =
244                new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
245        return converter.convert();
246    }
247
248    /**
249     * @deprecated use {@link #toHtml(Spanned, int)} instead.
250     */
251    @Deprecated
252    public static String toHtml(Spanned text) {
253        return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
254    }
255
256    /**
257     * Returns an HTML representation of the provided Spanned text. A best effort is
258     * made to add HTML tags corresponding to spans. Also note that HTML metacharacters
259     * (such as "&lt;" and "&amp;") within the input text are escaped.
260     *
261     * @param text input text to convert
262     * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or
263     *     {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}
264     * @return string containing input converted to HTML
265     */
266    public static String toHtml(Spanned text, int option) {
267        StringBuilder out = new StringBuilder();
268        withinHtml(out, text, option);
269        return out.toString();
270    }
271
272    /**
273     * Returns an HTML escaped representation of the given plain text.
274     */
275    public static String escapeHtml(CharSequence text) {
276        StringBuilder out = new StringBuilder();
277        withinStyle(out, text, 0, text.length());
278        return out.toString();
279    }
280
281    private static void withinHtml(StringBuilder out, Spanned text, int option) {
282        if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
283            encodeTextAlignmentByDiv(out, text, option);
284            return;
285        }
286
287        withinDiv(out, text, 0, text.length(), option);
288    }
289
290    private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) {
291        int len = text.length();
292
293        int next;
294        for (int i = 0; i < len; i = next) {
295            next = text.nextSpanTransition(i, len, ParagraphStyle.class);
296            ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
297            String elements = " ";
298            boolean needDiv = false;
299
300            for(int j = 0; j < style.length; j++) {
301                if (style[j] instanceof AlignmentSpan) {
302                    Layout.Alignment align =
303                        ((AlignmentSpan) style[j]).getAlignment();
304                    needDiv = true;
305                    if (align == Layout.Alignment.ALIGN_CENTER) {
306                        elements = "align=\"center\" " + elements;
307                    } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
308                        elements = "align=\"right\" " + elements;
309                    } else {
310                        elements = "align=\"left\" " + elements;
311                    }
312                }
313            }
314            if (needDiv) {
315                out.append("<div ").append(elements).append(">");
316            }
317
318            withinDiv(out, text, i, next, option);
319
320            if (needDiv) {
321                out.append("</div>");
322            }
323        }
324    }
325
326    private static void withinDiv(StringBuilder out, Spanned text, int start, int end,
327            int option) {
328        int next;
329        for (int i = start; i < end; i = next) {
330            next = text.nextSpanTransition(i, end, QuoteSpan.class);
331            QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
332
333            for (QuoteSpan quote : quotes) {
334                out.append("<blockquote>");
335            }
336
337            withinBlockquote(out, text, i, next, option);
338
339            for (QuoteSpan quote : quotes) {
340                out.append("</blockquote>\n");
341            }
342        }
343    }
344
345    private static String getTextDirection(Spanned text, int start, int end) {
346        final int len = end - start;
347        final byte[] levels = ArrayUtils.newUnpaddedByteArray(len);
348        final char[] buffer = TextUtils.obtain(len);
349        TextUtils.getChars(text, start, end, buffer, 0);
350
351        int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,
352                false /* no info */);
353        switch(paraDir) {
354            case Layout.DIR_RIGHT_TO_LEFT:
355                return " dir=\"rtl\"";
356            case Layout.DIR_LEFT_TO_RIGHT:
357            default:
358                return " dir=\"ltr\"";
359        }
360    }
361
362    private static String getTextStyles(Spanned text, int start, int end,
363            boolean forceNoVerticalMargin, boolean includeTextAlign) {
364        String margin = null;
365        String textAlign = null;
366
367        if (forceNoVerticalMargin) {
368            margin = "margin-top:0; margin-bottom:0;";
369        }
370        if (includeTextAlign) {
371            final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class);
372
373            // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH
374            for (int i = alignmentSpans.length - 1; i >= 0; i--) {
375                AlignmentSpan s = alignmentSpans[i];
376                if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) {
377                    final Layout.Alignment alignment = s.getAlignment();
378                    if (alignment == Layout.Alignment.ALIGN_NORMAL) {
379                        textAlign = "text-align:start;";
380                    } else if (alignment == Layout.Alignment.ALIGN_CENTER) {
381                        textAlign = "text-align:center;";
382                    } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
383                        textAlign = "text-align:end;";
384                    }
385                    break;
386                }
387            }
388        }
389
390        if (margin == null && textAlign == null) {
391            return "";
392        }
393
394        final StringBuilder style = new StringBuilder(" style=\"");
395        if (margin != null && textAlign != null) {
396            style.append(margin).append(" ").append(textAlign);
397        } else if (margin != null) {
398            style.append(margin);
399        } else if (textAlign != null) {
400            style.append(textAlign);
401        }
402
403        return style.append("\"").toString();
404    }
405
406    private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end,
407            int option) {
408        if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
409            withinBlockquoteConsecutive(out, text, start, end);
410        } else {
411            withinBlockquoteIndividual(out, text, start, end);
412        }
413    }
414
415    private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start,
416            int end) {
417        boolean isInList = false;
418        int next;
419        for (int i = start; i <= end; i = next) {
420            next = TextUtils.indexOf(text, '\n', i, end);
421            if (next < 0) {
422                next = end;
423            }
424
425            if (next == i) {
426                if (isInList) {
427                    // Current paragraph is no longer a list item; close the previously opened list
428                    isInList = false;
429                    out.append("</ul>\n");
430                }
431                out.append("<br>\n");
432            } else {
433                boolean isListItem = false;
434                ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class);
435                for (ParagraphStyle paragraphStyle : paragraphStyles) {
436                    final int spanFlags = text.getSpanFlags(paragraphStyle);
437                    if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH
438                            && paragraphStyle instanceof BulletSpan) {
439                        isListItem = true;
440                        break;
441                    }
442                }
443
444                if (isListItem && !isInList) {
445                    // Current paragraph is the first item in a list
446                    isInList = true;
447                    out.append("<ul")
448                            .append(getTextStyles(text, i, next, true, false))
449                            .append(">\n");
450                }
451
452                if (isInList && !isListItem) {
453                    // Current paragraph is no longer a list item; close the previously opened list
454                    isInList = false;
455                    out.append("</ul>\n");
456                }
457
458                String tagType = isListItem ? "li" : "p";
459                out.append("<").append(tagType)
460                        .append(getTextDirection(text, i, next))
461                        .append(getTextStyles(text, i, next, !isListItem, true))
462                        .append(">");
463
464                withinParagraph(out, text, i, next);
465
466                out.append("</");
467                out.append(tagType);
468                out.append(">\n");
469
470                if (next == end && isInList) {
471                    isInList = false;
472                    out.append("</ul>\n");
473                }
474            }
475
476            next++;
477        }
478    }
479
480    private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start,
481            int end) {
482        out.append("<p").append(getTextDirection(text, start, end)).append(">");
483
484        int next;
485        for (int i = start; i < end; i = next) {
486            next = TextUtils.indexOf(text, '\n', i, end);
487            if (next < 0) {
488                next = end;
489            }
490
491            int nl = 0;
492
493            while (next < end && text.charAt(next) == '\n') {
494                nl++;
495                next++;
496            }
497
498            withinParagraph(out, text, i, next - nl);
499
500            if (nl == 1) {
501                out.append("<br>\n");
502            } else {
503                for (int j = 2; j < nl; j++) {
504                    out.append("<br>");
505                }
506                if (next != end) {
507                    /* Paragraph should be closed and reopened */
508                    out.append("</p>\n");
509                    out.append("<p").append(getTextDirection(text, start, end)).append(">");
510                }
511            }
512        }
513
514        out.append("</p>\n");
515    }
516
517    private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {
518        int next;
519        for (int i = start; i < end; i = next) {
520            next = text.nextSpanTransition(i, end, CharacterStyle.class);
521            CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
522
523            for (int j = 0; j < style.length; j++) {
524                if (style[j] instanceof StyleSpan) {
525                    int s = ((StyleSpan) style[j]).getStyle();
526
527                    if ((s & Typeface.BOLD) != 0) {
528                        out.append("<b>");
529                    }
530                    if ((s & Typeface.ITALIC) != 0) {
531                        out.append("<i>");
532                    }
533                }
534                if (style[j] instanceof TypefaceSpan) {
535                    String s = ((TypefaceSpan) style[j]).getFamily();
536
537                    if ("monospace".equals(s)) {
538                        out.append("<tt>");
539                    }
540                }
541                if (style[j] instanceof SuperscriptSpan) {
542                    out.append("<sup>");
543                }
544                if (style[j] instanceof SubscriptSpan) {
545                    out.append("<sub>");
546                }
547                if (style[j] instanceof UnderlineSpan) {
548                    out.append("<u>");
549                }
550                if (style[j] instanceof StrikethroughSpan) {
551                    out.append("<span style=\"text-decoration:line-through;\">");
552                }
553                if (style[j] instanceof URLSpan) {
554                    out.append("<a href=\"");
555                    out.append(((URLSpan) style[j]).getURL());
556                    out.append("\">");
557                }
558                if (style[j] instanceof ImageSpan) {
559                    out.append("<img src=\"");
560                    out.append(((ImageSpan) style[j]).getSource());
561                    out.append("\">");
562
563                    // Don't output the dummy character underlying the image.
564                    i = next;
565                }
566                if (style[j] instanceof AbsoluteSizeSpan) {
567                    AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
568                    float sizeDip = s.getSize();
569                    if (!s.getDip()) {
570                        Application application = ActivityThread.currentApplication();
571                        sizeDip /= application.getResources().getDisplayMetrics().density;
572                    }
573
574                    // px in CSS is the equivalance of dip in Android
575                    out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip));
576                }
577                if (style[j] instanceof RelativeSizeSpan) {
578                    float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
579                    out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm));
580                }
581                if (style[j] instanceof ForegroundColorSpan) {
582                    int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
583                    out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color));
584                }
585                if (style[j] instanceof BackgroundColorSpan) {
586                    int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
587                    out.append(String.format("<span style=\"background-color:#%06X;\">",
588                            0xFFFFFF & color));
589                }
590            }
591
592            withinStyle(out, text, i, next);
593
594            for (int j = style.length - 1; j >= 0; j--) {
595                if (style[j] instanceof BackgroundColorSpan) {
596                    out.append("</span>");
597                }
598                if (style[j] instanceof ForegroundColorSpan) {
599                    out.append("</span>");
600                }
601                if (style[j] instanceof RelativeSizeSpan) {
602                    out.append("</span>");
603                }
604                if (style[j] instanceof AbsoluteSizeSpan) {
605                    out.append("</span>");
606                }
607                if (style[j] instanceof URLSpan) {
608                    out.append("</a>");
609                }
610                if (style[j] instanceof StrikethroughSpan) {
611                    out.append("</span>");
612                }
613                if (style[j] instanceof UnderlineSpan) {
614                    out.append("</u>");
615                }
616                if (style[j] instanceof SubscriptSpan) {
617                    out.append("</sub>");
618                }
619                if (style[j] instanceof SuperscriptSpan) {
620                    out.append("</sup>");
621                }
622                if (style[j] instanceof TypefaceSpan) {
623                    String s = ((TypefaceSpan) style[j]).getFamily();
624
625                    if (s.equals("monospace")) {
626                        out.append("</tt>");
627                    }
628                }
629                if (style[j] instanceof StyleSpan) {
630                    int s = ((StyleSpan) style[j]).getStyle();
631
632                    if ((s & Typeface.BOLD) != 0) {
633                        out.append("</b>");
634                    }
635                    if ((s & Typeface.ITALIC) != 0) {
636                        out.append("</i>");
637                    }
638                }
639            }
640        }
641    }
642
643    private static void withinStyle(StringBuilder out, CharSequence text,
644                                    int start, int end) {
645        for (int i = start; i < end; i++) {
646            char c = text.charAt(i);
647
648            if (c == '<') {
649                out.append("&lt;");
650            } else if (c == '>') {
651                out.append("&gt;");
652            } else if (c == '&') {
653                out.append("&amp;");
654            } else if (c >= 0xD800 && c <= 0xDFFF) {
655                if (c < 0xDC00 && i + 1 < end) {
656                    char d = text.charAt(i + 1);
657                    if (d >= 0xDC00 && d <= 0xDFFF) {
658                        i++;
659                        int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
660                        out.append("&#").append(codepoint).append(";");
661                    }
662                }
663            } else if (c > 0x7E || c < ' ') {
664                out.append("&#").append((int) c).append(";");
665            } else if (c == ' ') {
666                while (i + 1 < end && text.charAt(i + 1) == ' ') {
667                    out.append("&nbsp;");
668                    i++;
669                }
670
671                out.append(' ');
672            } else {
673                out.append(c);
674            }
675        }
676    }
677}
678
679class HtmlToSpannedConverter implements ContentHandler {
680
681    private static final float[] HEADING_SIZES = {
682        1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
683    };
684
685    private String mSource;
686    private XMLReader mReader;
687    private SpannableStringBuilder mSpannableStringBuilder;
688    private Html.ImageGetter mImageGetter;
689    private Html.TagHandler mTagHandler;
690    private int mFlags;
691
692    private static Pattern sTextAlignPattern;
693    private static Pattern sForegroundColorPattern;
694    private static Pattern sBackgroundColorPattern;
695    private static Pattern sTextDecorationPattern;
696
697    /**
698     * Name-value mapping of HTML/CSS colors which have different values in {@link Color}.
699     */
700    private static final Map<String, Integer> sColorMap;
701
702    static {
703        sColorMap = new HashMap<>();
704        sColorMap.put("darkgray", 0xFFA9A9A9);
705        sColorMap.put("gray", 0xFF808080);
706        sColorMap.put("lightgray", 0xFFD3D3D3);
707        sColorMap.put("darkgrey", 0xFFA9A9A9);
708        sColorMap.put("grey", 0xFF808080);
709        sColorMap.put("lightgrey", 0xFFD3D3D3);
710        sColorMap.put("green", 0xFF008000);
711    }
712
713    private static Pattern getTextAlignPattern() {
714        if (sTextAlignPattern == null) {
715            sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b");
716        }
717        return sTextAlignPattern;
718    }
719
720    private static Pattern getForegroundColorPattern() {
721        if (sForegroundColorPattern == null) {
722            sForegroundColorPattern = Pattern.compile(
723                    "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
724        }
725        return sForegroundColorPattern;
726    }
727
728    private static Pattern getBackgroundColorPattern() {
729        if (sBackgroundColorPattern == null) {
730            sBackgroundColorPattern = Pattern.compile(
731                    "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
732        }
733        return sBackgroundColorPattern;
734    }
735
736    private static Pattern getTextDecorationPattern() {
737        if (sTextDecorationPattern == null) {
738            sTextDecorationPattern = Pattern.compile(
739                    "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
740        }
741        return sTextDecorationPattern;
742    }
743
744    public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,
745            Html.TagHandler tagHandler, Parser parser, int flags) {
746        mSource = source;
747        mSpannableStringBuilder = new SpannableStringBuilder();
748        mImageGetter = imageGetter;
749        mTagHandler = tagHandler;
750        mReader = parser;
751        mFlags = flags;
752    }
753
754    public Spanned convert() {
755
756        mReader.setContentHandler(this);
757        try {
758            mReader.parse(new InputSource(new StringReader(mSource)));
759        } catch (IOException e) {
760            // We are reading from a string. There should not be IO problems.
761            throw new RuntimeException(e);
762        } catch (SAXException e) {
763            // TagSoup doesn't throw parse exceptions.
764            throw new RuntimeException(e);
765        }
766
767        // Fix flags and range for paragraph-type markup.
768        Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
769        for (int i = 0; i < obj.length; i++) {
770            int start = mSpannableStringBuilder.getSpanStart(obj[i]);
771            int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
772
773            // If the last line of the range is blank, back off by one.
774            if (end - 2 >= 0) {
775                if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
776                    mSpannableStringBuilder.charAt(end - 2) == '\n') {
777                    end--;
778                }
779            }
780
781            if (end == start) {
782                mSpannableStringBuilder.removeSpan(obj[i]);
783            } else {
784                mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
785            }
786        }
787
788        return mSpannableStringBuilder;
789    }
790
791    private void handleStartTag(String tag, Attributes attributes) {
792        if (tag.equalsIgnoreCase("br")) {
793            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
794            // so we can safely emit the linebreaks when we handle the close tag.
795        } else if (tag.equalsIgnoreCase("p")) {
796            startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
797            startCssStyle(mSpannableStringBuilder, attributes);
798        } else if (tag.equalsIgnoreCase("ul")) {
799            startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
800        } else if (tag.equalsIgnoreCase("li")) {
801            startLi(mSpannableStringBuilder, attributes);
802        } else if (tag.equalsIgnoreCase("div")) {
803            startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
804        } else if (tag.equalsIgnoreCase("span")) {
805            startCssStyle(mSpannableStringBuilder, attributes);
806        } else if (tag.equalsIgnoreCase("strong")) {
807            start(mSpannableStringBuilder, new Bold());
808        } else if (tag.equalsIgnoreCase("b")) {
809            start(mSpannableStringBuilder, new Bold());
810        } else if (tag.equalsIgnoreCase("em")) {
811            start(mSpannableStringBuilder, new Italic());
812        } else if (tag.equalsIgnoreCase("cite")) {
813            start(mSpannableStringBuilder, new Italic());
814        } else if (tag.equalsIgnoreCase("dfn")) {
815            start(mSpannableStringBuilder, new Italic());
816        } else if (tag.equalsIgnoreCase("i")) {
817            start(mSpannableStringBuilder, new Italic());
818        } else if (tag.equalsIgnoreCase("big")) {
819            start(mSpannableStringBuilder, new Big());
820        } else if (tag.equalsIgnoreCase("small")) {
821            start(mSpannableStringBuilder, new Small());
822        } else if (tag.equalsIgnoreCase("font")) {
823            startFont(mSpannableStringBuilder, attributes);
824        } else if (tag.equalsIgnoreCase("blockquote")) {
825            startBlockquote(mSpannableStringBuilder, attributes);
826        } else if (tag.equalsIgnoreCase("tt")) {
827            start(mSpannableStringBuilder, new Monospace());
828        } else if (tag.equalsIgnoreCase("a")) {
829            startA(mSpannableStringBuilder, attributes);
830        } else if (tag.equalsIgnoreCase("u")) {
831            start(mSpannableStringBuilder, new Underline());
832        } else if (tag.equalsIgnoreCase("del")) {
833            start(mSpannableStringBuilder, new Strikethrough());
834        } else if (tag.equalsIgnoreCase("s")) {
835            start(mSpannableStringBuilder, new Strikethrough());
836        } else if (tag.equalsIgnoreCase("strike")) {
837            start(mSpannableStringBuilder, new Strikethrough());
838        } else if (tag.equalsIgnoreCase("sup")) {
839            start(mSpannableStringBuilder, new Super());
840        } else if (tag.equalsIgnoreCase("sub")) {
841            start(mSpannableStringBuilder, new Sub());
842        } else if (tag.length() == 2 &&
843                Character.toLowerCase(tag.charAt(0)) == 'h' &&
844                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
845            startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
846        } else if (tag.equalsIgnoreCase("img")) {
847            startImg(mSpannableStringBuilder, attributes, mImageGetter);
848        } else if (mTagHandler != null) {
849            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
850        }
851    }
852
853    private void handleEndTag(String tag) {
854        if (tag.equalsIgnoreCase("br")) {
855            handleBr(mSpannableStringBuilder);
856        } else if (tag.equalsIgnoreCase("p")) {
857            endCssStyle(mSpannableStringBuilder);
858            endBlockElement(mSpannableStringBuilder);
859        } else if (tag.equalsIgnoreCase("ul")) {
860            endBlockElement(mSpannableStringBuilder);
861        } else if (tag.equalsIgnoreCase("li")) {
862            endLi(mSpannableStringBuilder);
863        } else if (tag.equalsIgnoreCase("div")) {
864            endBlockElement(mSpannableStringBuilder);
865        } else if (tag.equalsIgnoreCase("span")) {
866            endCssStyle(mSpannableStringBuilder);
867        } else if (tag.equalsIgnoreCase("strong")) {
868            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
869        } else if (tag.equalsIgnoreCase("b")) {
870            end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
871        } else if (tag.equalsIgnoreCase("em")) {
872            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
873        } else if (tag.equalsIgnoreCase("cite")) {
874            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
875        } else if (tag.equalsIgnoreCase("dfn")) {
876            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
877        } else if (tag.equalsIgnoreCase("i")) {
878            end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
879        } else if (tag.equalsIgnoreCase("big")) {
880            end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
881        } else if (tag.equalsIgnoreCase("small")) {
882            end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
883        } else if (tag.equalsIgnoreCase("font")) {
884            endFont(mSpannableStringBuilder);
885        } else if (tag.equalsIgnoreCase("blockquote")) {
886            endBlockquote(mSpannableStringBuilder);
887        } else if (tag.equalsIgnoreCase("tt")) {
888            end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
889        } else if (tag.equalsIgnoreCase("a")) {
890            endA(mSpannableStringBuilder);
891        } else if (tag.equalsIgnoreCase("u")) {
892            end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
893        } else if (tag.equalsIgnoreCase("del")) {
894            end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
895        } else if (tag.equalsIgnoreCase("s")) {
896            end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
897        } else if (tag.equalsIgnoreCase("strike")) {
898            end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
899        } else if (tag.equalsIgnoreCase("sup")) {
900            end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
901        } else if (tag.equalsIgnoreCase("sub")) {
902            end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
903        } else if (tag.length() == 2 &&
904                Character.toLowerCase(tag.charAt(0)) == 'h' &&
905                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
906            endHeading(mSpannableStringBuilder);
907        } else if (mTagHandler != null) {
908            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
909        }
910    }
911
912    private int getMarginParagraph() {
913        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
914    }
915
916    private int getMarginHeading() {
917        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
918    }
919
920    private int getMarginListItem() {
921        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
922    }
923
924    private int getMarginList() {
925        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
926    }
927
928    private int getMarginDiv() {
929        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
930    }
931
932    private int getMarginBlockquote() {
933        return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE);
934    }
935
936    /**
937     * Returns the minimum number of newline characters needed before and after a given block-level
938     * element.
939     *
940     * @param flag the corresponding option flag defined in {@link Html} of a block-level element
941     */
942    private int getMargin(int flag) {
943        if ((flag & mFlags) != 0) {
944            return 1;
945        }
946        return 2;
947    }
948
949    private static void appendNewlines(Editable text, int minNewline) {
950        final int len = text.length();
951
952        if (len == 0) {
953            return;
954        }
955
956        int existingNewlines = 0;
957        for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
958            existingNewlines++;
959        }
960
961        for (int j = existingNewlines; j < minNewline; j++) {
962            text.append("\n");
963        }
964    }
965
966    private static void startBlockElement(Editable text, Attributes attributes, int margin) {
967        final int len = text.length();
968        if (margin > 0) {
969            appendNewlines(text, margin);
970            start(text, new Newline(margin));
971        }
972
973        String style = attributes.getValue("", "style");
974        if (style != null) {
975            Matcher m = getTextAlignPattern().matcher(style);
976            if (m.find()) {
977                String alignment = m.group(1);
978                if (alignment.equalsIgnoreCase("start")) {
979                    start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
980                } else if (alignment.equalsIgnoreCase("center")) {
981                    start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
982                } else if (alignment.equalsIgnoreCase("end")) {
983                    start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
984                }
985            }
986        }
987    }
988
989    private static void endBlockElement(Editable text) {
990        Newline n = getLast(text, Newline.class);
991        if (n != null) {
992            appendNewlines(text, n.mNumNewlines);
993            text.removeSpan(n);
994        }
995
996        Alignment a = getLast(text, Alignment.class);
997        if (a != null) {
998            setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
999        }
1000    }
1001
1002    private static void handleBr(Editable text) {
1003        text.append('\n');
1004    }
1005
1006    private void startLi(Editable text, Attributes attributes) {
1007        startBlockElement(text, attributes, getMarginListItem());
1008        start(text, new Bullet());
1009        startCssStyle(text, attributes);
1010    }
1011
1012    private static void endLi(Editable text) {
1013        endCssStyle(text);
1014        endBlockElement(text);
1015        end(text, Bullet.class, new BulletSpan());
1016    }
1017
1018    private void startBlockquote(Editable text, Attributes attributes) {
1019        startBlockElement(text, attributes, getMarginBlockquote());
1020        start(text, new Blockquote());
1021    }
1022
1023    private static void endBlockquote(Editable text) {
1024        endBlockElement(text);
1025        end(text, Blockquote.class, new QuoteSpan());
1026    }
1027
1028    private void startHeading(Editable text, Attributes attributes, int level) {
1029        startBlockElement(text, attributes, getMarginHeading());
1030        start(text, new Heading(level));
1031    }
1032
1033    private static void endHeading(Editable text) {
1034        // RelativeSizeSpan and StyleSpan are CharacterStyles
1035        // Their ranges should not include the newlines at the end
1036        Heading h = getLast(text, Heading.class);
1037        if (h != null) {
1038            setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
1039                    new StyleSpan(Typeface.BOLD));
1040        }
1041
1042        endBlockElement(text);
1043    }
1044
1045    private static <T> T getLast(Spanned text, Class<T> kind) {
1046        /*
1047         * This knows that the last returned object from getSpans()
1048         * will be the most recently added.
1049         */
1050        T[] objs = text.getSpans(0, text.length(), kind);
1051
1052        if (objs.length == 0) {
1053            return null;
1054        } else {
1055            return objs[objs.length - 1];
1056        }
1057    }
1058
1059    private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
1060        int where = text.getSpanStart(mark);
1061        text.removeSpan(mark);
1062        int len = text.length();
1063        if (where != len) {
1064            for (Object span : spans) {
1065                text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1066            }
1067        }
1068    }
1069
1070    private static void start(Editable text, Object mark) {
1071        int len = text.length();
1072        text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1073    }
1074
1075    private static void end(Editable text, Class kind, Object repl) {
1076        int len = text.length();
1077        Object obj = getLast(text, kind);
1078        if (obj != null) {
1079            setSpanFromMark(text, obj, repl);
1080        }
1081    }
1082
1083    private void startCssStyle(Editable text, Attributes attributes) {
1084        String style = attributes.getValue("", "style");
1085        if (style != null) {
1086            Matcher m = getForegroundColorPattern().matcher(style);
1087            if (m.find()) {
1088                int c = getHtmlColor(m.group(1));
1089                if (c != -1) {
1090                    start(text, new Foreground(c | 0xFF000000));
1091                }
1092            }
1093
1094            m = getBackgroundColorPattern().matcher(style);
1095            if (m.find()) {
1096                int c = getHtmlColor(m.group(1));
1097                if (c != -1) {
1098                    start(text, new Background(c | 0xFF000000));
1099                }
1100            }
1101
1102            m = getTextDecorationPattern().matcher(style);
1103            if (m.find()) {
1104                String textDecoration = m.group(1);
1105                if (textDecoration.equalsIgnoreCase("line-through")) {
1106                    start(text, new Strikethrough());
1107                }
1108            }
1109        }
1110    }
1111
1112    private static void endCssStyle(Editable text) {
1113        Strikethrough s = getLast(text, Strikethrough.class);
1114        if (s != null) {
1115            setSpanFromMark(text, s, new StrikethroughSpan());
1116        }
1117
1118        Background b = getLast(text, Background.class);
1119        if (b != null) {
1120            setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
1121        }
1122
1123        Foreground f = getLast(text, Foreground.class);
1124        if (f != null) {
1125            setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
1126        }
1127    }
1128
1129    private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
1130        String src = attributes.getValue("", "src");
1131        Drawable d = null;
1132
1133        if (img != null) {
1134            d = img.getDrawable(src);
1135        }
1136
1137        if (d == null) {
1138            d = Resources.getSystem().
1139                    getDrawable(com.android.internal.R.drawable.unknown_image);
1140            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
1141        }
1142
1143        int len = text.length();
1144        text.append("\uFFFC");
1145
1146        text.setSpan(new ImageSpan(d, src), len, text.length(),
1147                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1148    }
1149
1150    private void startFont(Editable text, Attributes attributes) {
1151        String color = attributes.getValue("", "color");
1152        String face = attributes.getValue("", "face");
1153
1154        if (!TextUtils.isEmpty(color)) {
1155            int c = getHtmlColor(color);
1156            if (c != -1) {
1157                start(text, new Foreground(c | 0xFF000000));
1158            }
1159        }
1160
1161        if (!TextUtils.isEmpty(face)) {
1162            start(text, new Font(face));
1163        }
1164    }
1165
1166    private static void endFont(Editable text) {
1167        Font font = getLast(text, Font.class);
1168        if (font != null) {
1169            setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
1170        }
1171
1172        Foreground foreground = getLast(text, Foreground.class);
1173        if (foreground != null) {
1174            setSpanFromMark(text, foreground,
1175                    new ForegroundColorSpan(foreground.mForegroundColor));
1176        }
1177    }
1178
1179    private static void startA(Editable text, Attributes attributes) {
1180        String href = attributes.getValue("", "href");
1181        start(text, new Href(href));
1182    }
1183
1184    private static void endA(Editable text) {
1185        Href h = getLast(text, Href.class);
1186        if (h != null) {
1187            if (h.mHref != null) {
1188                setSpanFromMark(text, h, new URLSpan((h.mHref)));
1189            }
1190        }
1191    }
1192
1193    private int getHtmlColor(String color) {
1194        if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
1195                == Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
1196            Integer i = sColorMap.get(color.toLowerCase(Locale.US));
1197            if (i != null) {
1198                return i;
1199            }
1200        }
1201        return Color.getHtmlColor(color);
1202    }
1203
1204    public void setDocumentLocator(Locator locator) {
1205    }
1206
1207    public void startDocument() throws SAXException {
1208    }
1209
1210    public void endDocument() throws SAXException {
1211    }
1212
1213    public void startPrefixMapping(String prefix, String uri) throws SAXException {
1214    }
1215
1216    public void endPrefixMapping(String prefix) throws SAXException {
1217    }
1218
1219    public void startElement(String uri, String localName, String qName, Attributes attributes)
1220            throws SAXException {
1221        handleStartTag(localName, attributes);
1222    }
1223
1224    public void endElement(String uri, String localName, String qName) throws SAXException {
1225        handleEndTag(localName);
1226    }
1227
1228    public void characters(char ch[], int start, int length) throws SAXException {
1229        StringBuilder sb = new StringBuilder();
1230
1231        /*
1232         * Ignore whitespace that immediately follows other whitespace;
1233         * newlines count as spaces.
1234         */
1235
1236        for (int i = 0; i < length; i++) {
1237            char c = ch[i + start];
1238
1239            if (c == ' ' || c == '\n') {
1240                char pred;
1241                int len = sb.length();
1242
1243                if (len == 0) {
1244                    len = mSpannableStringBuilder.length();
1245
1246                    if (len == 0) {
1247                        pred = '\n';
1248                    } else {
1249                        pred = mSpannableStringBuilder.charAt(len - 1);
1250                    }
1251                } else {
1252                    pred = sb.charAt(len - 1);
1253                }
1254
1255                if (pred != ' ' && pred != '\n') {
1256                    sb.append(' ');
1257                }
1258            } else {
1259                sb.append(c);
1260            }
1261        }
1262
1263        mSpannableStringBuilder.append(sb);
1264    }
1265
1266    public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
1267    }
1268
1269    public void processingInstruction(String target, String data) throws SAXException {
1270    }
1271
1272    public void skippedEntity(String name) throws SAXException {
1273    }
1274
1275    private static class Bold { }
1276    private static class Italic { }
1277    private static class Underline { }
1278    private static class Strikethrough { }
1279    private static class Big { }
1280    private static class Small { }
1281    private static class Monospace { }
1282    private static class Blockquote { }
1283    private static class Super { }
1284    private static class Sub { }
1285    private static class Bullet { }
1286
1287    private static class Font {
1288        public String mFace;
1289
1290        public Font(String face) {
1291            mFace = face;
1292        }
1293    }
1294
1295    private static class Href {
1296        public String mHref;
1297
1298        public Href(String href) {
1299            mHref = href;
1300        }
1301    }
1302
1303    private static class Foreground {
1304        private int mForegroundColor;
1305
1306        public Foreground(int foregroundColor) {
1307            mForegroundColor = foregroundColor;
1308        }
1309    }
1310
1311    private static class Background {
1312        private int mBackgroundColor;
1313
1314        public Background(int backgroundColor) {
1315            mBackgroundColor = backgroundColor;
1316        }
1317    }
1318
1319    private static class Heading {
1320        private int mLevel;
1321
1322        public Heading(int level) {
1323            mLevel = level;
1324        }
1325    }
1326
1327    private static class Newline {
1328        private int mNumNewlines;
1329
1330        public Newline(int numNewlines) {
1331            mNumNewlines = numNewlines;
1332        }
1333    }
1334
1335    private static class Alignment {
1336        private Layout.Alignment mAlignment;
1337
1338        public Alignment(Layout.Alignment alignment) {
1339            mAlignment = alignment;
1340        }
1341    }
1342}
1343