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.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.ParagraphStyle; import android.text.style.QuoteSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; import android.text.style.SuperscriptSpan; 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.fluxc.model.PostModel; import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.helpers.MediaGallery; 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.XMLReader; 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, PostModel 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, PostModel 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(Locale.US, "<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 = " "; 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); } } } }