diff --git a/app/src/main/java/io/github/hidroh/materialistic/AppUtils.java b/app/src/main/java/io/github/hidroh/materialistic/AppUtils.java index df5479af4..72268fc59 100644 --- a/app/src/main/java/io/github/hidroh/materialistic/AppUtils.java +++ b/app/src/main/java/io/github/hidroh/materialistic/AppUtils.java @@ -92,6 +92,7 @@ public class AppUtils { public static final int HOT_FACTOR = 3; private static final String HOST_ITEM = "item"; private static final String HOST_USER = "user"; + private static final HtmlCodeTagHandler CODE_TAG_HANDLER = new HtmlCodeTagHandler(); public static void openWebUrlExternal(Context context, @Nullable WebItem item, String url, @Nullable CustomTabsSession session) { @@ -184,14 +185,16 @@ public static CharSequence fromHtml(String htmlText, boolean compact) { if (TextUtils.isEmpty(htmlText)) { return null; } - CharSequence spanned; + final CharSequence spanned; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //noinspection InlinedApi spanned = Html.fromHtml(htmlText, compact ? - Html.FROM_HTML_MODE_COMPACT : Html.FROM_HTML_MODE_LEGACY); + Html.FROM_HTML_MODE_COMPACT : Html.FROM_HTML_MODE_LEGACY, + null, + CODE_TAG_HANDLER); } else { //noinspection deprecation - spanned = Html.fromHtml(htmlText); + spanned = Html.fromHtml(htmlText, null, CODE_TAG_HANDLER); } return trim(spanned); } diff --git a/app/src/main/java/io/github/hidroh/materialistic/HtmlCodeTagHandler.java b/app/src/main/java/io/github/hidroh/materialistic/HtmlCodeTagHandler.java new file mode 100644 index 000000000..c49a4090f --- /dev/null +++ b/app/src/main/java/io/github/hidroh/materialistic/HtmlCodeTagHandler.java @@ -0,0 +1,71 @@ +package io.github.hidroh.materialistic; + +import android.text.Editable; +import android.text.Html; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.TypefaceSpan; + +import org.xml.sax.XMLReader; + +/** + * TagHandler that 'knows' how to handle code tags. + * This handler can be used in the Html.fromHtml method to support formatting of code tags using the monospace typeface. + * + * Hackernews returns <pre><code> blocks, but we are only interested in the inner code. + * The pre tag is then ignored by the Html.fromHtml default tag handler. + */ +public class HtmlCodeTagHandler implements Html.TagHandler { + @Override + public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { + if (tag.equalsIgnoreCase("code")) { + if (opening) { + start(output, new Monospace()); + } else { + end(output, Monospace.class, new TypefaceSpan("monospace")); + } + } + } + + // This is copied from android.text.HtmlToSpannedConverter#start + private static void start(Editable text, Object mark) { + int len = text.length(); + text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + + // This is copied from android.text.HtmlToSpannedConverter#end + private static void end(Editable text, Class kind, Object repl) { + Object obj = getLast(text, kind); + if (obj != null) { + setSpanFromMark(text, obj, repl); + } + } + + // This is copied from android.text.HtmlToSpannedConverter#setSpanFromMark + private static void setSpanFromMark(Spannable text, Object mark, Object... spans) { + int where = text.getSpanStart(mark); + text.removeSpan(mark); + int len = text.length(); + if (where != len) { + for (Object span : spans) { + text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + // This is copied from android.text.HtmlToSpannedConverter#getLast + private static T getLast(Spanned text, Class kind) { + /* + * This knows that the last returned object from getSpans() + * will be the most recently added. + */ + T[] objs = text.getSpans(0, text.length(), kind); + if (objs.length == 0) { + return null; + } else { + return objs[objs.length - 1]; + } + } + // This is copied from android.text.HtmlToSpannedConverter.Monospace + private static class Monospace {} +} diff --git a/app/src/test/java/io/github/hidroh/materialistic/AppUtilsTest.java b/app/src/test/java/io/github/hidroh/materialistic/AppUtilsTest.java index feacd1935..e824e8864 100644 --- a/app/src/test/java/io/github/hidroh/materialistic/AppUtilsTest.java +++ b/app/src/test/java/io/github/hidroh/materialistic/AppUtilsTest.java @@ -10,7 +10,9 @@ import android.support.design.widget.AppBarLayout; import android.support.design.widget.FloatingActionButton; import android.support.v4.content.LocalBroadcastManager; +import android.text.SpannedString; import android.text.format.DateUtils; +import android.text.style.TypefaceSpan; import android.view.ContextThemeWrapper; import android.view.MotionEvent; import android.view.View; @@ -239,6 +241,30 @@ public void testTrimHtmlWhitespaces() { assertThat(textView).hasTextString("paragraph"); } + @Test + public void testPreformattedTextHasMonospaceTypeface() { + TextView textView = new TextView(context); + textView.setText(AppUtils.fromHtml("
val x = myCode()
")); + assertThat(textView).hasTextString("val x = myCode()"); + + SpannedString view = (SpannedString) textView.getText(); + TypefaceSpan[] spans = view.getSpans(0, view.length(), TypefaceSpan.class); + assertThat(spans.length).isEqualTo(1); + assertThat(spans[0].getFamily()).isEqualTo("monospace"); + } + + @Test + public void testPreformattedTextHasMultipleMonospaceTypeface() { + TextView textView = new TextView(context); + textView.setText(AppUtils.fromHtml("
val x = myCode()

some more text

val y = myCode()

And more text")); + + SpannedString view = (SpannedString) textView.getText(); + TypefaceSpan[] spans = view.getSpans(0, view.length(), TypefaceSpan.class); + assertThat(spans.length).isEqualTo(2); + assertThat(spans[0].getFamily()).isEqualTo("monospace"); + assertThat(spans[1].getFamily()).isEqualTo("monospace"); + } + @Test public void testOpenExternalUrlNoConnection() { shadowOf((ConnectivityManager) context