diff --git a/CHANGELOG.md b/CHANGELOG.md index 1910f583..405beaaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +# 4.2.1 +* Fix SpannableBuilder `subSequence` method +* Introduce Nougat check in `BulletListItemSpan` to position bullet (for bullets to be +positioned correctly when nested inside other `LeadingMarginSpan`s) +* Reduced number of invalidations in AsyncDrawable when result is ready +* AsyncDrawable#hasKnownDimentions -> AsyncDrawable#hasKnownDimensions typo fix + # 4.2.0 * `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`) * `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174]) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e5a6805..311a39e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,6 @@ package="io.noties.markwon.app"> -
[![markwon](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20) -[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon) +[![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](https://github.com/noties/Markwon/actions) **Markwon** is a markdown library for Android. It parses markdown following with the help of amazing library @@ -79,7 +79,26 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht ::: -## Awesome Markwon + + + +
+ +## # Awesome Markwon + +
Applications using Markwon: @@ -88,9 +107,12 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht * [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds. + Extension/plugins: * [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background. diff --git a/docs/docs/v4/image/README.md b/docs/docs/v4/image/README.md index 62cb8e77..efd3b867 100644 --- a/docs/docs/v4/image/README.md +++ b/docs/docs/v4/image/README.md @@ -184,11 +184,12 @@ imagesPlugin.addSchemeHandler(new SchemeHandler() { :::warning If you wish to add support for **SVG** or **GIF** you must explicitly add these dependencies to your project: -* for `SVG`: `com.caverock:androidsvg:1.4` -* for `GIF`: `pl.droidsonroids.gif:android-gif-drawable:1.2.14` +* to support `SVG`: [com.caverock:androidsvg](https://github.com/BigBadaboom/androidsvg) +* to support `GIF`: [pl.droidsonroids.gif:android-gif-drawable](https://github.com/koral--/android-gif-drawable) -You can try more recent versions of these libraries, but make sure that they doesn't -introduce any unexpected behavior. +For [security reasons](https://github.com/noties/Markwon/issues/186) it's advisable to use latest +versions of these libraries. If you notice compilation and/or runtime issues when used with Markwon, +please [create an issue](https://github.com/noties/Markwon/issues/new) specifying library and library version used. ::: diff --git a/gradle.properties b/gradle.properties index b1d69d1b..b0b8871f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ android.enableJetifier=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=4.2.0 +VERSION_NAME=4.2.1 GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android diff --git a/markwon-core/src/main/java/io/noties/markwon/SpannableBuilder.java b/markwon-core/src/main/java/io/noties/markwon/SpannableBuilder.java index 6a15a9a8..8d4568d2 100644 --- a/markwon-core/src/main/java/io/noties/markwon/SpannableBuilder.java +++ b/markwon-core/src/main/java/io/noties/markwon/SpannableBuilder.java @@ -187,7 +187,7 @@ public class SpannableBuilder implements Appendable, CharSequence { // if a span was fully including resulting subSequence it's start and // end must be within 0..length bounds s = Math.max(0, span.start - start); - e = Math.max(length, s + (span.end - span.start)); + e = Math.min(length, s + (span.end - span.start)); builder.setSpan( span.what, diff --git a/markwon-core/src/main/java/io/noties/markwon/core/spans/BulletListItemSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/BulletListItemSpan.java index 6d8da746..c548ab66 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/spans/BulletListItemSpan.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/spans/BulletListItemSpan.java @@ -4,6 +4,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; import android.text.Layout; import android.text.style.LeadingMarginSpan; @@ -15,6 +16,13 @@ import io.noties.markwon.utils.LeadingMarginUtils; public class BulletListItemSpan implements LeadingMarginSpan { + private static final boolean IS_NOUGAT; + + static { + final int sdk = Build.VERSION.SDK_INT; + IS_NOUGAT = Build.VERSION_CODES.N == sdk || Build.VERSION_CODES.N_MR1 == sdk; + } + private MarkwonTheme theme; private final Paint paint = ObjectsPool.paint(); @@ -62,28 +70,41 @@ public class BulletListItemSpan implements LeadingMarginSpan { final int marginLeft = (width - side) / 2; - // @since 2.0.2 - // There is a bug in Android Nougat, when this span receives an `x` that - // doesn't correspond to what it should be (text is placed correctly though). - // Let's make this a general rule -> manually calculate difference between expected/actual - // and add this difference to resulting left/right values. If everything goes well - // we do not encounter a bug -> this `diff` value will be 0 - final int diff; - if (dir < 0) { - // rtl - diff = x - (layout.getWidth() - (width * level)); - } else { - diff = (width * level) - x; - } - // in order to support RTL final int l; final int r; { - final int left = x + (dir * marginLeft); - final int right = left + (dir * side); - l = Math.min(left, right) + (dir * diff); - r = Math.max(left, right) + (dir * diff); + // @since 4.2.1 to correctly position bullet + // when nested inside other LeadingMarginSpans (sorry, Nougat) + if (IS_NOUGAT) { + + // @since 2.0.2 + // There is a bug in Android Nougat, when this span receives an `x` that + // doesn't correspond to what it should be (text is placed correctly though). + // Let's make this a general rule -> manually calculate difference between expected/actual + // and add this difference to resulting left/right values. If everything goes well + // we do not encounter a bug -> this `diff` value will be 0 + final int diff; + if (dir < 0) { + // rtl + diff = x - (layout.getWidth() - (width * level)); + } else { + diff = (width * level) - x; + } + + final int left = x + (dir * marginLeft); + final int right = left + (dir * side); + l = Math.min(left, right) + (dir * diff); + r = Math.max(left, right) + (dir * diff); + + } else { + if (dir > 0) { + l = x + marginLeft; + } else { + l = x - width + marginLeft; + } + r = l + side; + } } final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2); diff --git a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawable.java b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawable.java index 62ea138a..ce6f92d5 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawable.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawable.java @@ -55,6 +55,7 @@ public class AsyncDrawable extends Drawable { /** * @since 4.0.0 */ + @SuppressWarnings("WeakerAccess") @Nullable public ImageSize getImageSize() { return imageSize; @@ -63,20 +64,33 @@ public class AsyncDrawable extends Drawable { /** * @since 4.0.0 */ + @SuppressWarnings("unused") @NonNull public ImageSizeResolver getImageSizeResolver() { return imageSizeResolver; } /** + * @see #hasKnownDimensions() * @since 4.0.0 + * @deprecated 4.2.1 */ + @SuppressWarnings({"unused", "WeakerAccess"}) + @Deprecated public boolean hasKnownDimentions() { return canvasWidth > 0; } /** - * @see #hasKnownDimentions() + * @since 4.2.1 + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + public boolean hasKnownDimensions() { + return canvasWidth > 0; + } + + /** + * @see #hasKnownDimensions() * @since 4.0.0 */ public int getLastKnownCanvasWidth() { @@ -84,9 +98,10 @@ public class AsyncDrawable extends Drawable { } /** - * @see #hasKnownDimentions() + * @see #hasKnownDimensions() * @since 4.0.0 */ + @SuppressWarnings("WeakerAccess") public float getLastKnowTextSize() { return textSize; } @@ -95,6 +110,7 @@ public class AsyncDrawable extends Drawable { return result; } + @SuppressWarnings("WeakerAccess") public boolean hasResult() { return result != null; } @@ -104,10 +120,17 @@ public class AsyncDrawable extends Drawable { } // yeah - public void setCallback2(@Nullable Callback callback) { + @SuppressWarnings("WeakerAccess") + public void setCallback2(@Nullable Callback cb) { - this.callback = callback; - super.setCallback(callback); + // @since 4.2.1 + // wrap callback so invalidation happens to this AsyncDrawable instance + // and not for wrapped result/placeholder + this.callback = cb == null + ? null + : new WrappedCallback(cb); + + super.setCallback(cb); // if not null -> means we are attached if (callback != null) { @@ -138,6 +161,7 @@ public class AsyncDrawable extends Drawable { /** * @since 3.0.1 */ + @SuppressWarnings("WeakerAccess") protected void setPlaceholderResult(@NonNull Drawable placeholder) { // okay, if placeholder has bounds -> use it, otherwise use original imageSize @@ -175,7 +199,6 @@ public class AsyncDrawable extends Drawable { } this.result = result; - this.result.setCallback(callback); initBounds(); } @@ -210,6 +233,12 @@ public class AsyncDrawable extends Drawable { final Rect bounds = resolveBounds(); result.setBounds(bounds); + // @since 4.2.1, we set callback after bounds are resolved + // to reduce number of invalidations + result.setCallback(callback); + + // so, this method will check if there is previous bounds and call invalidate _BEFORE_ + // applying new bounds. This is why it is important to have initial bounds empty. setBounds(bounds); invalidateSelf(); @@ -291,6 +320,7 @@ public class AsyncDrawable extends Drawable { return imageSizeResolver.resolveImageSize(this); } + @NonNull @Override public String toString() { return "AsyncDrawable{" + @@ -302,4 +332,30 @@ public class AsyncDrawable extends Drawable { ", waitingForDimensions=" + waitingForDimensions + '}'; } + + // @since 4.2.1 + // Wrapped callback to trigger invalidation for this AsyncDrawable instance (and not result/placeholder) + private class WrappedCallback implements Callback { + + private final Callback callback; + + WrappedCallback(@NonNull Callback callback) { + this.callback = callback; + } + + @Override + public void invalidateDrawable(@NonNull Drawable who) { + callback.invalidateDrawable(AsyncDrawable.this); + } + + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + callback.scheduleDrawable(AsyncDrawable.this, what, when); + } + + @Override + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + callback.unscheduleDrawable(AsyncDrawable.this, what); + } + } } diff --git a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableSpan.java b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableSpan.java index 0d70b62a..915adf8d 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableSpan.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableSpan.java @@ -42,11 +42,9 @@ public class AsyncDrawableSpan extends ReplacementSpan { this.alignment = alignment; this.replacementTextIsLink = replacementTextIsLink; - // additionally set intrinsic bounds if empty - final Rect rect = drawable.getBounds(); - if (rect.isEmpty()) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } + // @since 4.2.1 we do not set intrinsic bounds + // at this point they will always be 0,0-1,1, but this + // will trigger another invalidation when we will have bounds } @Override diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ee887f8c..5e0ae714 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:windowSoftInputMode="adjustResize" /> +
diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java index db937d19..4cdd6d73 100644 --- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java @@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity; import io.noties.markwon.sample.customextension2.CustomExtensionActivity2; import io.noties.markwon.sample.editor.EditorActivity; import io.noties.markwon.sample.html.HtmlActivity; +import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity; import io.noties.markwon.sample.inlineparser.InlineParserActivity; import io.noties.markwon.sample.latex.LatexActivity; import io.noties.markwon.sample.precomputed.PrecomputedActivity; @@ -127,6 +128,10 @@ public class MainActivity extends Activity { activity = InlineParserActivity.class; break; + case HTML_DETAILS: + activity = HtmlDetailsActivity.class; + break; + default: throw new IllegalStateException("No Activity is associated with sample-item: " + item); } diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java index 221ee0bc..36b13cd2 100644 --- a/sample/src/main/java/io/noties/markwon/sample/Sample.java +++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java @@ -25,7 +25,9 @@ public enum Sample { EDITOR(R.string.sample_editor), - INLINE_PARSER(R.string.sample_inline_parser); + INLINE_PARSER(R.string.sample_inline_parser), + + HTML_DETAILS(R.string.sample_html_details); private final int textResId; diff --git a/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java b/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java new file mode 100644 index 00000000..8c668cf4 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/htmldetails/HtmlDetailsActivity.java @@ -0,0 +1,410 @@ +package io.noties.markwon.sample.htmldetails; + +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.Bundle; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.text.style.LeadingMarginSpan; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import io.noties.markwon.Markwon; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.html.HtmlPlugin; +import io.noties.markwon.html.HtmlTag; +import io.noties.markwon.html.MarkwonHtmlRenderer; +import io.noties.markwon.html.TagHandler; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.sample.R; +import io.noties.markwon.utils.LeadingMarginUtils; +import io.noties.markwon.utils.NoCopySpannableFactory; + +public class HtmlDetailsActivity extends Activity { + + private ViewGroup content; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_html_details); + + content = findViewById(R.id.content); + + sample_details(); + } + + private void sample_details() { + + final String md = "# Hello\n\n
\n" + + " stuff with \n\n*mark* **down**\n\n\n" + + "

\n\n" + + "\n" + + "## *formatted* **heading** with [a](link)\n" + + "```java\n" + + "code block\n" + + "```\n" + + "\n" + + "

\n" + + " nested stuff

\n" + + "\n" + + "\n" + + "* list\n" + + "* with\n" + + "\n\n" + + "![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" + + " 1. nested\n" + + " 1. items\n" + + "\n" + + " ```java\n" + + " // including code\n" + + " ```\n" + + " 1. blocks\n" + + "\n" + + "

The 3rd!\n\n" + + "**bold** _em_\n
" + + "

\n" + + "

\n\n" + + "and **this** *is* how..."; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(HtmlPlugin.create(plugin -> + plugin.addHandler(new DetailsTagHandler()))) + .usePlugin(ImagesPlugin.create()) + .build(); + + final Spanned spanned = markwon.toMarkdown(md); + final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class); + + // if we have no details, proceed as usual (single text-view) + if (spans == null || spans.length == 0) { + // no details + final TextView textView = appendTextView(); + markwon.setParsedMarkdown(textView, spanned); + return; + } + + final List list = new ArrayList<>(); + + for (DetailsParsingSpan span : spans) { + final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list); + if (e != null) { + list.add(e); + } + } + + for (DetailsElement element : list) { + initDetails(element, spanned); + } + + sort(list); + + + TextView textView; + int start = 0; + + for (DetailsElement element : list) { + + if (element.start != start) { + // subSequence and add new TextView + textView = appendTextView(); + textView.setText(subSequenceTrimmed(spanned, start, element.start)); + } + + // now add details TextView + textView = appendTextView(); + initDetailsTextView(markwon, textView, element); + + start = element.end; + } + + if (start != spanned.length()) { + // another textView with rest content + textView = appendTextView(); + textView.setText(subSequenceTrimmed(spanned, start, spanned.length())); + } + } + + @NonNull + private TextView appendTextView() { + final View view = getLayoutInflater().inflate(R.layout.view_html_details_text_view, content, false); + final TextView textView = view.findViewById(R.id.text); + content.addView(view); + return textView; + } + + private void initDetailsTextView( + @NonNull Markwon markwon, + @NonNull TextView textView, + @NonNull DetailsElement element) { + + // minor optimization + textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); + + // so, each element with children is a details tag + // there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans +// final SpannableStringBuilder builder = new SpannableStringBuilder(); + final SpannableBuilder builder = new SpannableBuilder(); + append(builder, markwon, textView, element, element); + markwon.setParsedMarkdown(textView, builder.spannableStringBuilder()); + } + + private void append( + @NonNull SpannableBuilder builder, + @NonNull Markwon markwon, + @NonNull TextView textView, + @NonNull DetailsElement root, + @NonNull DetailsElement element) { + if (!element.children.isEmpty()) { + + final int start = builder.length(); + +// builder.append(element.content); + builder.append(subSequenceTrimmed(element.content, 0, element.content.length())); + + builder.setSpan(new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + element.expanded = !element.expanded; + + initDetailsTextView(markwon, textView, root); + } + }, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (element.expanded) { + for (DetailsElement child : element.children) { + append(builder, markwon, textView, root, child); + } + } + + builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start); + + } else { + builder.append(element.content); + } + } + + // if null -> remove from where it was processed, + // else replace from where it was processed with a new one (can become expandable) + @Nullable + private static DetailsElement settle( + @NonNull DetailsElement element, + @NonNull List elements) { + for (DetailsElement e : elements) { + if (element.start > e.start && element.end <= e.end) { + final DetailsElement settled = settle(element, e.children); + if (settled != null) { + + // the thing is we must balance children if done like this + // let's just create a tree actually, so we are easier to modify + final Iterator iterator = e.children.iterator(); + while (iterator.hasNext()) { + final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element)); + if (balanced == null) { + iterator.remove(); + } + } + + // add to our children + e.children.add(element); + } + return null; + } + } + return element; + } + + private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) { + int end = element.end; + for (int i = element.children.size() - 1; i >= 0; i--) { + final DetailsElement child = element.children.get(i); + if (child.end < end) { + element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end))); + } + initDetails(child, spanned); + end = child.start; + } + + final int start = (element.start + element.content.length()); + if (end != start) { + element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end))); + } + } + + private static void sort(@NonNull List elements) { + Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start)); + for (DetailsElement element : elements) { + sort(element.children); + } + } + + @NonNull + private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) { + + while (start < end) { + + final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start)); + final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1)); + + if (!isStartEmpty && !isEndEmpty) { + break; + } + + if (isStartEmpty) { + start += 1; + } + if (isEndEmpty) { + end -= 1; + } + } + + return cs.subSequence(start, end); + } + + private static class DetailsElement { + + final int start; + final int end; + final CharSequence content; + final List children = new ArrayList<>(0); + + boolean expanded; + + DetailsElement(int start, int end, @NonNull CharSequence content) { + this.start = start; + this.end = end; + this.content = content; + } + + @Override + public String toString() { + return "DetailsElement{" + + "start=" + start + + ", end=" + end + + ", content=" + toStringContent(content) + + ", children=" + children + + ", expanded=" + expanded + + '}'; + } + + @NonNull + private static String toStringContent(@NonNull CharSequence cs) { + return cs.toString().replaceAll("\n", "\\n"); + } + } + + private static class DetailsTagHandler extends TagHandler { + + @Override + public void handle( + @NonNull MarkwonVisitor visitor, + @NonNull MarkwonHtmlRenderer renderer, + @NonNull HtmlTag tag) { + + int summaryEnd = -1; + + for (HtmlTag child : tag.getAsBlock().children()) { + + if (!child.isClosed()) { + continue; + } + + if ("summary".equals(child.name())) { + summaryEnd = child.end(); + } + + final TagHandler tagHandler = renderer.tagHandler(child.name()); + if (tagHandler != null) { + tagHandler.handle(visitor, renderer, child); + } else if (child.isBlock()) { + visitChildren(visitor, renderer, child.getAsBlock()); + } + } + + if (summaryEnd > -1) { + visitor.builder().setSpan(new DetailsParsingSpan( + subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd) + ), tag.start(), tag.end()); + } + } + + @NonNull + @Override + public Collection supportedTags() { + return Collections.singleton("details"); + } + } + + private static class DetailsParsingSpan { + + final CharSequence summary; + + DetailsParsingSpan(@NonNull CharSequence summary) { + this.summary = summary; + } + } + + private static class DetailsSpan implements LeadingMarginSpan { + + private final DetailsElement element; + private final int blockMargin; + private final int blockQuoteWidth; + + private final Rect rect = new Rect(); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) { + this.element = element; + this.blockMargin = theme.getBlockMargin(); + this.blockQuoteWidth = theme.getBlockQuoteWidth(); + this.paint.setStyle(Paint.Style.FILL); + } + + @Override + public int getLeadingMargin(boolean first) { + return blockMargin; + } + + @Override + public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { + + if (LeadingMarginUtils.selfStart(start, text, this)) { + rect.set(x, top, x + blockMargin, bottom); + if (element.expanded) { + paint.setColor(Color.GREEN); + } else { + paint.setColor(Color.RED); + } + paint.setStyle(Paint.Style.FILL); + c.drawRect(rect, paint); + + } else { + + if (element.expanded) { + final int l = (blockMargin - blockQuoteWidth) / 2; + rect.set(x + l, top, x + l + blockQuoteWidth, bottom); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.GRAY); + c.drawRect(rect, paint); + } + } + } + } +} diff --git a/sample/src/main/res/layout/activity_html_details.xml b/sample/src/main/res/layout/activity_html_details.xml new file mode 100644 index 00000000..7a61f5d8 --- /dev/null +++ b/sample/src/main/res/layout/activity_html_details.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/view_html_details_text_view.xml b/sample/src/main/res/layout/view_html_details_text_view.xml new file mode 100644 index 00000000..36775ea9 --- /dev/null +++ b/sample/src/main/res/layout/view_html_details_text_view.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml index a26f62c5..d87585fd 100644 --- a/sample/src/main/res/values/strings-samples.xml +++ b/sample/src/main/res/values/strings-samples.xml @@ -29,4 +29,6 @@ # \# Inline Parser\n\nUsage of custom inline parser + # \# HTML <details> tag\n\n<details> tag parsed and rendered + \ No newline at end of file