blob: 3d8ab1ed2b74a9588c3ddeba099d6974d780849f [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
Romain Guya8f6d5f2012-11-27 11:12:26 -080019import android.graphics.Color;
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -080020import com.android.internal.util.ArrayUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import org.ccil.cowan.tagsoup.HTMLSchema;
22import org.ccil.cowan.tagsoup.Parser;
23import org.xml.sax.Attributes;
24import org.xml.sax.ContentHandler;
25import org.xml.sax.InputSource;
26import org.xml.sax.Locator;
27import org.xml.sax.SAXException;
28import org.xml.sax.XMLReader;
29
Bjorn Bringert9cab7f72009-07-15 22:14:24 +010030import android.content.res.ColorStateList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031import android.content.res.Resources;
32import android.graphics.Typeface;
33import android.graphics.drawable.Drawable;
The Android Open Source Project10592532009-03-18 17:39:46 -070034import android.text.style.AbsoluteSizeSpan;
35import android.text.style.AlignmentSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import android.text.style.CharacterStyle;
37import android.text.style.ForegroundColorSpan;
38import android.text.style.ImageSpan;
39import android.text.style.ParagraphStyle;
40import android.text.style.QuoteSpan;
41import android.text.style.RelativeSizeSpan;
42import android.text.style.StrikethroughSpan;
43import android.text.style.StyleSpan;
44import android.text.style.SubscriptSpan;
45import android.text.style.SuperscriptSpan;
Bjorn Bringert9cab7f72009-07-15 22:14:24 +010046import android.text.style.TextAppearanceSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047import android.text.style.TypefaceSpan;
48import android.text.style.URLSpan;
49import android.text.style.UnderlineSpan;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080050
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import java.io.IOException;
52import java.io.StringReader;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053
54/**
55 * This class processes HTML strings into displayable styled text.
56 * Not all HTML tags are supported.
57 */
58public class Html {
59 /**
60 * Retrieves images for HTML <img> tags.
61 */
62 public static interface ImageGetter {
63 /**
Mark Dolinerd0646dc2014-08-27 16:04:02 -070064 * This method is called when the HTML parser encounters an
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065 * &lt;img&gt; tag. The <code>source</code> argument is the
66 * string from the "src" attribute; the return value should be
67 * a Drawable representation of the image or <code>null</code>
68 * for a generic replacement image. Make sure you call
69 * setBounds() on your Drawable if it doesn't already have
70 * its bounds set.
71 */
72 public Drawable getDrawable(String source);
73 }
74
75 /**
76 * Is notified when HTML tags are encountered that the parser does
77 * not know how to interpret.
78 */
79 public static interface TagHandler {
80 /**
81 * This method will be called whenn the HTML parser encounters
82 * a tag that it does not know how to interpret.
83 */
84 public void handleTag(boolean opening, String tag,
85 Editable output, XMLReader xmlReader);
86 }
87
88 private Html() { }
89
90 /**
91 * Returns displayable styled text from the provided HTML string.
92 * Any &lt;img&gt; tags in the HTML will display as a generic
93 * replacement image which your program can then go through and
94 * replace with real images.
95 *
96 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
97 */
98 public static Spanned fromHtml(String source) {
99 return fromHtml(source, null, null);
100 }
101
102 /**
103 * Lazy initialization holder for HTML parser. This class will
104 * a) be preloaded by the zygote, or b) not loaded until absolutely
105 * necessary.
106 */
107 private static class HtmlParser {
108 private static final HTMLSchema schema = new HTMLSchema();
109 }
110
111 /**
112 * Returns displayable styled text from the provided HTML string.
113 * Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
114 * to request a representation of the image (use null if you don't
115 * want this) and the specified TagHandler to handle unknown tags
116 * (specify null if you don't want this).
117 *
118 * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
119 */
120 public static Spanned fromHtml(String source, ImageGetter imageGetter,
121 TagHandler tagHandler) {
122 Parser parser = new Parser();
123 try {
124 parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
125 } catch (org.xml.sax.SAXNotRecognizedException e) {
126 // Should not happen.
127 throw new RuntimeException(e);
128 } catch (org.xml.sax.SAXNotSupportedException e) {
129 // Should not happen.
130 throw new RuntimeException(e);
131 }
132
133 HtmlToSpannedConverter converter =
134 new HtmlToSpannedConverter(source, imageGetter, tagHandler,
135 parser);
136 return converter.convert();
137 }
138
139 /**
Raph Levienc55277d2015-07-10 17:11:39 -0700140 * Returns an HTML representation of the provided Spanned text. A best effort is
141 * made to add HTML tags corresponding to spans. Also note that HTML metacharacters
142 * (such as "&lt;" and "&amp;") within the input text are escaped.
143 *
144 * @param text input text to convert
145 * @return string containing input converted to HTML
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800146 */
147 public static String toHtml(Spanned text) {
148 StringBuilder out = new StringBuilder();
The Android Open Source Project10592532009-03-18 17:39:46 -0700149 withinHtml(out, text);
150 return out.toString();
151 }
152
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700153 /**
154 * Returns an HTML escaped representation of the given plain text.
155 */
156 public static String escapeHtml(CharSequence text) {
157 StringBuilder out = new StringBuilder();
158 withinStyle(out, text, 0, text.length());
159 return out.toString();
160 }
161
The Android Open Source Project10592532009-03-18 17:39:46 -0700162 private static void withinHtml(StringBuilder out, Spanned text) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800163 int len = text.length();
164
165 int next;
166 for (int i = 0; i < text.length(); i = next) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700167 next = text.nextSpanTransition(i, len, ParagraphStyle.class);
168 ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class);
Satoshi Kataoka32048302009-03-24 19:48:28 -0700169 String elements = " ";
Eric Fischer00ba7662009-03-25 16:08:50 -0700170 boolean needDiv = false;
171
The Android Open Source Project10592532009-03-18 17:39:46 -0700172 for(int j = 0; j < style.length; j++) {
173 if (style[j] instanceof AlignmentSpan) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800174 Layout.Alignment align =
The Android Open Source Project10592532009-03-18 17:39:46 -0700175 ((AlignmentSpan) style[j]).getAlignment();
Eric Fischer00ba7662009-03-25 16:08:50 -0700176 needDiv = true;
The Android Open Source Project10592532009-03-18 17:39:46 -0700177 if (align == Layout.Alignment.ALIGN_CENTER) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700178 elements = "align=\"center\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700179 } else if (align == Layout.Alignment.ALIGN_OPPOSITE) {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700180 elements = "align=\"right\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700181 } else {
Satoshi Kataoka32048302009-03-24 19:48:28 -0700182 elements = "align=\"left\" " + elements;
The Android Open Source Project10592532009-03-18 17:39:46 -0700183 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700184 }
185 }
Eric Fischer00ba7662009-03-25 16:08:50 -0700186 if (needDiv) {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800187 out.append("<div ").append(elements).append(">");
The Android Open Source Project10592532009-03-18 17:39:46 -0700188 }
189
190 withinDiv(out, text, i, next);
191
Eric Fischer00ba7662009-03-25 16:08:50 -0700192 if (needDiv) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700193 out.append("</div>");
194 }
195 }
196 }
197
198 private static void withinDiv(StringBuilder out, Spanned text,
199 int start, int end) {
200 int next;
201 for (int i = start; i < end; i = next) {
202 next = text.nextSpanTransition(i, end, QuoteSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
204
Romain Guya8f6d5f2012-11-27 11:12:26 -0800205 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 out.append("<blockquote>");
207 }
208
209 withinBlockquote(out, text, i, next);
210
Romain Guya8f6d5f2012-11-27 11:12:26 -0800211 for (QuoteSpan quote : quotes) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800212 out.append("</blockquote>\n");
213 }
214 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 }
216
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800217 private static String getOpenParaTagWithDirection(Spanned text, int start, int end) {
218 final int len = end - start;
Adam Lesinski776abc22014-03-07 11:30:59 -0500219 final byte[] levels = ArrayUtils.newUnpaddedByteArray(len);
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800220 final char[] buffer = TextUtils.obtain(len);
221 TextUtils.getChars(text, start, end, buffer, 0);
222
223 int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,
224 false /* no info */);
225 switch(paraDir) {
226 case Layout.DIR_RIGHT_TO_LEFT:
Raph Levien286da7b2012-10-07 16:55:41 -0700227 return "<p dir=\"rtl\">";
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800228 case Layout.DIR_LEFT_TO_RIGHT:
229 default:
Raph Levien286da7b2012-10-07 16:55:41 -0700230 return "<p dir=\"ltr\">";
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800231 }
232 }
233
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 private static void withinBlockquote(StringBuilder out, Spanned text,
235 int start, int end) {
Fabrice Di Megliocd4161b2012-02-28 15:46:46 -0800236 out.append(getOpenParaTagWithDirection(text, start, end));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800237
238 int next;
239 for (int i = start; i < end; i = next) {
240 next = TextUtils.indexOf(text, '\n', i, end);
241 if (next < 0) {
242 next = end;
243 }
244
245 int nl = 0;
246
247 while (next < end && text.charAt(next) == '\n') {
248 nl++;
249 next++;
250 }
251
Roozbeh Pournader2243ae12015-03-19 15:36:33 -0700252 if (withinParagraph(out, text, i, next - nl, nl, next == end)) {
253 /* Paragraph should be closed */
254 out.append("</p>\n");
255 out.append(getOpenParaTagWithDirection(text, next, end));
256 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800257 }
258
259 out.append("</p>\n");
260 }
261
Roozbeh Pournader2243ae12015-03-19 15:36:33 -0700262 /* Returns true if the caller should close and reopen the paragraph. */
263 private static boolean withinParagraph(StringBuilder out, Spanned text,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800264 int start, int end, int nl,
265 boolean last) {
266 int next;
267 for (int i = start; i < end; i = next) {
268 next = text.nextSpanTransition(i, end, CharacterStyle.class);
269 CharacterStyle[] style = text.getSpans(i, next,
270 CharacterStyle.class);
271
272 for (int j = 0; j < style.length; j++) {
273 if (style[j] instanceof StyleSpan) {
274 int s = ((StyleSpan) style[j]).getStyle();
275
276 if ((s & Typeface.BOLD) != 0) {
277 out.append("<b>");
278 }
279 if ((s & Typeface.ITALIC) != 0) {
280 out.append("<i>");
281 }
282 }
283 if (style[j] instanceof TypefaceSpan) {
284 String s = ((TypefaceSpan) style[j]).getFamily();
285
Raph Levien8e71a392015-05-05 10:13:54 -0700286 if ("monospace".equals(s)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 out.append("<tt>");
288 }
289 }
290 if (style[j] instanceof SuperscriptSpan) {
291 out.append("<sup>");
292 }
293 if (style[j] instanceof SubscriptSpan) {
294 out.append("<sub>");
295 }
296 if (style[j] instanceof UnderlineSpan) {
297 out.append("<u>");
298 }
299 if (style[j] instanceof StrikethroughSpan) {
300 out.append("<strike>");
301 }
302 if (style[j] instanceof URLSpan) {
303 out.append("<a href=\"");
304 out.append(((URLSpan) style[j]).getURL());
305 out.append("\">");
306 }
307 if (style[j] instanceof ImageSpan) {
308 out.append("<img src=\"");
309 out.append(((ImageSpan) style[j]).getSource());
310 out.append("\">");
311
312 // Don't output the dummy character underlying the image.
313 i = next;
314 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700315 if (style[j] instanceof AbsoluteSizeSpan) {
316 out.append("<font size =\"");
317 out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6);
318 out.append("\">");
319 }
320 if (style[j] instanceof ForegroundColorSpan) {
321 out.append("<font color =\"#");
322 String color = Integer.toHexString(((ForegroundColorSpan)
323 style[j]).getForegroundColor() + 0x01000000);
324 while (color.length() < 6) {
325 color = "0" + color;
326 }
327 out.append(color);
328 out.append("\">");
329 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800330 }
331
332 withinStyle(out, text, i, next);
333
334 for (int j = style.length - 1; j >= 0; j--) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700335 if (style[j] instanceof ForegroundColorSpan) {
336 out.append("</font>");
337 }
338 if (style[j] instanceof AbsoluteSizeSpan) {
339 out.append("</font>");
340 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800341 if (style[j] instanceof URLSpan) {
342 out.append("</a>");
343 }
344 if (style[j] instanceof StrikethroughSpan) {
345 out.append("</strike>");
346 }
347 if (style[j] instanceof UnderlineSpan) {
348 out.append("</u>");
349 }
350 if (style[j] instanceof SubscriptSpan) {
351 out.append("</sub>");
352 }
353 if (style[j] instanceof SuperscriptSpan) {
354 out.append("</sup>");
355 }
356 if (style[j] instanceof TypefaceSpan) {
357 String s = ((TypefaceSpan) style[j]).getFamily();
358
359 if (s.equals("monospace")) {
360 out.append("</tt>");
361 }
362 }
363 if (style[j] instanceof StyleSpan) {
364 int s = ((StyleSpan) style[j]).getStyle();
365
366 if ((s & Typeface.BOLD) != 0) {
367 out.append("</b>");
368 }
369 if ((s & Typeface.ITALIC) != 0) {
370 out.append("</i>");
371 }
372 }
373 }
374 }
375
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 if (nl == 1) {
377 out.append("<br>\n");
Roozbeh Pournader2243ae12015-03-19 15:36:33 -0700378 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 } else {
380 for (int i = 2; i < nl; i++) {
381 out.append("<br>");
382 }
Roozbeh Pournader2243ae12015-03-19 15:36:33 -0700383 return !last;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800384 }
385 }
386
Dianne Hackbornacb69bb2012-04-13 15:36:06 -0700387 private static void withinStyle(StringBuilder out, CharSequence text,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800388 int start, int end) {
389 for (int i = start; i < end; i++) {
390 char c = text.charAt(i);
391
392 if (c == '<') {
393 out.append("&lt;");
394 } else if (c == '>') {
395 out.append("&gt;");
396 } else if (c == '&') {
397 out.append("&amp;");
Victoria Lease3d476412013-10-29 15:34:51 -0700398 } else if (c >= 0xD800 && c <= 0xDFFF) {
399 if (c < 0xDC00 && i + 1 < end) {
400 char d = text.charAt(i + 1);
401 if (d >= 0xDC00 && d <= 0xDFFF) {
402 i++;
403 int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00;
404 out.append("&#").append(codepoint).append(";");
405 }
406 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800407 } else if (c > 0x7E || c < ' ') {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800408 out.append("&#").append((int) c).append(";");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800409 } else if (c == ' ') {
410 while (i + 1 < end && text.charAt(i + 1) == ' ') {
411 out.append("&nbsp;");
412 i++;
413 }
414
415 out.append(' ');
416 } else {
417 out.append(c);
418 }
419 }
420 }
421}
422
423class HtmlToSpannedConverter implements ContentHandler {
424
425 private static final float[] HEADER_SIZES = {
426 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
427 };
428
429 private String mSource;
430 private XMLReader mReader;
431 private SpannableStringBuilder mSpannableStringBuilder;
432 private Html.ImageGetter mImageGetter;
433 private Html.TagHandler mTagHandler;
434
435 public HtmlToSpannedConverter(
436 String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
437 Parser parser) {
438 mSource = source;
439 mSpannableStringBuilder = new SpannableStringBuilder();
440 mImageGetter = imageGetter;
441 mTagHandler = tagHandler;
442 mReader = parser;
443 }
444
445 public Spanned convert() {
446
447 mReader.setContentHandler(this);
448 try {
449 mReader.parse(new InputSource(new StringReader(mSource)));
450 } catch (IOException e) {
451 // We are reading from a string. There should not be IO problems.
452 throw new RuntimeException(e);
453 } catch (SAXException e) {
454 // TagSoup doesn't throw parse exceptions.
455 throw new RuntimeException(e);
456 }
457
458 // Fix flags and range for paragraph-type markup.
459 Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
460 for (int i = 0; i < obj.length; i++) {
461 int start = mSpannableStringBuilder.getSpanStart(obj[i]);
462 int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
463
464 // If the last line of the range is blank, back off by one.
465 if (end - 2 >= 0) {
466 if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
467 mSpannableStringBuilder.charAt(end - 2) == '\n') {
468 end--;
469 }
470 }
471
472 if (end == start) {
473 mSpannableStringBuilder.removeSpan(obj[i]);
474 } else {
475 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
476 }
477 }
478
479 return mSpannableStringBuilder;
480 }
481
482 private void handleStartTag(String tag, Attributes attributes) {
483 if (tag.equalsIgnoreCase("br")) {
484 // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
485 // so we can safely emite the linebreaks when we handle the close tag.
486 } else if (tag.equalsIgnoreCase("p")) {
487 handleP(mSpannableStringBuilder);
488 } else if (tag.equalsIgnoreCase("div")) {
489 handleP(mSpannableStringBuilder);
Romain Guy94d5e9a2011-08-29 11:12:19 -0700490 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800491 start(mSpannableStringBuilder, new Bold());
492 } else if (tag.equalsIgnoreCase("b")) {
493 start(mSpannableStringBuilder, new Bold());
Romain Guy94d5e9a2011-08-29 11:12:19 -0700494 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800495 start(mSpannableStringBuilder, new Italic());
496 } else if (tag.equalsIgnoreCase("cite")) {
497 start(mSpannableStringBuilder, new Italic());
498 } else if (tag.equalsIgnoreCase("dfn")) {
499 start(mSpannableStringBuilder, new Italic());
500 } else if (tag.equalsIgnoreCase("i")) {
501 start(mSpannableStringBuilder, new Italic());
502 } else if (tag.equalsIgnoreCase("big")) {
503 start(mSpannableStringBuilder, new Big());
504 } else if (tag.equalsIgnoreCase("small")) {
505 start(mSpannableStringBuilder, new Small());
506 } else if (tag.equalsIgnoreCase("font")) {
507 startFont(mSpannableStringBuilder, attributes);
508 } else if (tag.equalsIgnoreCase("blockquote")) {
509 handleP(mSpannableStringBuilder);
510 start(mSpannableStringBuilder, new Blockquote());
511 } else if (tag.equalsIgnoreCase("tt")) {
512 start(mSpannableStringBuilder, new Monospace());
513 } else if (tag.equalsIgnoreCase("a")) {
514 startA(mSpannableStringBuilder, attributes);
515 } else if (tag.equalsIgnoreCase("u")) {
516 start(mSpannableStringBuilder, new Underline());
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -0700517 } else if (tag.equalsIgnoreCase("del")) {
518 start(mSpannableStringBuilder, new Strikethrough());
519 } else if (tag.equalsIgnoreCase("s")) {
520 start(mSpannableStringBuilder, new Strikethrough());
521 } else if (tag.equalsIgnoreCase("strike")) {
522 start(mSpannableStringBuilder, new Strikethrough());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800523 } else if (tag.equalsIgnoreCase("sup")) {
524 start(mSpannableStringBuilder, new Super());
525 } else if (tag.equalsIgnoreCase("sub")) {
526 start(mSpannableStringBuilder, new Sub());
527 } else if (tag.length() == 2 &&
528 Character.toLowerCase(tag.charAt(0)) == 'h' &&
529 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
530 handleP(mSpannableStringBuilder);
531 start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
532 } else if (tag.equalsIgnoreCase("img")) {
533 startImg(mSpannableStringBuilder, attributes, mImageGetter);
534 } else if (mTagHandler != null) {
535 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
536 }
537 }
538
539 private void handleEndTag(String tag) {
540 if (tag.equalsIgnoreCase("br")) {
541 handleBr(mSpannableStringBuilder);
542 } else if (tag.equalsIgnoreCase("p")) {
543 handleP(mSpannableStringBuilder);
544 } else if (tag.equalsIgnoreCase("div")) {
545 handleP(mSpannableStringBuilder);
Romain Guydd808c02011-09-06 10:54:46 -0700546 } else if (tag.equalsIgnoreCase("strong")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800547 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
548 } else if (tag.equalsIgnoreCase("b")) {
549 end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
Romain Guydd808c02011-09-06 10:54:46 -0700550 } else if (tag.equalsIgnoreCase("em")) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800551 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
552 } else if (tag.equalsIgnoreCase("cite")) {
553 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
554 } else if (tag.equalsIgnoreCase("dfn")) {
555 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
556 } else if (tag.equalsIgnoreCase("i")) {
557 end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
558 } else if (tag.equalsIgnoreCase("big")) {
559 end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
560 } else if (tag.equalsIgnoreCase("small")) {
561 end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
562 } else if (tag.equalsIgnoreCase("font")) {
563 endFont(mSpannableStringBuilder);
564 } else if (tag.equalsIgnoreCase("blockquote")) {
565 handleP(mSpannableStringBuilder);
566 end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
567 } else if (tag.equalsIgnoreCase("tt")) {
568 end(mSpannableStringBuilder, Monospace.class,
569 new TypefaceSpan("monospace"));
570 } else if (tag.equalsIgnoreCase("a")) {
571 endA(mSpannableStringBuilder);
572 } else if (tag.equalsIgnoreCase("u")) {
573 end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -0700574 } else if (tag.equalsIgnoreCase("del")) {
575 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
576 } else if (tag.equalsIgnoreCase("s")) {
577 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
578 } else if (tag.equalsIgnoreCase("strike")) {
579 end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800580 } else if (tag.equalsIgnoreCase("sup")) {
581 end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
582 } else if (tag.equalsIgnoreCase("sub")) {
583 end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
584 } else if (tag.length() == 2 &&
585 Character.toLowerCase(tag.charAt(0)) == 'h' &&
586 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
587 handleP(mSpannableStringBuilder);
588 endHeader(mSpannableStringBuilder);
589 } else if (mTagHandler != null) {
590 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
591 }
592 }
593
594 private static void handleP(SpannableStringBuilder text) {
595 int len = text.length();
596
597 if (len >= 1 && text.charAt(len - 1) == '\n') {
598 if (len >= 2 && text.charAt(len - 2) == '\n') {
599 return;
600 }
601
602 text.append("\n");
603 return;
604 }
605
606 if (len != 0) {
607 text.append("\n\n");
608 }
609 }
610
611 private static void handleBr(SpannableStringBuilder text) {
612 text.append("\n");
613 }
614
615 private static Object getLast(Spanned text, Class kind) {
616 /*
617 * This knows that the last returned object from getSpans()
618 * will be the most recently added.
619 */
620 Object[] objs = text.getSpans(0, text.length(), kind);
621
622 if (objs.length == 0) {
623 return null;
624 } else {
625 return objs[objs.length - 1];
626 }
627 }
628
629 private static void start(SpannableStringBuilder text, Object mark) {
630 int len = text.length();
631 text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
632 }
633
634 private static void end(SpannableStringBuilder text, Class kind,
635 Object repl) {
636 int len = text.length();
637 Object obj = getLast(text, kind);
638 int where = text.getSpanStart(obj);
639
640 text.removeSpan(obj);
641
642 if (where != len) {
643 text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
644 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800645 }
646
647 private static void startImg(SpannableStringBuilder text,
648 Attributes attributes, Html.ImageGetter img) {
649 String src = attributes.getValue("", "src");
650 Drawable d = null;
651
652 if (img != null) {
653 d = img.getDrawable(src);
654 }
655
656 if (d == null) {
657 d = Resources.getSystem().
658 getDrawable(com.android.internal.R.drawable.unknown_image);
659 d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
660 }
661
662 int len = text.length();
663 text.append("\uFFFC");
664
665 text.setSpan(new ImageSpan(d, src), len, text.length(),
666 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
667 }
668
669 private static void startFont(SpannableStringBuilder text,
670 Attributes attributes) {
671 String color = attributes.getValue("", "color");
672 String face = attributes.getValue("", "face");
673
674 int len = text.length();
675 text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
676 }
677
678 private static void endFont(SpannableStringBuilder text) {
679 int len = text.length();
680 Object obj = getLast(text, Font.class);
681 int where = text.getSpanStart(obj);
682
683 text.removeSpan(obj);
684
685 if (where != len) {
686 Font f = (Font) obj;
687
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100688 if (!TextUtils.isEmpty(f.mColor)) {
689 if (f.mColor.startsWith("@")) {
690 Resources res = Resources.getSystem();
691 String name = f.mColor.substring(1);
692 int colorRes = res.getIdentifier(name, "color", "android");
693 if (colorRes != 0) {
Alan Viverette4a357cd2015-03-18 18:37:18 -0700694 ColorStateList colors = res.getColorStateList(colorRes, null);
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100695 text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
696 where, len,
697 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800698 }
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100699 } else {
Romain Guya8f6d5f2012-11-27 11:12:26 -0800700 int c = Color.getHtmlColor(f.mColor);
Bjorn Bringert9cab7f72009-07-15 22:14:24 +0100701 if (c != -1) {
702 text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
703 where, len,
704 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
705 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800706 }
707 }
708
709 if (f.mFace != null) {
710 text.setSpan(new TypefaceSpan(f.mFace), where, len,
711 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
712 }
713 }
714 }
715
716 private static void startA(SpannableStringBuilder text, Attributes attributes) {
717 String href = attributes.getValue("", "href");
718
719 int len = text.length();
720 text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
721 }
722
723 private static void endA(SpannableStringBuilder text) {
724 int len = text.length();
725 Object obj = getLast(text, Href.class);
726 int where = text.getSpanStart(obj);
727
728 text.removeSpan(obj);
729
730 if (where != len) {
731 Href h = (Href) obj;
732
733 if (h.mHref != null) {
734 text.setSpan(new URLSpan(h.mHref), where, len,
735 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
736 }
737 }
738 }
739
740 private static void endHeader(SpannableStringBuilder text) {
741 int len = text.length();
742 Object obj = getLast(text, Header.class);
743
744 int where = text.getSpanStart(obj);
745
746 text.removeSpan(obj);
747
748 // Back off not to change only the text, not the blank line.
749 while (len > where && text.charAt(len - 1) == '\n') {
750 len--;
751 }
752
753 if (where != len) {
754 Header h = (Header) obj;
755
756 text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
757 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
758 text.setSpan(new StyleSpan(Typeface.BOLD),
759 where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
760 }
761 }
762
763 public void setDocumentLocator(Locator locator) {
764 }
765
766 public void startDocument() throws SAXException {
767 }
768
769 public void endDocument() throws SAXException {
770 }
771
772 public void startPrefixMapping(String prefix, String uri) throws SAXException {
773 }
774
775 public void endPrefixMapping(String prefix) throws SAXException {
776 }
777
778 public void startElement(String uri, String localName, String qName, Attributes attributes)
779 throws SAXException {
780 handleStartTag(localName, attributes);
781 }
782
783 public void endElement(String uri, String localName, String qName) throws SAXException {
784 handleEndTag(localName);
785 }
786
787 public void characters(char ch[], int start, int length) throws SAXException {
788 StringBuilder sb = new StringBuilder();
789
790 /*
791 * Ignore whitespace that immediately follows other whitespace;
792 * newlines count as spaces.
793 */
794
795 for (int i = 0; i < length; i++) {
796 char c = ch[i + start];
797
798 if (c == ' ' || c == '\n') {
799 char pred;
800 int len = sb.length();
801
802 if (len == 0) {
803 len = mSpannableStringBuilder.length();
804
805 if (len == 0) {
806 pred = '\n';
807 } else {
808 pred = mSpannableStringBuilder.charAt(len - 1);
809 }
810 } else {
811 pred = sb.charAt(len - 1);
812 }
813
814 if (pred != ' ' && pred != '\n') {
815 sb.append(' ');
816 }
817 } else {
818 sb.append(c);
819 }
820 }
821
822 mSpannableStringBuilder.append(sb);
823 }
824
825 public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
826 }
827
828 public void processingInstruction(String target, String data) throws SAXException {
829 }
830
831 public void skippedEntity(String name) throws SAXException {
832 }
833
834 private static class Bold { }
835 private static class Italic { }
836 private static class Underline { }
Roozbeh Pournader5e9ed362015-08-11 15:30:27 -0700837 private static class Strikethrough { }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800838 private static class Big { }
839 private static class Small { }
840 private static class Monospace { }
841 private static class Blockquote { }
842 private static class Super { }
843 private static class Sub { }
844
845 private static class Font {
846 public String mColor;
847 public String mFace;
848
849 public Font(String color, String face) {
850 mColor = color;
851 mFace = face;
852 }
853 }
854
855 private static class Href {
856 public String mHref;
857
858 public Href(String href) {
859 mHref = href;
860 }
861 }
862
863 private static class Header {
864 private int mLevel;
865
866 public Header(int level) {
867 mLevel = level;
868 }
869 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800870}