blob: 18f8db23db744bbd78640f6b4572c37a04aeff39 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Mathew Inwoodefeab842018-08-14 15:21:30 +010019import android.annotation.UnsupportedAppUsage;
Daniel Ub138e282015-12-23 10:43:46 +000020import android.app.ActivityThread;
21import android.app.Application;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.content.res.Resources;
Daniel U2102de92015-12-17 15:00:27 +000023import android.graphics.Color;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.graphics.Typeface;
25import android.graphics.drawable.Drawable;
The Android Open Source Project10592532009-03-18 17:39:46 -070026import android.text.style.AbsoluteSizeSpan;
27import android.text.style.AlignmentSpan;
Daniel U5c02d732015-12-22 15:00:48 +000028import android.text.style.BackgroundColorSpan;
Daniel U2102de92015-12-17 15:00:27 +000029import android.text.style.BulletSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.text.style.CharacterStyle;
31import android.text.style.ForegroundColorSpan;
32import android.text.style.ImageSpan;
33import android.text.style.ParagraphStyle;
34import android.text.style.QuoteSpan;
35import android.text.style.RelativeSizeSpan;
36import android.text.style.StrikethroughSpan;
37import android.text.style.StyleSpan;
38import android.text.style.SubscriptSpan;
39import android.text.style.SuperscriptSpan;
40import android.text.style.TypefaceSpan;
41import android.text.style.URLSpan;
42import android.text.style.UnderlineSpan;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080043
Aurimas Liutikas4037d512016-10-11 17:20:06 -070044import org.ccil.cowan.tagsoup.HTMLSchema;
45import org.ccil.cowan.tagsoup.Parser;
46import org.xml.sax.Attributes;
47import org.xml.sax.ContentHandler;
48import org.xml.sax.InputSource;
49import org.xml.sax.Locator;
50import org.xml.sax.SAXException;
51import org.xml.sax.XMLReader;
52
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053import java.io.IOException;
54import java.io.StringReader;
Daniel U1eab97a2016-02-03 14:29:09 +000055import java.util.HashMap;
56import java.util.Locale;
57import java.util.Map;
Daniel Ucf1fa602016-01-21 13:52:07 +000058import java.util.regex.Matcher;
59import java.util.regex.Pattern;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060
61/**
62 * This class processes HTML strings into displayable styled text.
63 * Not all HTML tags are supported.
64 */
65public class Html {
66 /**
67 * Retrieves images for HTML <img> tags.
68 */
69 public static interface ImageGetter {
70 /**
Mark Dolinerd0646dc2014-08-27 16:04:02 -070071 * This method is called when the HTML parser encounters an
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072 * &lt;img&gt; tag. The <code>source</code> argument is the
73 * string from the "src" attribute; the return value should be
74 * a Drawable representation of the image or <code>null</code>
75 * for a generic replacement image. Make sure you call
76 * setBounds() on your Drawable if it doesn't already have
77 * its bounds set.
78 */
79 public Drawable getDrawable(String source);
80 }
81
82 /**
83 * Is notified when HTML tags are encountered that the parser does
84 * not know how to interpret.
85 */
86 public static interface TagHandler {
87 /**
88 * This method will be called whenn the HTML parser encounters
89 * a tag that it does not know how to interpret.
90 */
91 public void handleTag(boolean opening, String tag,
92 Editable output, XMLReader xmlReader);
93 }
94
Daniel U2102de92015-12-17 15:00:27 +000095 /**
96 * Option for {@link #toHtml(Spanned, int)}: Wrap consecutive lines of text delimited by '\n'
97 * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored.
98 */
99 public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE = 0x00000000;
100
101 /**
102 * Option for {@link #toHtml(Spanned, int)}: Wrap each line of text delimited by '\n' inside a
103 * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be
104 * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element.
105 */
106 public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL = 0x00000001;
107
108 /**
109 * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with
110 * one newline character by default.
111 */
112 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH = 0x00000001;
113
114 /**
115 * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from
116 * other texts with one newline character by default.
117 */
118 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING = 0x00000002;
119
120 /**
121 * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts
122 * with one newline character by default.
123 */
124 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM = 0x00000004;
125
126 /**
127 * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts
128 * with one newline character by default.
129 */
130 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST = 0x00000008;
131
132 /**
133 * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts
134 * with one newline character by default.
135 */
136 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV = 0x00000010;
137
138 /**
139 * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other
140 * texts with one newline character by default.
141 */
142 public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE = 0x00000020;
143
144 /**
145 * Flag indicating that CSS color values should be used instead of those defined in
146 * {@link Color}.
147 */
148 public static final int FROM_HTML_OPTION_USE_CSS_COLORS = 0x00000100;
149
150 /**
151 * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
152 * elements with blank lines (two newline characters) in between. This is the legacy behavior
153 * prior to N.
154 */
155 public static final int FROM_HTML_MODE_LEGACY = 0x00000000;
156
157 /**
158 * Flags for {@link #fromHtml(String, int, ImageGetter, TagHandler)}: Separate block-level
159 * elements with line breaks (single newline character) in between. This inverts the
160 * {@link Spanned} to HTML string conversion done with the option
161 * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}.
162 */
163 public static final int FROM_HTML_MODE_COMPACT =
164 FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH
165 | FROM_HTML_SEPARATOR_LINE_BREAK_HEADING
166 | FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM
167 | FROM_HTML_SEPARATOR_LINE_BREAK_LIST
168 | FROM_HTML_SEPARATOR_LINE_BREAK_DIV
169 | FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE;
170
Daniel Uca124ab2015-12-23 12:32:03 +0000171 /**
172 * The bit which indicates if lines delimited by '\n' will be grouped into &lt;p&gt; elements.
173 */
174 private static final int TO_HTML_PARAGRAPH_FLAG = 0x00000001;
175
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176 private Html() { }
177
178 /**
Daniel U2102de92015-12-17 15:00:27 +0000179 * Returns displayable styled text from the provided HTML string with the legacy flags
180 * {@link #FROM_HTML_MODE_LEGACY}.
181 *
182 * @deprecated use {@link #fromHtml(String, int)} instead.
183 */
184 @Deprecated
185 public static Spanned fromHtml(String source) {
186 return fromHtml(source, FROM_HTML_MODE_LEGACY, null, null);
187 }
188
189 /**
190 * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
191 * HTML will display as a generic replacement image which your program can then go through and
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800192 * replace with real images.
193 *
194 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
195 */
Daniel U2102de92015-12-17 15:00:27 +0000196 public static Spanned fromHtml(String source, int flags) {
197 return fromHtml(source, flags, null, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 }
199
200 /**
201 * Lazy initialization holder for HTML parser. This class will
202 * a) be preloaded by the zygote, or b) not loaded until absolutely
203 * necessary.
204 */
205 private static class HtmlParser {
206 private static final HTMLSchema schema = new HTMLSchema();
207 }
208
209 /**
Daniel U2102de92015-12-17 15:00:27 +0000210 * Returns displayable styled text from the provided HTML string with the legacy flags
211 * {@link #FROM_HTML_MODE_LEGACY}.
212 *
213 * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead.
214 */
215 @Deprecated
216 public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
217 return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
218 }
219
220 /**
221 * Returns displayable styled text from the provided HTML string. Any &lt;img&gt; tags in the
222 * HTML will use the specified ImageGetter to request a representation of the image (use null
223 * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if
224 * you don't want this).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800225 *
226 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
227 */
Daniel U2102de92015-12-17 15:00:27 +0000228 public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter,
229 TagHandler tagHandler) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230 Parser parser = new Parser();
231 try {
232 parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
233 } catch (org.xml.sax.SAXNotRecognizedException e) {
234 // Should not happen.
235 throw new RuntimeException(e);
236 } catch (org.xml.sax.SAXNotSupportedException e) {
237 // Should not happen.
238 throw new RuntimeException(e);
239 }
240
241 HtmlToSpannedConverter converter =
Daniel U2102de92015-12-17 15:00:27 +0000242 new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800243 return converter.convert();
244 }
245
246 /**
Daniel U2102de92015-12-17 15:00:27 +0000247 * @deprecated use {@link #toHtml(Spanned, int)} instead.
248 */
249 @Deprecated
250 public static String toHtml(Spanned text) {
251 return toHtml(text, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
252 }
253
254 /**
Raph Levienc55277d2015-07-10 17:11:39 -0700255 * Returns an HTML representation of the provided Spanned text. A best effort is
256 * made to add HTML tags corresponding to spans. Also note that HTML metacharacters
257 * (such as "&lt;" and "&amp;") within the input text are escaped.
258 *
259 * @param text input text to convert
Daniel U2102de92015-12-17 15:00:27 +0000260 * @param option one of {@link #TO_HTML_PARAGRAPH_LINES_CONSECUTIVE} or
261 * {@link #TO_HTML_PARAGRAPH_LINES_INDIVIDUAL}
Raph Levienc55277d2015-07-10 17:11:39 -0700262 * @return string containing input converted to HTML
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 */
Daniel U2102de92015-12-17 15:00:27 +0000264 public static String toHtml(Spanned text, int option) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800265 StringBuilder out = new StringBuilder();
Daniel Uca124ab2015-12-23 12:32:03 +0000266 withinHtml(out, text, option);
The Android Open Source Project10592532009-03-18 17:39:46 -0700267 return out.toString();
268 }
269
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700270 /**
271 * Returns an HTML escaped representation of the given plain text.
272 */
273 public static String escapeHtml(CharSequence text) {
274 StringBuilder out = new StringBuilder();
275 withinStyle(out, text, 0, text.length());
276 return out.toString();
277 }
278
Daniel Uca124ab2015-12-23 12:32:03 +0000279 private static void withinHtml(StringBuilder out, Spanned text, int option) {
280 if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
281 encodeTextAlignmentByDiv(out, text, option);
282 return;
283 }
284
285 withinDiv(out, text, 0, text.length(), option);
286 }
287
288 private static void encodeTextAlignmentByDiv(StringBuilder out, Spanned text, int option) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 int len = text.length();
290
291 int next;
Daniel U5c02d732015-12-22 15:00:48 +0000292 for (int i = 0; i < len; i = next) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700293 next = text.nextSpanTransition(i, len, ParagraphStyle.class);
294 ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
Satoshi Kataoka32048302009-03-24 19:48:28 -0700295 String elements = " ";
Eric Fischer00ba7662009-03-25 16:08:50 -0700296 boolean needDiv = false;
297
The Android Open Source Project10592532009-03-18 17:39:46 -0700298 for(int j = 0; j < style.length; j++) {
299 if (style[j] instanceof AlignmentSpan) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800300 Layout.Alignment align =
The Android Open Source Project10592532009-03-18 17:39:46 -0700301 ((AlignmentSpan) style[j]).getAlignment();
Eric Fischer00ba7662009-03-25 16:08:50 -0700302 needDiv = true;
The Android Open Source Project10592532009-03-18 17:39:46 -0700303 if (align == Layout.Alignment.ALIGN_CENTER) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700304 elements = "align=\"center\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700305 } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700306 elements = "align=\"right\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700307 } else {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700308 elements = "align=\"left\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700309 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700310 }
311 }
Eric Fischer00ba7662009-03-25 16:08:50 -0700312 if (needDiv) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800313 out.append("<div ").append(elements).append(">");
The Android Open Source Project10592532009-03-18 17:39:46 -0700314 }
315
Daniel Uca124ab2015-12-23 12:32:03 +0000316 withinDiv(out, text, i, next, option);
The Android Open Source Project10592532009-03-18 17:39:46 -0700317
Eric Fischer00ba7662009-03-25 16:08:50 -0700318 if (needDiv) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700319 out.append("</div>");
320 }
321 }
322 }
323
Daniel Uca124ab2015-12-23 12:32:03 +0000324 private static void withinDiv(StringBuilder out, Spanned text, int start, int end,
325 int option) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700326 int next;
327 for (int i = start; i < end; i = next) {
328 next = text.nextSpanTransition(i, end, QuoteSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329 QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
330
Romain Guya8f6d5f2012-11-27 11:12:26 -0800331 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800332 out.append("<blockquote>");
333 }
334
Daniel Uca124ab2015-12-23 12:32:03 +0000335 withinBlockquote(out, text, i, next, option);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800336
Romain Guya8f6d5f2012-11-27 11:12:26 -0800337 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338 out.append("</blockquote>\n");
339 }
340 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800341 }
342
Daniel Uca124ab2015-12-23 12:32:03 +0000343 private static String getTextDirection(Spanned text, int start, int end) {
Roozbeh Pournaderaa0af8e2016-12-13 17:19:58 -0800344 if (TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(text, start, end - start)) {
345 return " dir=\"rtl\"";
346 } else {
347 return " dir=\"ltr\"";
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800348 }
349 }
350
Daniel U4a70f092016-01-29 16:58:36 +0000351 private static String getTextStyles(Spanned text, int start, int end,
352 boolean forceNoVerticalMargin, boolean includeTextAlign) {
353 String margin = null;
354 String textAlign = null;
Daniel Uca124ab2015-12-23 12:32:03 +0000355
Daniel U4a70f092016-01-29 16:58:36 +0000356 if (forceNoVerticalMargin) {
357 margin = "margin-top:0; margin-bottom:0;";
358 }
359 if (includeTextAlign) {
360 final AlignmentSpan[] alignmentSpans = text.getSpans(start, end, AlignmentSpan.class);
361
362 // Only use the last AlignmentSpan with flag SPAN_PARAGRAPH
363 for (int i = alignmentSpans.length - 1; i >= 0; i--) {
364 AlignmentSpan s = alignmentSpans[i];
365 if ((text.getSpanFlags(s) & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH) {
366 final Layout.Alignment alignment = s.getAlignment();
367 if (alignment == Layout.Alignment.ALIGN_NORMAL) {
368 textAlign = "text-align:start;";
369 } else if (alignment == Layout.Alignment.ALIGN_CENTER) {
370 textAlign = "text-align:center;";
371 } else if (alignment == Layout.Alignment.ALIGN_OPPOSITE) {
372 textAlign = "text-align:end;";
373 }
374 break;
375 }
Daniel Uca124ab2015-12-23 12:32:03 +0000376 }
377 }
378
Daniel U4a70f092016-01-29 16:58:36 +0000379 if (margin == null && textAlign == null) {
380 return "";
381 }
382
383 final StringBuilder style = new StringBuilder(" style=\"");
384 if (margin != null && textAlign != null) {
385 style.append(margin).append(" ").append(textAlign);
386 } else if (margin != null) {
387 style.append(margin);
388 } else if (textAlign != null) {
389 style.append(textAlign);
390 }
391
392 return style.append("\"").toString();
Daniel Uca124ab2015-12-23 12:32:03 +0000393 }
394
395 private static void withinBlockquote(StringBuilder out, Spanned text, int start, int end,
396 int option) {
397 if ((option & TO_HTML_PARAGRAPH_FLAG) == TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) {
398 withinBlockquoteConsecutive(out, text, start, end);
399 } else {
400 withinBlockquoteIndividual(out, text, start, end);
401 }
402 }
403
404 private static void withinBlockquoteIndividual(StringBuilder out, Spanned text, int start,
405 int end) {
406 boolean isInList = false;
407 int next;
408 for (int i = start; i <= end; i = next) {
409 next = TextUtils.indexOf(text, '\n', i, end);
410 if (next < 0) {
411 next = end;
412 }
413
Daniel U4a70f092016-01-29 16:58:36 +0000414 if (next == i) {
415 if (isInList) {
416 // Current paragraph is no longer a list item; close the previously opened list
417 isInList = false;
418 out.append("</ul>\n");
Daniel Uca124ab2015-12-23 12:32:03 +0000419 }
Daniel U4a70f092016-01-29 16:58:36 +0000420 out.append("<br>\n");
Daniel Uca124ab2015-12-23 12:32:03 +0000421 } else {
Daniel U4a70f092016-01-29 16:58:36 +0000422 boolean isListItem = false;
423 ParagraphStyle[] paragraphStyles = text.getSpans(i, next, ParagraphStyle.class);
424 for (ParagraphStyle paragraphStyle : paragraphStyles) {
425 final int spanFlags = text.getSpanFlags(paragraphStyle);
426 if ((spanFlags & Spanned.SPAN_PARAGRAPH) == Spanned.SPAN_PARAGRAPH
427 && paragraphStyle instanceof BulletSpan) {
428 isListItem = true;
429 break;
430 }
431 }
432
433 if (isListItem && !isInList) {
434 // Current paragraph is the first item in a list
435 isInList = true;
436 out.append("<ul")
437 .append(getTextStyles(text, i, next, true, false))
438 .append(">\n");
439 }
440
441 if (isInList && !isListItem) {
442 // Current paragraph is no longer a list item; close the previously opened list
443 isInList = false;
444 out.append("</ul>\n");
445 }
446
447 String tagType = isListItem ? "li" : "p";
448 out.append("<").append(tagType)
449 .append(getTextDirection(text, i, next))
450 .append(getTextStyles(text, i, next, !isListItem, true))
451 .append(">");
452
Daniel Uca124ab2015-12-23 12:32:03 +0000453 withinParagraph(out, text, i, next);
Daniel Uca124ab2015-12-23 12:32:03 +0000454
Daniel U4a70f092016-01-29 16:58:36 +0000455 out.append("</");
456 out.append(tagType);
457 out.append(">\n");
Daniel Uca124ab2015-12-23 12:32:03 +0000458
Daniel U4a70f092016-01-29 16:58:36 +0000459 if (next == end && isInList) {
460 isInList = false;
461 out.append("</ul>\n");
462 }
Daniel Uca124ab2015-12-23 12:32:03 +0000463 }
464
465 next++;
466 }
467 }
468
469 private static void withinBlockquoteConsecutive(StringBuilder out, Spanned text, int start,
470 int end) {
471 out.append("<p").append(getTextDirection(text, start, end)).append(">");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800472
473 int next;
474 for (int i = start; i < end; i = next) {
475 next = TextUtils.indexOf(text, '\n', i, end);
476 if (next < 0) {
477 next = end;
478 }
479
480 int nl = 0;
481
482 while (next < end && text.charAt(next) == '\n') {
483 nl++;
484 next++;
485 }
486
Daniel Uca124ab2015-12-23 12:32:03 +0000487 withinParagraph(out, text, i, next - nl);
488
489 if (nl == 1) {
490 out.append("<br>\n");
491 } else {
492 for (int j = 2; j < nl; j++) {
493 out.append("<br>");
494 }
495 if (next != end) {
496 /* Paragraph should be closed and reopened */
497 out.append("</p>\n");
498 out.append("<p").append(getTextDirection(text, start, end)).append(">");
499 }
Roozbeh Pournader2243ae12015-03-19 15:36:33 -0700500 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800501 }
502
503 out.append("</p>\n");
504 }
505
Daniel Uca124ab2015-12-23 12:32:03 +0000506 private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800507 int next;
508 for (int i = start; i < end; i = next) {
509 next = text.nextSpanTransition(i, end, CharacterStyle.class);
Daniel U5c02d732015-12-22 15:00:48 +0000510 CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800511
512 for (int j = 0; j < style.length; j++) {
513 if (style[j] instanceof StyleSpan) {
514 int s = ((StyleSpan) style[j]).getStyle();
515
516 if ((s & Typeface.BOLD) != 0) {
517 out.append("<b>");
518 }
519 if ((s & Typeface.ITALIC) != 0) {
520 out.append("<i>");
521 }
522 }
523 if (style[j] instanceof TypefaceSpan) {
524 String s = ((TypefaceSpan) style[j]).getFamily();
525
Raph Levien8e71a392015-05-05 10:13:54 -0700526 if ("monospace".equals(s)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800527 out.append("<tt>");
528 }
529 }
530 if (style[j] instanceof SuperscriptSpan) {
531 out.append("<sup>");
532 }
533 if (style[j] instanceof SubscriptSpan) {
534 out.append("<sub>");
535 }
536 if (style[j] instanceof UnderlineSpan) {
537 out.append("<u>");
538 }
539 if (style[j] instanceof StrikethroughSpan) {
Daniel U5c02d732015-12-22 15:00:48 +0000540 out.append("<span style=\"text-decoration:line-through;\">");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800541 }
542 if (style[j] instanceof URLSpan) {
543 out.append("<a href=\"");
544 out.append(((URLSpan) style[j]).getURL());
545 out.append("\">");
546 }
547 if (style[j] instanceof ImageSpan) {
548 out.append("<img src=\"");
549 out.append(((ImageSpan) style[j]).getSource());
550 out.append("\">");
551
552 // Don't output the dummy character underlying the image.
553 i = next;
554 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700555 if (style[j] instanceof AbsoluteSizeSpan) {
Daniel Ub138e282015-12-23 10:43:46 +0000556 AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
557 float sizeDip = s.getSize();
558 if (!s.getDip()) {
559 Application application = ActivityThread.currentApplication();
560 sizeDip /= application.getResources().getDisplayMetrics().density;
561 }
562
563 // px in CSS is the equivalance of dip in Android
564 out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip));
565 }
566 if (style[j] instanceof RelativeSizeSpan) {
567 float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
568 out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm));
The Android Open Source Project10592532009-03-18 17:39:46 -0700569 }
570 if (style[j] instanceof ForegroundColorSpan) {
Daniel U5c02d732015-12-22 15:00:48 +0000571 int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
572 out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color));
573 }
574 if (style[j] instanceof BackgroundColorSpan) {
575 int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
576 out.append(String.format("<span style=\"background-color:#%06X;\">",
577 0xFFFFFF & color));
The Android Open Source Project10592532009-03-18 17:39:46 -0700578 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800579 }
580
581 withinStyle(out, text, i, next);
582
583 for (int j = style.length - 1; j >= 0; j--) {
Daniel U5c02d732015-12-22 15:00:48 +0000584 if (style[j] instanceof BackgroundColorSpan) {
585 out.append("</span>");
586 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700587 if (style[j] instanceof ForegroundColorSpan) {
Daniel U5c02d732015-12-22 15:00:48 +0000588 out.append("</span>");
The Android Open Source Project10592532009-03-18 17:39:46 -0700589 }
Daniel Ub138e282015-12-23 10:43:46 +0000590 if (style[j] instanceof RelativeSizeSpan) {
591 out.append("</span>");
592 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700593 if (style[j] instanceof AbsoluteSizeSpan) {
Daniel Ub138e282015-12-23 10:43:46 +0000594 out.append("</span>");
The Android Open Source Project10592532009-03-18 17:39:46 -0700595 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800596 if (style[j] instanceof URLSpan) {
597 out.append("</a>");
598 }
599 if (style[j] instanceof StrikethroughSpan) {
Daniel U5c02d732015-12-22 15:00:48 +0000600 out.append("</span>");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800601 }
602 if (style[j] instanceof UnderlineSpan) {
603 out.append("</u>");
604 }
605 if (style[j] instanceof SubscriptSpan) {
606 out.append("</sub>");
607 }
608 if (style[j] instanceof SuperscriptSpan) {
609 out.append("</sup>");
610 }
611 if (style[j] instanceof TypefaceSpan) {
612 String s = ((TypefaceSpan) style[j]).getFamily();
613
614 if (s.equals("monospace")) {
615 out.append("</tt>");
616 }
617 }
618 if (style[j] instanceof StyleSpan) {
619 int s = ((StyleSpan) style[j]).getStyle();
620
621 if ((s & Typeface.BOLD) != 0) {
622 out.append("</b>");
623 }
624 if ((s & Typeface.ITALIC) != 0) {
625 out.append("</i>");
626 }
627 }
628 }
629 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800630 }
631
Mathew Inwoodefeab842018-08-14 15:21:30 +0100632 @UnsupportedAppUsage
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700633 private static void withinStyle(StringBuilder out, CharSequence text,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800634 int start, int end) {
635 for (int i = start; i < end; i++) {
636 char c = text.charAt(i);
637
638 if (c == '<') {
639 out.append("&lt;");
640 } else if (c == '>') {
641 out.append("&gt;");
642 } else if (c == '&') {
643 out.append("&amp;");
Victoria Lease3d476412013-10-29 15:34:51 -0700644 } else if (c >= 0xD800 && c <= 0xDFFF) {
645 if (c < 0xDC00 && i + 1 < end) {
646 char d = text.charAt(i + 1);
647 if (d >= 0xDC00 && d <= 0xDFFF) {
648 i++;
649 int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
650 out.append("&#").append(codepoint).append(";");
651 }
652 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800653 } else if (c > 0x7E || c < ' ') {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800654 out.append("&#").append((int) c).append(";");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800655 } else if (c == ' ') {
656 while (i + 1 < end && text.charAt(i + 1) == ' ') {
657 out.append("&nbsp;");
658 i++;
659 }
660
661 out.append(' ');
662 } else {
663 out.append(c);
664 }
665 }
666 }
667}
668
669class HtmlToSpannedConverter implements ContentHandler {
670
Daniel Ucf1fa602016-01-21 13:52:07 +0000671 private static final float[] HEADING_SIZES = {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800672 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
673 };
674
675 private String mSource;
676 private XMLReader mReader;
677 private SpannableStringBuilder mSpannableStringBuilder;
678 private Html.ImageGetter mImageGetter;
679 private Html.TagHandler mTagHandler;
Daniel U2102de92015-12-17 15:00:27 +0000680 private int mFlags;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800681
Daniel Ucf1fa602016-01-21 13:52:07 +0000682 private static Pattern sTextAlignPattern;
Daniel U8b36c0b2016-02-01 16:09:18 +0000683 private static Pattern sForegroundColorPattern;
684 private static Pattern sBackgroundColorPattern;
685 private static Pattern sTextDecorationPattern;
Daniel Ucf1fa602016-01-21 13:52:07 +0000686
Daniel U1eab97a2016-02-03 14:29:09 +0000687 /**
688 * Name-value mapping of HTML/CSS colors which have different values in {@link Color}.
689 */
690 private static final Map<String, Integer> sColorMap;
691
692 static {
693 sColorMap = new HashMap<>();
694 sColorMap.put("darkgray", 0xFFA9A9A9);
695 sColorMap.put("gray", 0xFF808080);
696 sColorMap.put("lightgray", 0xFFD3D3D3);
697 sColorMap.put("darkgrey", 0xFFA9A9A9);
698 sColorMap.put("grey", 0xFF808080);
699 sColorMap.put("lightgrey", 0xFFD3D3D3);
700 sColorMap.put("green", 0xFF008000);
701 }
702
Daniel Ucf1fa602016-01-21 13:52:07 +0000703 private static Pattern getTextAlignPattern() {
704 if (sTextAlignPattern == null) {
705 sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b");
706 }
707 return sTextAlignPattern;
708 }
709
Daniel U8b36c0b2016-02-01 16:09:18 +0000710 private static Pattern getForegroundColorPattern() {
711 if (sForegroundColorPattern == null) {
712 sForegroundColorPattern = Pattern.compile(
713 "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
714 }
715 return sForegroundColorPattern;
716 }
717
718 private static Pattern getBackgroundColorPattern() {
719 if (sBackgroundColorPattern == null) {
720 sBackgroundColorPattern = Pattern.compile(
721 "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
722 }
723 return sBackgroundColorPattern;
724 }
725
726 private static Pattern getTextDecorationPattern() {
727 if (sTextDecorationPattern == null) {
728 sTextDecorationPattern = Pattern.compile(
729 "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
730 }
731 return sTextDecorationPattern;
732 }
733
Daniel U2102de92015-12-17 15:00:27 +0000734 public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,
735 Html.TagHandler tagHandler, Parser parser, int flags) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800736 mSource = source;
737 mSpannableStringBuilder = new SpannableStringBuilder();
738 mImageGetter = imageGetter;
739 mTagHandler = tagHandler;
740 mReader = parser;
Daniel U2102de92015-12-17 15:00:27 +0000741 mFlags = flags;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800742 }
743
744 public Spanned convert() {
745
746 mReader.setContentHandler(this);
747 try {
748 mReader.parse(new InputSource(new StringReader(mSource)));
749 } catch (IOException e) {
750 // We are reading from a string. There should not be IO problems.
751 throw new RuntimeException(e);
752 } catch (SAXException e) {
753 // TagSoup doesn't throw parse exceptions.
754 throw new RuntimeException(e);
755 }
756
757 // Fix flags and range for paragraph-type markup.
758 Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
759 for (int i = 0; i < obj.length; i++) {
760 int start = mSpannableStringBuilder.getSpanStart(obj[i]);
761 int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
762
763 // If the last line of the range is blank, back off by one.
764 if (end - 2 >= 0) {
765 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
766 mSpannableStringBuilder.charAt(end - 2) == '\n') {
767 end--;
768 }
769 }
770
771 if (end == start) {
772 mSpannableStringBuilder.removeSpan(obj[i]);
773 } else {
774 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
775 }
776 }
777
778 return mSpannableStringBuilder;
779 }
780
781 private void handleStartTag(String tag, Attributes attributes) {
782 if (tag.equalsIgnoreCase("br")) {
783 // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
Daniel Ucf1fa602016-01-21 13:52:07 +0000784 // so we can safely emit the linebreaks when we handle the close tag.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800785 } else if (tag.equalsIgnoreCase("p")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000786 startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
Daniel U8b36c0b2016-02-01 16:09:18 +0000787 startCssStyle(mSpannableStringBuilder, attributes);
788 } else if (tag.equalsIgnoreCase("ul")) {
789 startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
790 } else if (tag.equalsIgnoreCase("li")) {
791 startLi(mSpannableStringBuilder, attributes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800792 } else if (tag.equalsIgnoreCase("div")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000793 startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
Daniel U8b36c0b2016-02-01 16:09:18 +0000794 } else if (tag.equalsIgnoreCase("span")) {
795 startCssStyle(mSpannableStringBuilder, attributes);
Romain Guy94d5e9a2011-08-29 11:12:19 -0700796 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800797 start(mSpannableStringBuilder, new Bold());
798 } else if (tag.equalsIgnoreCase("b")) {
799 start(mSpannableStringBuilder, new Bold());
Romain Guy94d5e9a2011-08-29 11:12:19 -0700800 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800801 start(mSpannableStringBuilder, new Italic());
802 } else if (tag.equalsIgnoreCase("cite")) {
803 start(mSpannableStringBuilder, new Italic());
804 } else if (tag.equalsIgnoreCase("dfn")) {
805 start(mSpannableStringBuilder, new Italic());
806 } else if (tag.equalsIgnoreCase("i")) {
807 start(mSpannableStringBuilder, new Italic());
808 } else if (tag.equalsIgnoreCase("big")) {
809 start(mSpannableStringBuilder, new Big());
810 } else if (tag.equalsIgnoreCase("small")) {
811 start(mSpannableStringBuilder, new Small());
812 } else if (tag.equalsIgnoreCase("font")) {
813 startFont(mSpannableStringBuilder, attributes);
814 } else if (tag.equalsIgnoreCase("blockquote")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000815 startBlockquote(mSpannableStringBuilder, attributes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800816 } else if (tag.equalsIgnoreCase("tt")) {
817 start(mSpannableStringBuilder, new Monospace());
818 } else if (tag.equalsIgnoreCase("a")) {
819 startA(mSpannableStringBuilder, attributes);
820 } else if (tag.equalsIgnoreCase("u")) {
821 start(mSpannableStringBuilder, new Underline());
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -0700822 } else if (tag.equalsIgnoreCase("del")) {
823 start(mSpannableStringBuilder, new Strikethrough());
824 } else if (tag.equalsIgnoreCase("s")) {
825 start(mSpannableStringBuilder, new Strikethrough());
826 } else if (tag.equalsIgnoreCase("strike")) {
827 start(mSpannableStringBuilder, new Strikethrough());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800828 } else if (tag.equalsIgnoreCase("sup")) {
829 start(mSpannableStringBuilder, new Super());
830 } else if (tag.equalsIgnoreCase("sub")) {
831 start(mSpannableStringBuilder, new Sub());
832 } else if (tag.length() == 2 &&
Daniel Ucf1fa602016-01-21 13:52:07 +0000833 Character.toLowerCase(tag.charAt(0)) == 'h' &&
834 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
835 startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 } else if (tag.equalsIgnoreCase("img")) {
837 startImg(mSpannableStringBuilder, attributes, mImageGetter);
838 } else if (mTagHandler != null) {
839 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
840 }
841 }
842
843 private void handleEndTag(String tag) {
844 if (tag.equalsIgnoreCase("br")) {
845 handleBr(mSpannableStringBuilder);
846 } else if (tag.equalsIgnoreCase("p")) {
Daniel U8b36c0b2016-02-01 16:09:18 +0000847 endCssStyle(mSpannableStringBuilder);
Daniel Ucf1fa602016-01-21 13:52:07 +0000848 endBlockElement(mSpannableStringBuilder);
Daniel U8b36c0b2016-02-01 16:09:18 +0000849 } else if (tag.equalsIgnoreCase("ul")) {
850 endBlockElement(mSpannableStringBuilder);
851 } else if (tag.equalsIgnoreCase("li")) {
852 endLi(mSpannableStringBuilder);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800853 } else if (tag.equalsIgnoreCase("div")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000854 endBlockElement(mSpannableStringBuilder);
Daniel U8b36c0b2016-02-01 16:09:18 +0000855 } else if (tag.equalsIgnoreCase("span")) {
856 endCssStyle(mSpannableStringBuilder);
Romain Guydd808c02011-09-06 10:54:46 -0700857 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800858 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
859 } else if (tag.equalsIgnoreCase("b")) {
860 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
Romain Guydd808c02011-09-06 10:54:46 -0700861 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800862 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
863 } else if (tag.equalsIgnoreCase("cite")) {
864 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
865 } else if (tag.equalsIgnoreCase("dfn")) {
866 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
867 } else if (tag.equalsIgnoreCase("i")) {
868 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
869 } else if (tag.equalsIgnoreCase("big")) {
870 end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
871 } else if (tag.equalsIgnoreCase("small")) {
872 end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
873 } else if (tag.equalsIgnoreCase("font")) {
874 endFont(mSpannableStringBuilder);
875 } else if (tag.equalsIgnoreCase("blockquote")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000876 endBlockquote(mSpannableStringBuilder);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800877 } else if (tag.equalsIgnoreCase("tt")) {
Daniel Ucf1fa602016-01-21 13:52:07 +0000878 end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800879 } else if (tag.equalsIgnoreCase("a")) {
880 endA(mSpannableStringBuilder);
881 } else if (tag.equalsIgnoreCase("u")) {
882 end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -0700883 } else if (tag.equalsIgnoreCase("del")) {
884 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
885 } else if (tag.equalsIgnoreCase("s")) {
886 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
887 } else if (tag.equalsIgnoreCase("strike")) {
888 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800889 } else if (tag.equalsIgnoreCase("sup")) {
890 end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
891 } else if (tag.equalsIgnoreCase("sub")) {
892 end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
893 } else if (tag.length() == 2 &&
894 Character.toLowerCase(tag.charAt(0)) == 'h' &&
895 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
Daniel Ucf1fa602016-01-21 13:52:07 +0000896 endHeading(mSpannableStringBuilder);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800897 } else if (mTagHandler != null) {
898 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
899 }
900 }
901
Daniel Ucf1fa602016-01-21 13:52:07 +0000902 private int getMarginParagraph() {
903 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH);
904 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800905
Daniel Ucf1fa602016-01-21 13:52:07 +0000906 private int getMarginHeading() {
907 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
908 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800909
Daniel U8b36c0b2016-02-01 16:09:18 +0000910 private int getMarginListItem() {
911 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);
912 }
913
914 private int getMarginList() {
915 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST);
916 }
917
Daniel Ucf1fa602016-01-21 13:52:07 +0000918 private int getMarginDiv() {
919 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV);
920 }
921
922 private int getMarginBlockquote() {
923 return getMargin(Html.FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE);
924 }
925
926 /**
927 * Returns the minimum number of newline characters needed before and after a given block-level
928 * element.
929 *
930 * @param flag the corresponding option flag defined in {@link Html} of a block-level element
931 */
932 private int getMargin(int flag) {
933 if ((flag & mFlags) != 0) {
934 return 1;
935 }
936 return 2;
937 }
938
939 private static void appendNewlines(Editable text, int minNewline) {
940 final int len = text.length();
941
942 if (len == 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800943 return;
944 }
945
Daniel Ucf1fa602016-01-21 13:52:07 +0000946 int existingNewlines = 0;
947 for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
948 existingNewlines++;
949 }
950
951 for (int j = existingNewlines; j < minNewline; j++) {
952 text.append("\n");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800953 }
954 }
955
Daniel Ucf1fa602016-01-21 13:52:07 +0000956 private static void startBlockElement(Editable text, Attributes attributes, int margin) {
957 final int len = text.length();
958 if (margin > 0) {
959 appendNewlines(text, margin);
Daniel U1eab97a2016-02-03 14:29:09 +0000960 start(text, new Newline(margin));
Daniel Ucf1fa602016-01-21 13:52:07 +0000961 }
962
963 String style = attributes.getValue("", "style");
964 if (style != null) {
965 Matcher m = getTextAlignPattern().matcher(style);
966 if (m.find()) {
967 String alignment = m.group(1);
968 if (alignment.equalsIgnoreCase("start")) {
Daniel U1eab97a2016-02-03 14:29:09 +0000969 start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
Daniel Ucf1fa602016-01-21 13:52:07 +0000970 } else if (alignment.equalsIgnoreCase("center")) {
Daniel U1eab97a2016-02-03 14:29:09 +0000971 start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
Daniel Ucf1fa602016-01-21 13:52:07 +0000972 } else if (alignment.equalsIgnoreCase("end")) {
Daniel U1eab97a2016-02-03 14:29:09 +0000973 start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
Daniel Ucf1fa602016-01-21 13:52:07 +0000974 }
975 }
976 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800977 }
978
Daniel Ucf1fa602016-01-21 13:52:07 +0000979 private static void endBlockElement(Editable text) {
980 Newline n = getLast(text, Newline.class);
981 if (n != null) {
982 appendNewlines(text, n.mNumNewlines);
983 text.removeSpan(n);
984 }
985
986 Alignment a = getLast(text, Alignment.class);
987 if (a != null) {
988 setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
989 }
990 }
991
992 private static void handleBr(Editable text) {
993 text.append('\n');
994 }
995
Daniel U8b36c0b2016-02-01 16:09:18 +0000996 private void startLi(Editable text, Attributes attributes) {
997 startBlockElement(text, attributes, getMarginListItem());
998 start(text, new Bullet());
999 startCssStyle(text, attributes);
1000 }
1001
1002 private static void endLi(Editable text) {
1003 endCssStyle(text);
1004 endBlockElement(text);
1005 end(text, Bullet.class, new BulletSpan());
1006 }
1007
Daniel Ucf1fa602016-01-21 13:52:07 +00001008 private void startBlockquote(Editable text, Attributes attributes) {
1009 startBlockElement(text, attributes, getMarginBlockquote());
1010 start(text, new Blockquote());
1011 }
1012
1013 private static void endBlockquote(Editable text) {
1014 endBlockElement(text);
1015 end(text, Blockquote.class, new QuoteSpan());
1016 }
1017
1018 private void startHeading(Editable text, Attributes attributes, int level) {
1019 startBlockElement(text, attributes, getMarginHeading());
1020 start(text, new Heading(level));
1021 }
1022
1023 private static void endHeading(Editable text) {
1024 // RelativeSizeSpan and StyleSpan are CharacterStyles
1025 // Their ranges should not include the newlines at the end
1026 Heading h = getLast(text, Heading.class);
1027 if (h != null) {
1028 setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
1029 new StyleSpan(Typeface.BOLD));
1030 }
1031
1032 endBlockElement(text);
1033 }
1034
1035 private static <T> T getLast(Spanned text, Class<T> kind) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001036 /*
1037 * This knows that the last returned object from getSpans()
1038 * will be the most recently added.
1039 */
Daniel Ucf1fa602016-01-21 13:52:07 +00001040 T[] objs = text.getSpans(0, text.length(), kind);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001041
1042 if (objs.length == 0) {
1043 return null;
1044 } else {
1045 return objs[objs.length - 1];
1046 }
1047 }
1048
Daniel Ucf1fa602016-01-21 13:52:07 +00001049 private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
1050 int where = text.getSpanStart(mark);
1051 text.removeSpan(mark);
1052 int len = text.length();
1053 if (where != len) {
1054 for (Object span : spans) {
1055 text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1056 }
1057 }
1058 }
1059
1060 private static void start(Editable text, Object mark) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001061 int len = text.length();
Daniel U1eab97a2016-02-03 14:29:09 +00001062 text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001063 }
1064
Daniel Ucf1fa602016-01-21 13:52:07 +00001065 private static void end(Editable text, Class kind, Object repl) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001066 int len = text.length();
1067 Object obj = getLast(text, kind);
Daniel Ucf1fa602016-01-21 13:52:07 +00001068 if (obj != null) {
1069 setSpanFromMark(text, obj, repl);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001070 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001071 }
1072
Daniel U1eab97a2016-02-03 14:29:09 +00001073 private void startCssStyle(Editable text, Attributes attributes) {
Daniel U8b36c0b2016-02-01 16:09:18 +00001074 String style = attributes.getValue("", "style");
1075 if (style != null) {
Daniel U8b36c0b2016-02-01 16:09:18 +00001076 Matcher m = getForegroundColorPattern().matcher(style);
1077 if (m.find()) {
Daniel U1eab97a2016-02-03 14:29:09 +00001078 int c = getHtmlColor(m.group(1));
Daniel U8b36c0b2016-02-01 16:09:18 +00001079 if (c != -1) {
Daniel U1eab97a2016-02-03 14:29:09 +00001080 start(text, new Foreground(c | 0xFF000000));
Daniel U8b36c0b2016-02-01 16:09:18 +00001081 }
1082 }
1083
1084 m = getBackgroundColorPattern().matcher(style);
1085 if (m.find()) {
Daniel U1eab97a2016-02-03 14:29:09 +00001086 int c = getHtmlColor(m.group(1));
Daniel U8b36c0b2016-02-01 16:09:18 +00001087 if (c != -1) {
Daniel U1eab97a2016-02-03 14:29:09 +00001088 start(text, new Background(c | 0xFF000000));
Daniel U8b36c0b2016-02-01 16:09:18 +00001089 }
1090 }
1091
1092 m = getTextDecorationPattern().matcher(style);
1093 if (m.find()) {
1094 String textDecoration = m.group(1);
1095 if (textDecoration.equalsIgnoreCase("line-through")) {
Daniel U1eab97a2016-02-03 14:29:09 +00001096 start(text, new Strikethrough());
Daniel U8b36c0b2016-02-01 16:09:18 +00001097 }
1098 }
1099 }
1100 }
1101
1102 private static void endCssStyle(Editable text) {
1103 Strikethrough s = getLast(text, Strikethrough.class);
1104 if (s != null) {
1105 setSpanFromMark(text, s, new StrikethroughSpan());
1106 }
1107
1108 Background b = getLast(text, Background.class);
1109 if (b != null) {
1110 setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
1111 }
1112
1113 Foreground f = getLast(text, Foreground.class);
1114 if (f != null) {
1115 setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
1116 }
1117 }
1118
Daniel Ucf1fa602016-01-21 13:52:07 +00001119 private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001120 String src = attributes.getValue("", "src");
1121 Drawable d = null;
1122
1123 if (img != null) {
1124 d = img.getDrawable(src);
1125 }
1126
1127 if (d == null) {
1128 d = Resources.getSystem().
1129 getDrawable(com.android.internal.R.drawable.unknown_image);
1130 d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
1131 }
1132
1133 int len = text.length();
1134 text.append("\uFFFC");
1135
1136 text.setSpan(new ImageSpan(d, src), len, text.length(),
1137 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1138 }
1139
Daniel U1eab97a2016-02-03 14:29:09 +00001140 private void startFont(Editable text, Attributes attributes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001141 String color = attributes.getValue("", "color");
1142 String face = attributes.getValue("", "face");
1143
Daniel U1eab97a2016-02-03 14:29:09 +00001144 if (!TextUtils.isEmpty(color)) {
1145 int c = getHtmlColor(color);
1146 if (c != -1) {
1147 start(text, new Foreground(c | 0xFF000000));
1148 }
1149 }
1150
1151 if (!TextUtils.isEmpty(face)) {
1152 start(text, new Font(face));
1153 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001154 }
1155
Daniel Ucf1fa602016-01-21 13:52:07 +00001156 private static void endFont(Editable text) {
Daniel U1eab97a2016-02-03 14:29:09 +00001157 Font font = getLast(text, Font.class);
1158 if (font != null) {
1159 setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
1160 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001161
Daniel U1eab97a2016-02-03 14:29:09 +00001162 Foreground foreground = getLast(text, Foreground.class);
1163 if (foreground != null) {
1164 setSpanFromMark(text, foreground,
1165 new ForegroundColorSpan(foreground.mForegroundColor));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001166 }
1167 }
1168
Daniel Ucf1fa602016-01-21 13:52:07 +00001169 private static void startA(Editable text, Attributes attributes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001170 String href = attributes.getValue("", "href");
Daniel U1eab97a2016-02-03 14:29:09 +00001171 start(text, new Href(href));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001172 }
1173
Daniel Ucf1fa602016-01-21 13:52:07 +00001174 private static void endA(Editable text) {
Daniel Ucf1fa602016-01-21 13:52:07 +00001175 Href h = getLast(text, Href.class);
1176 if (h != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001177 if (h.mHref != null) {
Daniel Ucf1fa602016-01-21 13:52:07 +00001178 setSpanFromMark(text, h, new URLSpan((h.mHref)));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001179 }
1180 }
1181 }
1182
Daniel U1eab97a2016-02-03 14:29:09 +00001183 private int getHtmlColor(String color) {
1184 if ((mFlags & Html.FROM_HTML_OPTION_USE_CSS_COLORS)
1185 == Html.FROM_HTML_OPTION_USE_CSS_COLORS) {
1186 Integer i = sColorMap.get(color.toLowerCase(Locale.US));
1187 if (i != null) {
1188 return i;
1189 }
1190 }
1191 return Color.getHtmlColor(color);
1192 }
1193
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001194 public void setDocumentLocator(Locator locator) {
1195 }
1196
1197 public void startDocument() throws SAXException {
1198 }
1199
1200 public void endDocument() throws SAXException {
1201 }
1202
1203 public void startPrefixMapping(String prefix, String uri) throws SAXException {
1204 }
1205
1206 public void endPrefixMapping(String prefix) throws SAXException {
1207 }
1208
1209 public void startElement(String uri, String localName, String qName, Attributes attributes)
1210 throws SAXException {
1211 handleStartTag(localName, attributes);
1212 }
1213
1214 public void endElement(String uri, String localName, String qName) throws SAXException {
1215 handleEndTag(localName);
1216 }
1217
1218 public void characters(char ch[], int start, int length) throws SAXException {
1219 StringBuilder sb = new StringBuilder();
1220
1221 /*
1222 * Ignore whitespace that immediately follows other whitespace;
1223 * newlines count as spaces.
1224 */
1225
1226 for (int i = 0; i < length; i++) {
1227 char c = ch[i + start];
1228
1229 if (c == ' ' || c == '\n') {
1230 char pred;
1231 int len = sb.length();
1232
1233 if (len == 0) {
1234 len = mSpannableStringBuilder.length();
1235
1236 if (len == 0) {
1237 pred = '\n';
1238 } else {
1239 pred = mSpannableStringBuilder.charAt(len - 1);
1240 }
1241 } else {
1242 pred = sb.charAt(len - 1);
1243 }
1244
1245 if (pred != ' ' && pred != '\n') {
1246 sb.append(' ');
1247 }
1248 } else {
1249 sb.append(c);
1250 }
1251 }
1252
1253 mSpannableStringBuilder.append(sb);
1254 }
1255
1256 public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
1257 }
1258
1259 public void processingInstruction(String target, String data) throws SAXException {
1260 }
1261
1262 public void skippedEntity(String name) throws SAXException {
1263 }
1264
1265 private static class Bold { }
1266 private static class Italic { }
1267 private static class Underline { }
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -07001268 private static class Strikethrough { }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001269 private static class Big { }
1270 private static class Small { }
1271 private static class Monospace { }
1272 private static class Blockquote { }
1273 private static class Super { }
1274 private static class Sub { }
Daniel U8b36c0b2016-02-01 16:09:18 +00001275 private static class Bullet { }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001276
1277 private static class Font {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001278 public String mFace;
1279
Daniel U1eab97a2016-02-03 14:29:09 +00001280 public Font(String face) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001281 mFace = face;
1282 }
1283 }
1284
1285 private static class Href {
1286 public String mHref;
1287
1288 public Href(String href) {
1289 mHref = href;
1290 }
1291 }
1292
Daniel U8b36c0b2016-02-01 16:09:18 +00001293 private static class Foreground {
1294 private int mForegroundColor;
1295
1296 public Foreground(int foregroundColor) {
1297 mForegroundColor = foregroundColor;
1298 }
1299 }
1300
1301 private static class Background {
1302 private int mBackgroundColor;
1303
1304 public Background(int backgroundColor) {
1305 mBackgroundColor = backgroundColor;
1306 }
1307 }
1308
Daniel Ucf1fa602016-01-21 13:52:07 +00001309 private static class Heading {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001310 private int mLevel;
1311
Daniel Ucf1fa602016-01-21 13:52:07 +00001312 public Heading(int level) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001313 mLevel = level;
1314 }
1315 }
Daniel Ucf1fa602016-01-21 13:52:07 +00001316
1317 private static class Newline {
1318 private int mNumNewlines;
1319
1320 public Newline(int numNewlines) {
1321 mNumNewlines = numNewlines;
1322 }
1323 }
1324
1325 private static class Alignment {
1326 private Layout.Alignment mAlignment;
1327
1328 public Alignment(Layout.Alignment alignment) {
1329 mAlignment = alignment;
1330 }
1331 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001332}