aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/util/WPHtml.java')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/util/WPHtml.java1225
1 files changed, 1225 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java b/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java
new file mode 100644
index 000000000..d44083dee
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/util/WPHtml.java
@@ -0,0 +1,1225 @@
+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
+ * &lt;img&gt; 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
+ * &lt;img&gt; 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("&lt;"); } else if (c == '>') {
+ * out.append("&gt;"); } else if (c == '&') { out.append("&amp;");
+ * if (c > 0x7E || c < ' ') { out.append("&#" + ((int) c) + ";"); }
+ * else
+ */
+ if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ 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;
+ }
+}