| package org.wordpress.android.util; |
| |
| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.net.Uri; |
| import android.text.Editable; |
| import android.text.Layout; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.TextUtils; |
| import android.text.style.AbsoluteSizeSpan; |
| import android.text.style.AlignmentSpan; |
| import android.text.style.CharacterStyle; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.ImageSpan; |
| import android.text.style.ParagraphStyle; |
| import android.text.style.QuoteSpan; |
| import android.text.style.RelativeSizeSpan; |
| import android.text.style.StrikethroughSpan; |
| import android.text.style.StyleSpan; |
| import android.text.style.SubscriptSpan; |
| import android.text.style.SuperscriptSpan; |
| import android.text.style.TextAppearanceSpan; |
| import android.text.style.TypefaceSpan; |
| import android.text.style.URLSpan; |
| |
| import org.ccil.cowan.tagsoup.HTMLSchema; |
| import org.ccil.cowan.tagsoup.Parser; |
| import org.wordpress.android.R; |
| import org.wordpress.android.WordPress; |
| import org.wordpress.android.models.Blog; |
| import org.wordpress.android.util.helpers.MediaFile; |
| import org.wordpress.android.util.helpers.MediaGallery; |
| import org.wordpress.android.models.Post; |
| import org.wordpress.android.util.AppLog.T; |
| import org.wordpress.android.util.helpers.MediaGalleryImageSpan; |
| import org.wordpress.android.util.helpers.WPImageSpan; |
| import org.wordpress.android.util.helpers.WPUnderlineSpan; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.XMLReader; |
| |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.util.HashMap; |
| import java.util.Locale; |
| |
| /** |
| * This class processes HTML strings into displayable styled text. Not all HTML |
| * tags are supported. |
| */ |
| public class WPHtml { |
| /** |
| * Retrieves images for HTML <img> tags. |
| */ |
| public static interface ImageGetter { |
| /** |
| * This method is called when the HTML parser encounters an <img> |
| * tag. The <code>source</code> argument is the string from the "src" |
| * attribute; the return value should be a Drawable representation of |
| * the image or <code>null</code> for a generic replacement image. Make |
| * sure you call setBounds() on your Drawable if it doesn't already have |
| * its bounds set. |
| */ |
| public Drawable getDrawable(String source); |
| } |
| |
| /** |
| * Is notified when HTML tags are encountered that the parser does not know |
| * how to interpret. |
| */ |
| public static interface TagHandler { |
| /** |
| * This method will be called whenn the HTML parser encounters a tag |
| * that it does not know how to interpret. |
| * |
| * @param mysteryTagContent |
| */ |
| public void handleTag(boolean opening, String tag, Editable output, |
| XMLReader xmlReader, String mysteryTagContent); |
| } |
| |
| private WPHtml() { |
| } |
| |
| /** |
| * Returns displayable styled text from the provided HTML string. Any |
| * <img> tags in the HTML will display as a generic replacement image |
| * which your program can then go through and replace with real images. |
| * |
| * <p> |
| * This uses TagSoup to handle real HTML, including all of the brokenness |
| * found in the wild. |
| */ |
| public static Spanned fromHtml(String source, Context ctx, Post post, int maxImageWidth) { |
| return fromHtml(source, null, null, ctx, post, maxImageWidth); |
| } |
| |
| /** |
| * Lazy initialization holder for HTML parser. This class will a) be |
| * preloaded by the zygote, or b) not loaded until absolutely necessary. |
| */ |
| private static class HtmlParser { |
| private static final HTMLSchema schema = new HTMLSchema(); |
| } |
| |
| /** |
| * Returns displayable styled text from the provided HTML string. Any |
| * <img> tags in the HTML will use the specified ImageGetter to |
| * request a representation of the image (use null if you don't want this) |
| * and the specified TagHandler to handle unknown tags (specify null if you |
| * don't want this). |
| * |
| * <p> |
| * This uses TagSoup to handle real HTML, including all of the brokenness |
| * found in the wild. |
| */ |
| public static Spanned fromHtml(String source, ImageGetter imageGetter, |
| TagHandler tagHandler, Context ctx, Post post, int maxImageWidth) { |
| Parser parser = new Parser(); |
| try { |
| parser.setProperty(Parser.schemaProperty, HtmlParser.schema); |
| } catch (org.xml.sax.SAXNotRecognizedException e) { |
| // Should not happen. |
| throw new RuntimeException(e); |
| } catch (org.xml.sax.SAXNotSupportedException e) { |
| // Should not happen. |
| throw new RuntimeException(e); |
| } |
| |
| HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, |
| imageGetter, tagHandler, parser, ctx, post, maxImageWidth); |
| return converter.convert(); |
| } |
| |
| /** |
| * Returns an HTML representation of the provided Spanned text. |
| */ |
| public static String toHtml(Spanned text) { |
| StringBuilder out = new StringBuilder(); |
| withinHtml(out, text); |
| return out.toString(); |
| } |
| |
| private static void withinHtml(StringBuilder out, Spanned text) { |
| int len = text.length(); |
| |
| int next; |
| for (int i = 0; i < text.length(); i = next) { |
| next = text.nextSpanTransition(i, len, ParagraphStyle.class); |
| /*ParagraphStyle[] style = text.getSpans(i, next, |
| ParagraphStyle.class); |
| String elements = " "; |
| boolean needDiv = false; |
| |
| for (int j = 0; j < style.length; j++) { |
| if (style[j] instanceof AlignmentSpan) { |
| Layout.Alignment align = ((AlignmentSpan) style[j]) |
| .getAlignment(); |
| needDiv = true; |
| if (align == Layout.Alignment.ALIGN_CENTER) { |
| elements = "align=\"center\" " + elements; |
| } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { |
| elements = "align=\"right\" " + elements; |
| } else { |
| elements = "align=\"left\" " + elements; |
| } |
| } |
| } |
| if (needDiv) { |
| out.append("<div " + elements + ">"); |
| }*/ |
| |
| withinDiv(out, text, i, next); |
| |
| /*if (needDiv) { |
| out.append("</div>"); |
| }*/ |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| private static void withinDiv(StringBuilder out, Spanned text, int start, |
| int end) { |
| int next; |
| for (int i = start; i < end; i = next) { |
| next = text.nextSpanTransition(i, end, QuoteSpan.class); |
| QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); |
| |
| for (QuoteSpan quote : quotes) { |
| out.append("<blockquote>"); |
| } |
| |
| withinBlockquote(out, text, i, next); |
| |
| for (QuoteSpan quote : quotes) { |
| out.append("</blockquote>\n"); |
| } |
| } |
| } |
| |
| private static void withinBlockquote(StringBuilder out, Spanned text, |
| int start, int end) { |
| out.append("<p>"); |
| |
| int next; |
| for (int i = start; i < end; i = next) { |
| next = TextUtils.indexOf(text, '\n', i, end); |
| if (next < 0) { |
| next = end; |
| } |
| |
| int nl = 0; |
| |
| while (next < end && text.charAt(next) == '\n') { |
| nl++; |
| next++; |
| } |
| |
| withinParagraph(out, text, i, next - nl, nl, next == end); |
| } |
| |
| out.append("</p>\n"); |
| } |
| |
| private static void withinParagraph(StringBuilder out, Spanned text, |
| int start, int end, int nl, boolean last) { |
| int next; |
| for (int i = start; i < end; i = next) { |
| next = text.nextSpanTransition(i, end, CharacterStyle.class); |
| CharacterStyle[] style = text.getSpans(i, next, |
| CharacterStyle.class); |
| |
| for (int j = 0; j < style.length; j++) { |
| if (style[j] instanceof StyleSpan) { |
| int s = ((StyleSpan) style[j]).getStyle(); |
| |
| if ((s & Typeface.BOLD) != 0) { |
| out.append("<strong>"); |
| } |
| if ((s & Typeface.ITALIC) != 0) { |
| out.append("<em>"); |
| } |
| } |
| if (style[j] instanceof TypefaceSpan) { |
| String s = ((TypefaceSpan) style[j]).getFamily(); |
| |
| if (s.equals("monospace")) { |
| out.append("<tt>"); |
| } |
| } |
| if (style[j] instanceof SuperscriptSpan) { |
| out.append("<sup>"); |
| } |
| if (style[j] instanceof SubscriptSpan) { |
| out.append("<sub>"); |
| } |
| if (style[j] instanceof WPUnderlineSpan) { |
| out.append("<u>"); |
| } |
| if (style[j] instanceof StrikethroughSpan) { |
| out.append("<strike>"); |
| } |
| if (style[j] instanceof URLSpan) { |
| out.append("<a href=\""); |
| out.append(((URLSpan) style[j]).getURL()); |
| out.append("\">"); |
| } |
| if (style[j] instanceof MediaGalleryImageSpan) { |
| out.append(getGalleryShortcode((MediaGalleryImageSpan) style[j])); |
| } else if (style[j] instanceof WPImageSpan && ((WPImageSpan) style[j]).getMediaFile().getMediaId() != null) { |
| out.append(getContent((WPImageSpan) style[j])); |
| } else if (style[j] instanceof WPImageSpan) { |
| out.append("<img src=\""); |
| out.append(((WPImageSpan) style[j]).getSource()); |
| out.append("\" android-uri=\"" |
| + ((WPImageSpan) style[j]).getImageSource() |
| .toString() + "\""); |
| out.append(" />"); |
| // Don't output the dummy character underlying the image. |
| i = next; |
| } |
| if (style[j] instanceof AbsoluteSizeSpan) { |
| out.append("<font size =\""); |
| out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6); |
| out.append("\">"); |
| } |
| if (style[j] instanceof ForegroundColorSpan) { |
| out.append("<font color =\"#"); |
| String color = Integer |
| .toHexString(((ForegroundColorSpan) style[j]) |
| .getForegroundColor() + 0x01000000); |
| while (color.length() < 6) { |
| color = "0" + color; |
| } |
| out.append(color); |
| out.append("\">"); |
| } |
| } |
| |
| processWPImage(out, text, i, next); |
| |
| for (int j = style.length - 1; j >= 0; j--) { |
| if (style[j] instanceof ForegroundColorSpan) { |
| out.append("</font>"); |
| } |
| if (style[j] instanceof AbsoluteSizeSpan) { |
| out.append("</font>"); |
| } |
| if (style[j] instanceof URLSpan) { |
| out.append("</a>"); |
| } |
| if (style[j] instanceof StrikethroughSpan) { |
| out.append("</strike>"); |
| } |
| if (style[j] instanceof WPUnderlineSpan) { |
| out.append("</u>"); |
| } |
| if (style[j] instanceof SubscriptSpan) { |
| out.append("</sub>"); |
| } |
| if (style[j] instanceof SuperscriptSpan) { |
| out.append("</sup>"); |
| } |
| if (style[j] instanceof TypefaceSpan) { |
| String s = ((TypefaceSpan) style[j]).getFamily(); |
| |
| if (s.equals("monospace")) { |
| out.append("</tt>"); |
| } |
| } |
| if (style[j] instanceof StyleSpan) { |
| int s = ((StyleSpan) style[j]).getStyle(); |
| |
| if ((s & Typeface.BOLD) != 0) { |
| out.append("</strong>"); |
| } |
| if ((s & Typeface.ITALIC) != 0) { |
| out.append("</em>"); |
| } |
| } |
| } |
| } |
| |
| String p = last ? "" : "</p>\n<p>"; |
| |
| if (nl == 1) { |
| out.append("<br>\n"); |
| } else if (nl == 2) { |
| out.append(p); |
| } else { |
| for (int i = 2; i < nl; i++) { |
| out.append("<br>"); |
| } |
| |
| out.append(p); |
| } |
| } |
| |
| /** Get gallery shortcode for a MediaGalleryImageSpan */ |
| public static String getGalleryShortcode(MediaGalleryImageSpan gallerySpan) { |
| String shortcode = ""; |
| MediaGallery gallery = gallerySpan.getMediaGallery(); |
| shortcode += "[gallery "; |
| if (gallery.isRandom()) |
| shortcode += " orderby=\"rand\""; |
| if (gallery.getType().equals("")) |
| shortcode += " columns=\"" + gallery.getNumColumns() + "\""; |
| else |
| shortcode += " type=\"" + gallery.getType() + "\""; |
| shortcode += " ids=\"" + gallery.getIdsStr() + "\""; |
| shortcode += "]"; |
| |
| return shortcode; |
| } |
| |
| /** Retrieve an image span content for a media file that exists on the server **/ |
| public static String getContent(WPImageSpan imageSpan) { |
| // based on PostUploadService |
| |
| String content = ""; |
| MediaFile mediaFile = imageSpan.getMediaFile(); |
| if (mediaFile == null) |
| return content; |
| String mediaId = mediaFile.getMediaId(); |
| if (mediaId == null || mediaId.length() == 0) |
| return content; |
| |
| boolean isVideo = mediaFile.isVideo(); |
| String url = imageSpan.getImageSource().toString(); |
| |
| if (isVideo) { |
| if (!TextUtils.isEmpty(mediaFile.getVideoPressShortCode())) { |
| content = mediaFile.getVideoPressShortCode(); |
| } else { |
| int xRes = mediaFile.getWidth(); |
| int yRes = mediaFile.getHeight(); |
| String mimeType = mediaFile.getMimeType(); |
| content = String.format("<video width=\"%s\" height=\"%s\" controls=\"controls\"><source src=\"%s\" type=\"%s\" /><a href=\"%s\">Click to view video</a>.</video>", |
| xRes, yRes, url, mimeType, url); |
| } |
| } else { |
| String alignment = ""; |
| switch (mediaFile.getHorizontalAlignment()) { |
| case 0: |
| alignment = "alignnone"; |
| break; |
| case 1: |
| alignment = "alignleft"; |
| break; |
| case 2: |
| alignment = "aligncenter"; |
| break; |
| case 3: |
| alignment = "alignright"; |
| break; |
| } |
| String alignmentCSS = "class=\"" + alignment + " size-full\" "; |
| String title = mediaFile.getTitle(); |
| String caption = mediaFile.getCaption(); |
| int width = mediaFile.getWidth(); |
| |
| String inlineCSS = " "; |
| String localBlogID = imageSpan.getMediaFile().getBlogId(); |
| Blog currentBlog = WordPress.wpDB.instantiateBlogByLocalId(Integer.parseInt(localBlogID)); |
| // If it's not a gif and blog don't keep original size, there is a chance we need to resize |
| if (currentBlog != null && !mediaFile.getMimeType().equals("image/gif") |
| && MediaUtils.getImageWidthSettingFromString(currentBlog.getMaxImageWidth()) != Integer.MAX_VALUE) { |
| width = MediaUtils.getMaximumImageWidth(width, currentBlog.getMaxImageWidth()); |
| // Use inline CSS on self-hosted blogs to enforce picture resize settings |
| if (!currentBlog.isDotcomFlag()) { |
| inlineCSS = String.format(Locale.US, " style=\"width:%dpx;max-width:%dpx;\" ", width, width); |
| } |
| } |
| content = content + "<a href=\"" + url + "\"><img" + inlineCSS + "title=\"" + title + "\" " |
| + alignmentCSS + "alt=\"image\" src=\"" + url + "?w=" + width +"\" /></a>"; |
| |
| if (!caption.equals("")) { |
| content = String.format(Locale.US, |
| "[caption id=\"\" align=\"%s\" width=\"%d\"]%s%s[/caption]", |
| alignment, width, content, TextUtils.htmlEncode(caption)); |
| } |
| } |
| |
| return content; |
| } |
| |
| private static void processWPImage(StringBuilder out, Spanned text, |
| int start, int end) { |
| int next; |
| |
| for (int i = start; i < end; i = next) { |
| next = text.nextSpanTransition(i, end, SpannableString.class); |
| SpannableString[] images = text.getSpans(i, next, |
| SpannableString.class); |
| |
| for (SpannableString image : images) { |
| out.append(image.toString()); |
| } |
| |
| withinStyle(out, text, i, next); |
| |
| } |
| } |
| |
| private static void withinStyle(StringBuilder out, Spanned text, int start, |
| int end) { |
| for (int i = start; i < end; i++) { |
| char c = text.charAt(i); |
| |
| /* |
| * if (c == '<') { out.append("<"); } else if (c == '>') { |
| * out.append(">"); } else if (c == '&') { out.append("&"); |
| * if (c > 0x7E || c < ' ') { out.append("&#" + ((int) c) + ";"); } |
| * else |
| */ |
| if (c == ' ') { |
| while (i + 1 < end && text.charAt(i + 1) == ' ') { |
| out.append(" "); |
| i++; |
| } |
| |
| out.append(' '); |
| } else { |
| out.append(c); |
| } |
| } |
| } |
| } |
| |
| class HtmlToSpannedConverter implements ContentHandler { |
| private static final float[] HEADER_SIZES = { 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, |
| 1f, }; |
| |
| private String mSource; |
| private XMLReader mReader; |
| private SpannableStringBuilder mSpannableStringBuilder; |
| private WPHtml.ImageGetter mImageGetter; |
| private String mysteryTagContent; |
| private boolean mysteryTagFound; |
| private int mMaxImageWidth; |
| private Context mContext; |
| private Post mPost; |
| |
| private String mysteryTagName; |
| |
| public HtmlToSpannedConverter(String source, |
| WPHtml.ImageGetter imageGetter, WPHtml.TagHandler tagHandler, |
| Parser parser, Context context, Post p, int maxImageWidth) { |
| mSource = source; |
| mSpannableStringBuilder = new SpannableStringBuilder(); |
| mImageGetter = imageGetter; |
| mReader = parser; |
| mysteryTagContent = ""; |
| mysteryTagName = null; |
| mContext = context; |
| mPost = p; |
| mMaxImageWidth = maxImageWidth; |
| } |
| |
| public Spanned convert() { |
| mReader.setContentHandler(this); |
| try { |
| mReader.parse(new InputSource(new StringReader(mSource))); |
| } catch (IOException e) { |
| // We are reading from a string. There should not be IO problems. |
| throw new RuntimeException(e); |
| } catch (SAXException e) { |
| // TagSoup doesn't throw parse exceptions. |
| throw new RuntimeException(e); |
| } |
| |
| // Fix flags and range for paragraph-type markup. |
| Object[] obj = mSpannableStringBuilder.getSpans(0, |
| mSpannableStringBuilder.length(), ParagraphStyle.class); |
| for (int i = 0; i < obj.length; i++) { |
| int start = mSpannableStringBuilder.getSpanStart(obj[i]); |
| int end = mSpannableStringBuilder.getSpanEnd(obj[i]); |
| |
| // If the last line of the range is blank, back off by one. |
| if (end - 2 >= 0) { |
| if (mSpannableStringBuilder.charAt(end - 1) == '\n' |
| && mSpannableStringBuilder.charAt(end - 2) == '\n') { |
| end--; |
| } |
| } |
| |
| if (end == start) { |
| mSpannableStringBuilder.removeSpan(obj[i]); |
| } else { |
| try { |
| mSpannableStringBuilder.setSpan(obj[i], start, end, |
| Spannable.SPAN_PARAGRAPH); |
| } catch (Exception e) { |
| } |
| } |
| } |
| |
| return mSpannableStringBuilder; |
| } |
| |
| private void handleStartTag(String tag, Attributes attributes) { |
| if (!mysteryTagFound) { |
| if (mPost != null) { |
| if (!mPost.isLocalDraft()) { |
| if (tag.equalsIgnoreCase("img")) |
| startImg(mSpannableStringBuilder, attributes, |
| mImageGetter); |
| |
| return; |
| } |
| } |
| |
| if (tag.equalsIgnoreCase("br")) { |
| // We don't need to handle this. TagSoup will ensure that |
| // there's a |
| // </br> for each <br> |
| // so we can safely emite the linebreaks when we handle the |
| // close |
| // tag. |
| } else if (tag.equalsIgnoreCase("p")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("div")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("em")) { |
| start(mSpannableStringBuilder, new Italic()); |
| } else if (tag.equalsIgnoreCase("b")) { |
| start(mSpannableStringBuilder, new Bold()); |
| } else if (tag.equalsIgnoreCase("strong")) { |
| start(mSpannableStringBuilder, new Bold()); |
| } else if (tag.equalsIgnoreCase("cite")) { |
| start(mSpannableStringBuilder, new Italic()); |
| } else if (tag.equalsIgnoreCase("dfn")) { |
| start(mSpannableStringBuilder, new Italic()); |
| } else if (tag.equalsIgnoreCase("i")) { |
| start(mSpannableStringBuilder, new Italic()); |
| } else if (tag.equalsIgnoreCase("big")) { |
| start(mSpannableStringBuilder, new Big()); |
| } else if (tag.equalsIgnoreCase("small")) { |
| start(mSpannableStringBuilder, new Small()); |
| } else if (tag.equalsIgnoreCase("font")) { |
| startFont(mSpannableStringBuilder, attributes); |
| } else if (tag.equalsIgnoreCase("blockquote")) { |
| handleP(mSpannableStringBuilder); |
| start(mSpannableStringBuilder, new Blockquote()); |
| } else if (tag.equalsIgnoreCase("tt")) { |
| start(mSpannableStringBuilder, new Monospace()); |
| } else if (tag.equalsIgnoreCase("a")) { |
| startA(mSpannableStringBuilder, attributes); |
| } else if (tag.equalsIgnoreCase("u")) { |
| start(mSpannableStringBuilder, new Underline()); |
| } else if (tag.equalsIgnoreCase("sup")) { |
| start(mSpannableStringBuilder, new Super()); |
| } else if (tag.equalsIgnoreCase("sub")) { |
| start(mSpannableStringBuilder, new Sub()); |
| } else if (tag.equalsIgnoreCase("strike")) { |
| start(mSpannableStringBuilder, new Strike()); |
| } else if (tag.length() == 2 |
| && Character.toLowerCase(tag.charAt(0)) == 'h' |
| && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { |
| handleP(mSpannableStringBuilder); |
| start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1')); |
| } else if (tag.equalsIgnoreCase("img")) { |
| startImg(mSpannableStringBuilder, attributes, mImageGetter); |
| } else { |
| if (tag.equalsIgnoreCase("html") || tag.equalsIgnoreCase("body")) { |
| return; |
| } |
| |
| mysteryTagFound = true; |
| mysteryTagName = tag; |
| } |
| // mTagHandler.handleTag(true, tag, mSpannableStringBuilder, |
| // mReader, mysteryTagContent); |
| } |
| } |
| |
| private void handleEndTag(String tag) { |
| if (mPost != null) { |
| if (!mPost.isLocalDraft()) |
| return; |
| } |
| if (!mysteryTagFound) { |
| if (tag.equalsIgnoreCase("br")) { |
| handleBr(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("p")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("div")) { |
| handleP(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("em")) { |
| end(mSpannableStringBuilder, Italic.class, new StyleSpan( |
| Typeface.ITALIC)); |
| } else if (tag.equalsIgnoreCase("b")) { |
| end(mSpannableStringBuilder, Bold.class, new StyleSpan( |
| Typeface.BOLD)); |
| } else if (tag.equalsIgnoreCase("strong")) { |
| end(mSpannableStringBuilder, Bold.class, new StyleSpan( |
| Typeface.BOLD)); |
| } else if (tag.equalsIgnoreCase("cite")) { |
| end(mSpannableStringBuilder, Italic.class, new StyleSpan( |
| Typeface.ITALIC)); |
| } else if (tag.equalsIgnoreCase("dfn")) { |
| end(mSpannableStringBuilder, Italic.class, new StyleSpan( |
| Typeface.ITALIC)); |
| } else if (tag.equalsIgnoreCase("i")) { |
| end(mSpannableStringBuilder, Italic.class, new StyleSpan( |
| Typeface.ITALIC)); |
| } else if (tag.equalsIgnoreCase("big")) { |
| end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan( |
| 1.25f)); |
| } else if (tag.equalsIgnoreCase("small")) { |
| end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan( |
| 0.8f)); |
| } else if (tag.equalsIgnoreCase("font")) { |
| endFont(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("blockquote")) { |
| handleP(mSpannableStringBuilder); |
| end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan()); |
| } else if (tag.equalsIgnoreCase("tt")) { |
| end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan( |
| "monospace")); |
| } else if (tag.equalsIgnoreCase("a")) { |
| endA(mSpannableStringBuilder); |
| } else if (tag.equalsIgnoreCase("u")) { |
| end(mSpannableStringBuilder, Underline.class, |
| new WPUnderlineSpan()); |
| } else if (tag.equalsIgnoreCase("sup")) { |
| end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); |
| } else if (tag.equalsIgnoreCase("sub")) { |
| end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); |
| } else if (tag.equalsIgnoreCase("strike")) { |
| end(mSpannableStringBuilder, Strike.class, |
| new StrikethroughSpan()); |
| } else if (tag.length() == 2 |
| && Character.toLowerCase(tag.charAt(0)) == 'h' |
| && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { |
| handleP(mSpannableStringBuilder); |
| endHeader(mSpannableStringBuilder); |
| } |
| } else { |
| if (tag.equalsIgnoreCase("html") || tag.equalsIgnoreCase("body")) { |
| return; |
| } |
| |
| if (mysteryTagName.equals(tag)) { |
| mysteryTagFound = false; |
| mSpannableStringBuilder.append(mysteryTagContent); |
| } |
| // mTagHandler.handleTag(false, tag, mSpannableStringBuilder, |
| // mReader, |
| // mysteryTagContent); |
| } |
| } |
| |
| private static void handleP(SpannableStringBuilder text) { |
| int len = text.length(); |
| |
| if (len >= 1 && text.charAt(len - 1) == '\n') { |
| if (len >= 2 && text.charAt(len - 2) == '\n') { |
| return; |
| } |
| |
| text.append("\n"); |
| return; |
| } |
| |
| if (len != 0) { |
| text.append("\n\n"); |
| } |
| } |
| |
| private static void handleBr(SpannableStringBuilder text) { |
| text.append("\n"); |
| } |
| |
| private static Object getLast(Spanned text, Class<?> kind) { |
| /* |
| * This knows that the last returned object from getSpans() will be the |
| * most recently added. |
| */ |
| Object[] objs = text.getSpans(0, text.length(), kind); |
| |
| if (objs.length == 0) { |
| return null; |
| } else { |
| return objs[objs.length - 1]; |
| } |
| } |
| |
| private static void start(SpannableStringBuilder text, Object mark) { |
| int len = text.length(); |
| text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); |
| } |
| |
| private static void end(SpannableStringBuilder text, Class<?> kind, |
| Object repl) { |
| int len = text.length(); |
| Object obj = getLast(text, kind); |
| int where = text.getSpanStart(obj); |
| if (where < 0) |
| where = 0; |
| |
| text.removeSpan(obj); |
| |
| if (where != len) { |
| text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| |
| return; |
| } |
| |
| private void startImg(SpannableStringBuilder text, Attributes attributes, WPHtml.ImageGetter img) { |
| if (mContext == null) return; |
| |
| String src = attributes.getValue("android-uri"); |
| |
| Bitmap resizedBitmap = null; |
| try { |
| resizedBitmap = ImageUtils.getWPImageSpanThumbnailFromFilePath(mContext, src, mMaxImageWidth); |
| if (resizedBitmap == null && src != null) { |
| if (src.contains("video")) { |
| resizedBitmap = BitmapFactory.decodeResource(mContext.getResources(), org.wordpress.android.editor.R.drawable.media_movieclip); |
| } else { |
| resizedBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.media_image_placeholder); |
| } |
| } |
| } catch (OutOfMemoryError e) { |
| CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC, AppLog.T.UTILS); |
| } |
| |
| if (resizedBitmap != null) { |
| int len = text.length(); |
| text.append("\uFFFC"); |
| |
| Uri curStream = Uri.parse(src); |
| |
| if (curStream == null) { |
| return; |
| } |
| |
| WPImageSpan is = new WPImageSpan(mContext, resizedBitmap, curStream); |
| |
| // get the MediaFile data from db |
| MediaFile mf = WordPress.wpDB.getMediaFile(src, mPost); |
| if (mf != null) { |
| is.setMediaFile(mf); |
| is.setImageSource(curStream); |
| text.setSpan(is, len, text.length(), |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| AlignmentSpan.Standard as = new AlignmentSpan.Standard( |
| Layout.Alignment.ALIGN_CENTER); |
| text.setSpan(as, text.getSpanStart(is), text.getSpanEnd(is), |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } else if (mPost != null) { |
| if (mPost.isLocalDraft()) { |
| if (attributes != null) { |
| text.append("<img"); |
| for (int i = 0; i < attributes.getLength(); i++) { |
| String aName = attributes.getLocalName(i); // Attr name |
| if ("".equals(aName)) |
| aName = attributes.getQName(i); |
| text.append(" "); |
| text.append(aName + "=\"" + attributes.getValue(i) + "\""); |
| } |
| text.append(" />\n"); |
| } |
| } |
| } else if (src == null) { |
| |
| //get regular src value from <img/> tag's src attribute |
| src = attributes.getValue("", "src"); |
| Drawable d = null; |
| |
| if (img != null) { |
| d = img.getDrawable(src); |
| } |
| |
| if (d != null) { |
| int len = text.length(); |
| text.append("\uFFFC"); |
| |
| text.setSpan(new ImageSpan(d, src), len, text.length(), |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } else { |
| // noop - we're not showing a default image here |
| } |
| |
| } |
| } |
| |
| private static void startFont(SpannableStringBuilder text, |
| Attributes attributes) { |
| String color = attributes.getValue("", "color"); |
| String face = attributes.getValue("", "face"); |
| |
| int len = text.length(); |
| text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK); |
| } |
| |
| private static void endFont(SpannableStringBuilder text) { |
| int len = text.length(); |
| Object obj = getLast(text, Font.class); |
| int where = text.getSpanStart(obj); |
| |
| text.removeSpan(obj); |
| |
| if (where != len) { |
| Font f = (Font) obj; |
| |
| if (!TextUtils.isEmpty(f.mColor)) { |
| if (f.mColor.startsWith("@")) { |
| Resources res = Resources.getSystem(); |
| String name = f.mColor.substring(1); |
| int colorRes = res.getIdentifier(name, "color", "android"); |
| if (colorRes != 0) { |
| ColorStateList colors = res.getColorStateList(colorRes); |
| text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, |
| null), where, len, |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } else { |
| int c = getHtmlColor(f.mColor); |
| if (c != -1) { |
| text.setSpan(new ForegroundColorSpan(c | 0xFF000000), |
| where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } |
| } |
| |
| if (f.mFace != null) { |
| text.setSpan(new TypefaceSpan(f.mFace), where, len, |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } |
| } |
| |
| private static void startA(SpannableStringBuilder text, |
| Attributes attributes) { |
| String href = attributes.getValue("", "href"); |
| |
| int len = text.length(); |
| text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK); |
| } |
| |
| private static void endA(SpannableStringBuilder text) { |
| int len = text.length(); |
| Object obj = getLast(text, Href.class); |
| int where = text.getSpanStart(obj); |
| |
| text.removeSpan(obj); |
| |
| if (where != len) { |
| Href h = (Href) obj; |
| |
| if (h != null) { |
| if (h.mHref != null) { |
| text.setSpan(new URLSpan(h.mHref), where, len, |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } |
| } |
| } |
| |
| private static void endHeader(SpannableStringBuilder text) { |
| int len = text.length(); |
| Object obj = getLast(text, Header.class); |
| |
| int where = text.getSpanStart(obj); |
| |
| text.removeSpan(obj); |
| |
| // Back off not to change only the text, not the blank line. |
| while (len > where && text.charAt(len - 1) == '\n') { |
| len--; |
| } |
| |
| if (where != len) { |
| Header h = (Header) obj; |
| |
| text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]), where, |
| len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| text.setSpan(new StyleSpan(Typeface.BOLD), where, len, |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } |
| |
| public void setDocumentLocator(Locator locator) { |
| } |
| |
| public void startDocument() throws SAXException { |
| } |
| |
| public void endDocument() throws SAXException { |
| } |
| |
| public void startPrefixMapping(String prefix, String uri) |
| throws SAXException { |
| } |
| |
| public void endPrefixMapping(String prefix) throws SAXException { |
| } |
| |
| public void startElement(String uri, String localName, String qName, |
| Attributes attributes) throws SAXException { |
| if (!mysteryTagFound) { |
| mysteryTagContent = ""; |
| } |
| |
| String eName = localName; // element name |
| if ("".equals(eName)) |
| eName = qName; // not namespace-aware |
| mysteryTagContent += "<" + eName; |
| if (attributes != null) { |
| for (int i = 0; i < attributes.getLength(); i++) { |
| String aName = attributes.getLocalName(i); // Attr name |
| if ("".equals(aName)) |
| aName = attributes.getQName(i); |
| mysteryTagContent += " "; |
| mysteryTagContent += aName + "=\"" + attributes.getValue(i) |
| + "\""; |
| } |
| } |
| mysteryTagContent += ">"; |
| |
| handleStartTag(localName, attributes); |
| } |
| |
| public void endElement(String uri, String localName, String qName) |
| throws SAXException { |
| if (mysteryTagFound) { |
| mysteryTagContent += "</" + localName + ">" + "\n"; |
| } |
| handleEndTag(localName); |
| } |
| |
| public void characters(char ch[], int start, int length) |
| throws SAXException { |
| StringBuilder sb = new StringBuilder(); |
| |
| /* |
| * Ignore whitespace that immediately follows other whitespace; newlines |
| * count as spaces. |
| */ |
| |
| for (int i = 0; i < length; i++) { |
| char c = ch[i + start]; |
| |
| if (c == ' ' || c == '\n') { |
| char pred; |
| int len = sb.length(); |
| |
| if (len == 0) { |
| len = mSpannableStringBuilder.length(); |
| |
| if (len == 0) { |
| pred = '\n'; |
| } else { |
| pred = mSpannableStringBuilder.charAt(len - 1); |
| } |
| } else { |
| pred = sb.charAt(len - 1); |
| } |
| |
| if (pred != ' ' && pred != '\n') { |
| sb.append(' '); |
| } |
| } else { |
| sb.append(c); |
| } |
| } |
| |
| try { |
| if (mysteryTagFound) { |
| if (sb.length() < length) |
| mysteryTagContent += sb.toString().substring(start, |
| length - 1); |
| else |
| mysteryTagContent += sb.toString().substring(start, length); |
| } else |
| mSpannableStringBuilder.append(sb); |
| } catch (RuntimeException e) { |
| AppLog.e(T.UTILS, e); |
| } |
| } |
| |
| public void ignorableWhitespace(char ch[], int start, int length) |
| throws SAXException { |
| } |
| |
| public void processingInstruction(String target, String data) |
| throws SAXException { |
| } |
| |
| public void skippedEntity(String name) throws SAXException { |
| } |
| |
| private static class Bold { |
| } |
| |
| private static class Italic { |
| } |
| |
| private static class Underline { |
| } |
| |
| private static class Big { |
| } |
| |
| private static class Small { |
| } |
| |
| private static class Monospace { |
| } |
| |
| private static class Blockquote { |
| } |
| |
| private static class Super { |
| } |
| |
| private static class Sub { |
| } |
| |
| private static class Strike { |
| } |
| |
| private static class Font { |
| public String mColor; |
| public String mFace; |
| |
| public Font(String color, String face) { |
| mColor = color; |
| mFace = face; |
| } |
| } |
| |
| private static class Href { |
| public String mHref; |
| |
| public Href(String href) { |
| mHref = href; |
| } |
| } |
| |
| private static class Header { |
| private int mLevel; |
| |
| public Header(int level) { |
| mLevel = level; |
| } |
| } |
| |
| private static HashMap<String, Integer> COLORS = buildColorMap(); |
| |
| private static HashMap<String, Integer> buildColorMap() { |
| HashMap<String, Integer> map = new HashMap<String, Integer>(); |
| map.put("aqua", 0x00FFFF); |
| map.put("black", 0x000000); |
| map.put("blue", 0x0000FF); |
| map.put("fuchsia", 0xFF00FF); |
| map.put("green", 0x008000); |
| map.put("grey", 0x808080); |
| map.put("lime", 0x00FF00); |
| map.put("maroon", 0x800000); |
| map.put("navy", 0x000080); |
| map.put("olive", 0x808000); |
| map.put("purple", 0x800080); |
| map.put("red", 0xFF0000); |
| map.put("silver", 0xC0C0C0); |
| map.put("teal", 0x008080); |
| map.put("white", 0xFFFFFF); |
| map.put("yellow", 0xFFFF00); |
| return map; |
| } |
| |
| /** |
| * Converts an HTML color (named or numeric) to an integer RGB value. |
| * |
| * @param color |
| * Non-null color string. |
| * @return A color value, or {@code -1} if the color string could not be |
| * interpreted. |
| */ |
| private static int getHtmlColor(String color) { |
| Integer i = COLORS.get(color.toLowerCase()); |
| if (i != null) { |
| return i; |
| } else { |
| try { |
| return convertValueToInt(color, -1); |
| } catch (NumberFormatException nfe) { |
| return -1; |
| } |
| } |
| } |
| |
| public static final int convertValueToInt(CharSequence charSeq, |
| int defaultValue) { |
| if (null == charSeq) |
| return defaultValue; |
| |
| String nm = charSeq.toString(); |
| |
| // XXX This code is copied from Integer.decode() so we don't |
| // have to instantiate an Integer! |
| |
| int sign = 1; |
| int index = 0; |
| int len = nm.length(); |
| int base = 10; |
| |
| if ('-' == nm.charAt(0)) { |
| sign = -1; |
| index++; |
| } |
| |
| if ('0' == nm.charAt(index)) { |
| // Quick check for a zero by itself |
| if (index == (len - 1)) |
| return 0; |
| |
| char c = nm.charAt(index + 1); |
| |
| if ('x' == c || 'X' == c) { |
| index += 2; |
| base = 16; |
| } else { |
| index++; |
| base = 8; |
| } |
| } else if ('#' == nm.charAt(index)) { |
| index++; |
| base = 16; |
| } |
| |
| return Integer.parseInt(nm.substring(index), base) * sign; |
| } |
| } |