diff options
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.java | 1225 |
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 + * <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; + } +} |