From fc73e08bea1fbe0a5db857441590b73606521bbb Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 1 Apr 2020 11:38:35 +0300 Subject: [PATCH 01/17] Merge with master --- CHANGELOG.md | 2 ++ gradle.properties | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 886bd169..696a88d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +# $nap; + # 4.3.1 * Fix DexGuard optimization issue ([#216])
Thanks [@francescocervone] * module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name diff --git a/gradle.properties b/gradle.properties index 0e222c26..3d9ba174 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.3.1 +VERSION_NAME=4.3.2-SNAPSHOT GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android From 3006f8d486750a3794262580f64932d5a3f413d2 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 16:56:28 +0300 Subject: [PATCH 02/17] prepare 4.4.0-SNAPSHOT version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3d9ba174..c1d97523 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.3.2-SNAPSHOT +VERSION_NAME=4.4.0-SNAPSHOT GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android From b497f872e5920087f0304d522a2868eb5fab02ea Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 16:57:43 +0300 Subject: [PATCH 03/17] TextViewSpan and TextLayoutSpan --- CHANGELOG.md | 6 + .../io/noties/markwon/core/CorePlugin.java | 9 + .../markwon/core/spans/TextLayoutSpan.java | 70 +++++ .../markwon/core/spans/TextViewSpan.java | 64 +++++ .../markwon/image/AsyncDrawableSpan.java | 7 +- .../java/io/noties/markwon/utils/Dip.java | 1 + .../io/noties/markwon/utils/DumpNodes.java | 1 + .../io/noties/markwon/utils/LayoutUtils.java | 72 ++++++ .../markwon/utils/LeadingMarginUtils.java | 1 - .../io/noties/markwon/utils/SpanUtils.java | 42 +++ .../markwon/ext/tables/TableRowSpan.java | 26 +- .../markwon/html/HtmlEmptyTagReplacement.java | 4 + .../io/noties/markwon/html/HtmlPlugin.java | 18 +- markwon-spans-better/build.gradle | 20 ++ markwon-spans-better/gradle.properties | 4 + .../src/main/AndroidManifest.xml | 1 + .../spans/better/BetterUnderlineSpan.java | 183 +++++++++++++ sample/src/main/AndroidManifest.xml | 7 +- .../basicplugins/BasicPluginsActivity.java | 21 ++ .../sample/basicplugins/CodeTextView.java | 192 ++++++++++++++ .../markwon/sample/editor/EditorActivity.java | 20 +- .../sample/editor/MarkdownNewLine.java | 129 ++++++++++ .../sample/html/ElegantUnderlineSpan.java | 242 ++++++++++++++++++ .../markwon/sample/html/HtmlActivity.java | 94 ++++++- .../html/HtmlElegantUnderlineTagHandler.java | 38 +++ .../sample/html/HtmlFontTagHandler.java | 42 +++ .../markwon/sample/html/IFrameHtmlPlugin.java | 48 ++++ .../inlineparser/InlineParserActivity.java | 73 +++++- settings.gradle | 1 + 29 files changed, 1421 insertions(+), 15 deletions(-) create mode 100644 markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java create mode 100644 markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java create mode 100644 markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java create mode 100644 markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java create mode 100644 markwon-spans-better/build.gradle create mode 100644 markwon-spans-better/gradle.properties create mode 100644 markwon-spans-better/src/main/AndroidManifest.xml create mode 100644 markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 696a88d6..dff5b68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog # $nap; +* `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`) +* `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed) +* `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) +* `AsyncDrawableLoader` now uses `TextView` width without padding instead of width of canvas + +[#235]: https://github.com/noties/Markwon/issues/235 # 4.3.1 * Fix DexGuard optimization issue ([#216])
Thanks [@francescocervone] diff --git a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java index 29c63a2a..941ebacd 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java @@ -1,5 +1,6 @@ package io.noties.markwon.core; +import android.text.Spannable; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.widget.TextView; @@ -48,6 +49,7 @@ import io.noties.markwon.core.factory.StrongEmphasisSpanFactory; import io.noties.markwon.core.factory.ThematicBreakSpanFactory; import io.noties.markwon.core.spans.OrderedListItemSpan; +import io.noties.markwon.core.spans.TextViewSpan; import io.noties.markwon.image.ImageProps; /** @@ -150,6 +152,13 @@ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { OrderedListItemSpan.measure(textView, markdown); + + // @since $nap; + // we do not break API compatibility, instead we introduce the `instance of` check + if (markdown instanceof Spannable) { + final Spannable spannable = (Spannable) markdown; + TextViewSpan.applyTo(spannable, textView); + } } @Override diff --git a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java new file mode 100644 index 00000000..679bd58c --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java @@ -0,0 +1,70 @@ +package io.noties.markwon.core.spans; + +import android.text.Layout; +import android.text.Spannable; +import android.text.Spanned; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + +/** + * @since $nap; + */ +public class TextLayoutSpan { + + /** + * @see #applyTo(Spannable, Layout) + */ + @Nullable + public static Layout layoutOf(@NonNull CharSequence cs) { + if (cs instanceof Spanned) { + return layoutOf((Spanned) cs); + } + return null; + } + + @Nullable + public static Layout layoutOf(@NonNull Spanned spanned) { + final TextLayoutSpan[] spans = spanned.getSpans( + 0, + spanned.length(), + TextLayoutSpan.class + ); + return spans != null && spans.length > 0 + ? spans[0].layout() + : null; + } + + public static void applyTo(@NonNull Spannable spannable, @NonNull Layout layout) { + + // remove all current ones (only one should be present) + final TextLayoutSpan[] spans = spannable.getSpans(0, spannable.length(), TextLayoutSpan.class); + if (spans != null) { + for (TextLayoutSpan span : spans) { + spannable.removeSpan(span); + } + } + + final TextLayoutSpan span = new TextLayoutSpan(layout); + spannable.setSpan( + span, + 0, + spannable.length(), + Spanned.SPAN_INCLUSIVE_INCLUSIVE + ); + } + + private final WeakReference reference; + + @SuppressWarnings("WeakerAccess") + TextLayoutSpan(@NonNull Layout layout) { + this.reference = new WeakReference<>(layout); + } + + @Nullable + public Layout layout() { + return reference.get(); + } +} diff --git a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java new file mode 100644 index 00000000..3e527a9f --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java @@ -0,0 +1,64 @@ +package io.noties.markwon.core.spans; + +import android.text.Spannable; +import android.text.Spanned; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + +/** + * A special span that allows to obtain {@code TextView} in which spans are displayed + * + * @since $nap; + */ +public class TextViewSpan { + + @Nullable + public static TextView textViewOf(@NonNull CharSequence cs) { + if (cs instanceof Spanned) { + return textViewOf((Spanned) cs); + } + return null; + } + + @Nullable + public static TextView textViewOf(@NonNull Spanned spanned) { + final TextViewSpan[] spans = spanned.getSpans(0, spanned.length(), TextViewSpan.class); + return spans != null && spans.length > 0 + ? spans[0].textView() + : null; + } + + public static void applyTo(@NonNull Spannable spannable, @NonNull TextView textView) { + + final TextViewSpan[] spans = spannable.getSpans(0, spannable.length(), TextViewSpan.class); + if (spans != null) { + for (TextViewSpan span : spans) { + spannable.removeSpan(span); + } + } + + final TextViewSpan span = new TextViewSpan(textView); + // `SPAN_INCLUSIVE_INCLUSIVE` to persist in case of possible text change (deletion, etc) + spannable.setSpan( + span, + 0, + spannable.length(), + Spanned.SPAN_INCLUSIVE_INCLUSIVE + ); + } + + private final WeakReference reference; + + public TextViewSpan(@NonNull TextView textView) { + this.reference = new WeakReference<>(textView); + } + + @Nullable + public TextView textView() { + return reference.get(); + } +} 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 915adf8d..fe34b4c9 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 @@ -14,6 +14,7 @@ import java.lang.annotation.RetentionPolicy; import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.utils.SpanUtils; @SuppressWarnings("WeakerAccess") public class AsyncDrawableSpan extends ReplacementSpan { @@ -99,7 +100,11 @@ public void draw( int bottom, @NonNull Paint paint) { - drawable.initWithKnownDimensions(canvas.getWidth(), paint.getTextSize()); + // @since $nap; use SpanUtils instead of `canvas.getWidth` + drawable.initWithKnownDimensions( + SpanUtils.width(canvas, text), + paint.getTextSize() + ); final AsyncDrawable drawable = this.drawable; diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/Dip.java b/markwon-core/src/main/java/io/noties/markwon/utils/Dip.java index 8b579356..fe9d48dc 100644 --- a/markwon-core/src/main/java/io/noties/markwon/utils/Dip.java +++ b/markwon-core/src/main/java/io/noties/markwon/utils/Dip.java @@ -18,6 +18,7 @@ public static Dip create(float density) { private final float density; + @SuppressWarnings("WeakerAccess") public Dip(float density) { this.density = density; } diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/DumpNodes.java b/markwon-core/src/main/java/io/noties/markwon/utils/DumpNodes.java index ffa2bf86..474021e7 100644 --- a/markwon-core/src/main/java/io/noties/markwon/utils/DumpNodes.java +++ b/markwon-core/src/main/java/io/noties/markwon/utils/DumpNodes.java @@ -12,6 +12,7 @@ import java.lang.reflect.Proxy; // utility class to print parsed Nodes hierarchy +@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class DumpNodes { public interface NodeProcessor { diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java new file mode 100644 index 00000000..d1c4cb77 --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java @@ -0,0 +1,72 @@ +package io.noties.markwon.utils; + +import android.os.Build; +import android.text.Layout; + +import androidx.annotation.NonNull; + +/** + * @since $nap; + */ +public abstract class LayoutUtils { + + private static final float DEFAULT_EXTRA = 0F; + private static final float DEFAULT_MULTIPLIER = 1F; + + public static int getLineBottomWithoutPaddingAndSpacing( + @NonNull Layout layout, + int line + ) { + + final int bottom = layout.getLineBottom(line); + final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + final boolean isSpanLastLine = line == (layout.getLineCount() - 1); + + final int lineBottom; + final float lineSpacingExtra = layout.getSpacingAdd(); + final float lineSpacingMultiplier = layout.getSpacingMultiplier(); + + // simplified check + final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA + || lineSpacingMultiplier != DEFAULT_MULTIPLIER; + + if (!hasLineSpacing + || (isSpanLastLine && lastLineSpacingNotAdded)) { + lineBottom = bottom; + } else { + final float extra; + if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) { + final int lineHeight = getLineHeight(layout, line); + extra = lineHeight - + ((lineHeight - lineSpacingExtra) / lineSpacingMultiplier); + } else { + extra = lineSpacingExtra; + } + lineBottom = (int) (bottom - extra + .5F); + } + + // check if it is the last line that span is occupying **and** that this line is the last + // one in TextView + if (isSpanLastLine + && (line == layout.getLineCount() - 1)) { + return lineBottom - layout.getBottomPadding(); + } + + return lineBottom; + } + + public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) { + final int top = layout.getLineTop(line); + if (line == 0) { + return top - layout.getTopPadding(); + } + return top; + } + + public static int getLineHeight(@NonNull Layout layout, int line) { + return layout.getLineTop(line + 1) - layout.getLineTop(line); + } + + private LayoutUtils() { + } +} diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/LeadingMarginUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/LeadingMarginUtils.java index 072d2ccf..601ad470 100644 --- a/markwon-core/src/main/java/io/noties/markwon/utils/LeadingMarginUtils.java +++ b/markwon-core/src/main/java/io/noties/markwon/utils/LeadingMarginUtils.java @@ -4,7 +4,6 @@ public abstract class LeadingMarginUtils { - @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean selfStart(int start, CharSequence text, Object span) { return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start; } diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java new file mode 100644 index 00000000..96396add --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java @@ -0,0 +1,42 @@ +package io.noties.markwon.utils; + +import android.graphics.Canvas; +import android.text.Layout; +import android.text.Spanned; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import io.noties.markwon.core.spans.TextLayoutSpan; +import io.noties.markwon.core.spans.TextViewSpan; + +/** + * @since $nap; + */ +public abstract class SpanUtils { + + public static int width(@NonNull Canvas canvas, @NonNull CharSequence cs) { + // Layout + // TextView + // canvas + + if (cs instanceof Spanned) { + final Spanned spanned = (Spanned) cs; + + // if we are displayed with layout information -> use it + final Layout layout = TextLayoutSpan.layoutOf(spanned); + if (layout != null) { + return layout.getWidth(); + } + + // if we have TextView -> obtain width from it (exclude padding) + final TextView textView = TextViewSpan.textViewOf(spanned); + if (textView != null) { + return textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight(); + } + } + + // else just use canvas width + return canvas.getWidth(); + } +} diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java index 0248c4ef..32abef40 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java @@ -5,6 +5,8 @@ import android.graphics.Paint; import android.graphics.Rect; import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableString; import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; @@ -20,7 +22,9 @@ import java.util.ArrayList; import java.util.List; +import io.noties.markwon.core.spans.TextLayoutSpan; import io.noties.markwon.utils.LeadingMarginUtils; +import io.noties.markwon.utils.SpanUtils; public class TableRowSpan extends ReplacementSpan { @@ -144,7 +148,7 @@ public void draw( int bottom, @NonNull Paint p) { - if (recreateLayouts(canvas.getWidth())) { + if (recreateLayouts(SpanUtils.width(canvas, text))) { width = canvas.getWidth(); // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc if (p instanceof TextPaint) { @@ -295,17 +299,31 @@ private void makeNewLayouts() { this.layouts.clear(); Cell cell; StaticLayout layout; + Spannable spannable; + for (int i = 0, size = cells.size(); i < size; i++) { + cell = cells.get(i); + + if (cell.text instanceof Spannable) { + spannable = (Spannable) cell.text; + } else { + spannable = new SpannableString(cell.text); + } + layout = new StaticLayout( - cell.text, + spannable, textPaint, w, alignment(cell.alignment), - 1.F, - .0F, + 1.0F, + 0.0F, false ); + + // @since $nap; + TextLayoutSpan.applyTo(spannable, layout); + layouts.add(layout); } } diff --git a/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java index 48b8acb4..8ad5f05a 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java @@ -21,6 +21,7 @@ public static HtmlEmptyTagReplacement create() { } private static final String IMG_REPLACEMENT = "\uFFFC"; + private static final String IFRAME_REPLACEMENT = "\u00a0"; // non-breakable space /** * @return replacement for supplied startTag or null if no replacement should occur (which will @@ -44,6 +45,9 @@ public String replace(@NonNull HtmlTag tag) { } else { replacement = alt; } + } else if ("iframe".equals(name)) { + // @since $nap; make iframe non-empty + replacement = IFRAME_REPLACEMENT; } else { replacement = null; } diff --git a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java index c2c13c12..5e974ea0 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java @@ -53,13 +53,16 @@ public static HtmlPlugin create(@NonNull HtmlConfigure configure) { public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; private final MarkwonHtmlRendererImpl.Builder builder; - private final MarkwonHtmlParser htmlParser; + + private MarkwonHtmlParser htmlParser; private MarkwonHtmlRenderer htmlRenderer; + // @since $nap; + private HtmlEmptyTagReplacement emptyTagReplacement = new HtmlEmptyTagReplacement(); + @SuppressWarnings("WeakerAccess") HtmlPlugin() { this.builder = new MarkwonHtmlRendererImpl.Builder(); - this.htmlParser = MarkwonHtmlParserImpl.create(); } /** @@ -104,6 +107,16 @@ public HtmlPlugin excludeDefaults(boolean excludeDefaults) { return this; } + /** + * @param emptyTagReplacement {@link HtmlEmptyTagReplacement} + * @since $nap; + */ + @NonNull + public HtmlPlugin emptyTagReplacement(@NonNull HtmlEmptyTagReplacement emptyTagReplacement) { + this.emptyTagReplacement = emptyTagReplacement; + return this; + } + @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) { @@ -128,6 +141,7 @@ public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configu builder.addDefaultTagHandler(new HeadingHandler()); } + htmlParser = MarkwonHtmlParserImpl.create(emptyTagReplacement); htmlRenderer = builder.build(); } diff --git a/markwon-spans-better/build.gradle b/markwon-spans-better/build.gradle new file mode 100644 index 00000000..5272858b --- /dev/null +++ b/markwon-spans-better/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' + +android { + + compileSdkVersion config['compile-sdk'] + buildToolsVersion config['build-tools'] + + defaultConfig { + minSdkVersion config['min-sdk'] + targetSdkVersion config['target-sdk'] + versionCode 1 + versionName version + } +} + +dependencies { + api project(':markwon-core') +} + +registerArtifact(this) diff --git a/markwon-spans-better/gradle.properties b/markwon-spans-better/gradle.properties new file mode 100644 index 00000000..4d3a5f47 --- /dev/null +++ b/markwon-spans-better/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=Spans Better +POM_ARTIFACT_ID=spans-better +POM_DESCRIPTION=Better spans +POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-spans-better/src/main/AndroidManifest.xml b/markwon-spans-better/src/main/AndroidManifest.xml new file mode 100644 index 00000000..92bb979a --- /dev/null +++ b/markwon-spans-better/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java b/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java new file mode 100644 index 00000000..2de53b78 --- /dev/null +++ b/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java @@ -0,0 +1,183 @@ +package io.noties.markwon.spans.better; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.Build; +import android.text.Layout; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.LineBackgroundSpan; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import io.noties.markwon.core.spans.TextViewSpan; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline) + * + * @since $nap; + */ +public class BetterUnderlineSpan implements LineBackgroundSpan { + + public enum Type { + @RequiresApi(Build.VERSION_CODES.KITKAT) + PATH, + REGION + } + + private static final float UNDERLINE_CLEAR_GAP = 5.5F; + + private final Path underline = new Path(); + private final Path outline = new Path(); + private final Paint stroke = new Paint(); + private final Path strokedOutline = new Path(); + private char[] chars; + + BetterUnderlineSpan() { + stroke.setStyle(Paint.Style.FILL_AND_STROKE); + stroke.setStrokeCap(Paint.Cap.BUTT); + } + + @Override + public void drawBackground( + Canvas c, + Paint p, + int left, + int right, + int top, + int baseline, + int bottom, + CharSequence text, + int start, + int end, + int lnum + ) { + final Spanned spanned = (Spanned) text; + final TextView textView = TextViewSpan.textViewOf(spanned); + + if (textView == null) { + // no, cannot do it, the whole text will be changed +// p.setUnderlineText(true); + return; + } + + final Layout layout = textView.getLayout(); + + final int selfStart = spanned.getSpanStart(this); + final int selfEnd = spanned.getSpanEnd(this); + + // TODO: also doesn't mean that it is last line, imagine text after span is ended + final boolean isLastLine = end == selfEnd || (selfEnd == (end - 1)); + + final int s = max(selfStart, start); + + // e - 1, but only if not last? + // oh... layout line count != span lines.. + final int e = min(selfEnd, end) - (isLastLine ? 0 : 1); + + final int l = (int) (layout.getPrimaryHorizontal(s) + .5F); + final int r = (int) (layout.getPrimaryHorizontal(e) + .5F); + final int b = getLineBottom(layout, lnum, isLastLine); + + final float density = textView.getResources().getDisplayMetrics().density; + + underline.rewind(); + // TODO: proper baseline +// underline.addRect( +// l, b - (1.8F * density), +// r, b, +// Path.Direction.CW +// +// ); + + // TODO: this must be configured somehow... + final int diff = (int) (p.descent() / 2F + .5F); + + underline.addRect( + l, baseline + diff, + r, baseline + diff + (density * 0.8F), + Path.Direction.CW + ); + + + outline.rewind(); + + // reallocate only if less, otherwise re-use and then send actual indexes + // TODO: would this return proper array for the last line?! + chars = new char[e - s]; + TextUtils.getChars(spanned, s, e, chars, 0); + p.getTextPath( + chars, + 0, (e - s), + l, baseline, + outline + ); + + final Paint paint = new Paint(); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(Color.GREEN); + c.drawPath(outline, paint); + + outline.op(underline, Path.Op.INTERSECT); + + strokedOutline.rewind(); + stroke.setStrokeWidth(UNDERLINE_CLEAR_GAP * density); + stroke.getFillPath(outline, strokedOutline); + + underline.op(strokedOutline, Path.Op.DIFFERENCE); + + c.drawPath(underline, p); + } + + private static final float DEFAULT_EXTRA = 0F; + private static final float DEFAULT_MULTIPLIER = 1F; + + private static int getLineBottom(@NonNull Layout layout, int line, boolean isLastLine) { + + final int bottom = layout.getLineBottom(line); + final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // TODO: layout line count != span occupied lines +// final boolean isLastLine = line == layout.getLineCount() - 1; + + final int lineBottom; + final float lineSpacingExtra = layout.getSpacingAdd(); + final float lineSpacingMultiplier = layout.getSpacingMultiplier(); + + // simplified check + final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA + || lineSpacingMultiplier != DEFAULT_MULTIPLIER; + + if (!hasLineSpacing + || (isLastLine && lastLineSpacingNotAdded)) { + lineBottom = bottom; + } else { + final float extra; + if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) { + final int lineHeight = getLineHeight(layout, line); + extra = lineHeight - + ((lineHeight - lineSpacingExtra) / lineSpacingMultiplier); + } else { + extra = lineSpacingExtra; + } + lineBottom = (int) (bottom - extra + .5F); + } + + if (isLastLine) { + return lineBottom - layout.getBottomPadding(); + } + + return lineBottom; + } + + private static int getLineHeight(@NonNull Layout layout, int line) { + return layout.getLineTop(line + 1) - layout.getLineTop(line); + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index cda45b5d..190d2c05 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -24,7 +24,11 @@ - + + + @@ -32,6 +36,7 @@ diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java index 9b9ffdde..02d3308a 100644 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -434,4 +434,25 @@ private void tableOfContents() { markwon.setMarkdown(textView, md); } + +// private void code() { +// final String md = "" + +// "hello `there`!\n\n" + +// "so this, `is super duper long very very very long line that should be going further and further and further down` yep.\n\n" + +// "`okay`"; +// final Markwon markwon = Markwon.builder(this) +// .usePlugin(new AbstractMarkwonPlugin() { +// @Override +// public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { +// builder.setFactory(Code.class, new SpanFactory() { +// @Override +// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { +// return new CodeTextView.CodeSpan(); +// } +// }); +// } +// }) +// .build(); +// markwon.setMarkdown(textView, md); +// } } diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java new file mode 100644 index 00000000..fe795b63 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/CodeTextView.java @@ -0,0 +1,192 @@ +package io.noties.markwon.sample.basicplugins; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Build; +import android.text.Layout; +import android.text.Spanned; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.noties.debug.Debug; + +@SuppressLint("AppCompatCustomView") +public class CodeTextView extends TextView { + + static class CodeSpan { + } + + private int paddingHorizontal; + private int paddingVertical; + + private float cornerRadius; + private float strokeWidth; + private int strokeColor; + private int backgroundColor; + + public CodeTextView(Context context) { + super(context); + init(context, null); + } + + public CodeTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + private void init(Context context, @Nullable AttributeSet attrs) { + paint.setColor(0xFFff0000); + paint.setStyle(Paint.Style.FILL); + } + + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + @Override + protected void onDraw(Canvas canvas) { + final Layout layout = getLayout(); + if (layout != null) { + draw(this, canvas, layout); + } + super.onDraw(canvas); + } + + private void draw( + @NonNull View view, + @NonNull Canvas canvas, + @NonNull Layout layout + ) { + + final CharSequence cs = layout.getText(); + if (!(cs instanceof Spanned)) { + return; + } + final Spanned spanned = (Spanned) cs; + + final int save = canvas.save(); + try { + canvas.translate(view.getPaddingLeft(), view.getPaddingTop()); + + // TODO: block? + // TODO: we must remove _original_ spans + // TODO: cache (attach a listener?) + // TODO: editor? + + final CodeSpan[] spans = spanned.getSpans(0, spanned.length(), CodeSpan.class); + if (spans != null && spans.length > 0) { + for (CodeSpan span : spans) { + + final int startOffset = spanned.getSpanStart(span); + final int endOffset = spanned.getSpanEnd(span); + + final int startLine = layout.getLineForOffset(startOffset); + final int endLine = layout.getLineForOffset(endOffset); + + // do we need to round them? + final float left = layout.getPrimaryHorizontal(startOffset) + + (-1 * layout.getParagraphDirection(startLine) * paddingHorizontal); + + final float right = layout.getPrimaryHorizontal(endOffset) + + (layout.getParagraphDirection(endLine) * paddingHorizontal); + + final float top = getLineTop(layout, startLine, paddingVertical); + final float bottom = getLineBottom(layout, endLine, paddingVertical); + + Debug.i(new RectF(left, top, right, bottom).toShortString()); + + if (startLine == endLine) { + canvas.drawRect(left, top, right, bottom, paint); + } else { + // draw first line (start until the lineEnd) + // draw everything in-between (startLine - endLine) + // draw last line (lineStart until the end + + canvas.drawRect( + left, + top, + layout.getLineRight(startLine), + getLineBottom(layout, startLine, paddingVertical), + paint + ); + + for (int line = startLine + 1; line < endLine; line++) { + canvas.drawRect( + layout.getLineLeft(line), + getLineTop(layout, line, paddingVertical), + layout.getLineRight(line), + getLineBottom(layout, line, paddingVertical), + paint + ); + } + + canvas.drawRect( + layout.getLineLeft(endLine), + getLineTop(layout, endLine, paddingVertical), + right, + getLineBottom(layout, endLine, paddingVertical), + paint + ); + } + } + } + } finally { + canvas.restoreToCount(save); + } + } + + private static float getLineTop(@NonNull Layout layout, int line, float padding) { + float value = layout.getLineTop(line) - padding; + if (line == 0) { + value -= layout.getTopPadding(); + } + return value; + } + + private static float getLineBottom(@NonNull Layout layout, int line, float padding) { + float value = getLineBottomWithoutSpacing(layout, line) - padding; + if (line == (layout.getLineCount() - 1)) { + value -= layout.getBottomPadding(); + } + return value; + } + + private static float getLineBottomWithoutSpacing(@NonNull Layout layout, int line) { + final float value = layout.getLineBottom(line); + + final boolean isLastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + final boolean isLastLine = line == (layout.getLineCount() - 1); + + final float lineBottomWithoutSpacing; + + final float lineSpacingExtra = layout.getSpacingAdd(); + final float lineSpacingMultiplier = layout.getSpacingMultiplier(); + + final boolean hasLineSpacing = Float.compare(lineSpacingExtra, .0F) != 0 + || Float.compare(lineSpacingMultiplier, 1F) != 0; + + if (!hasLineSpacing || isLastLine && isLastLineSpacingNotAdded) { + lineBottomWithoutSpacing = value; + } else { + final float extra; + if (Float.compare(lineSpacingMultiplier, 1F) != 0) { + final float lineHeight = getLineHeight(layout, line); + extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier; + } else { + extra = lineSpacingExtra; + } + lineBottomWithoutSpacing = value - extra; + } + + return lineBottomWithoutSpacing; + } + + private static float getLineHeight(@NonNull Layout layout, int line) { + return layout.getLineTop(line + 1) - layout.getLineTop(line); + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java index 31a4370e..84a4fdb7 100644 --- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java @@ -6,6 +6,7 @@ import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; +import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.text.style.ForegroundColorSpan; import android.text.style.MetricAffectingSpan; @@ -25,8 +26,11 @@ import java.util.List; import java.util.concurrent.Executors; +import io.noties.debug.AndroidLogDebugOutput; +import io.noties.debug.Debug; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; +import io.noties.markwon.SoftBreakAddsNewLinePlugin; import io.noties.markwon.core.spans.EmphasisSpan; import io.noties.markwon.core.spans.StrongEmphasisSpan; import io.noties.markwon.editor.AbstractEditHandler; @@ -65,7 +69,8 @@ public MenuOptions menuOptions() { .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin) .add("pluginRequire", this::plugin_require) .add("pluginNoDefaults", this::plugin_no_defaults) - .add("heading", this::heading); + .add("heading", this::heading) + .add("newLine", this::newLine); } @Override @@ -98,7 +103,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); createView(); + Debug.init(new AndroidLogDebugOutput(true)); + multiple_edit_spans(); +// newLine(); } private void simple_process() { @@ -230,6 +238,7 @@ public void configureParser(@NonNull Parser.Builder builder) { builder.inlineParserFactory(inlineParserFactory); } }) + .usePlugin(SoftBreakAddsNewLinePlugin.create()) .build(); final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link); @@ -280,6 +289,13 @@ private void multiple_edit_spans_plugin() { editor, Executors.newSingleThreadExecutor(), editText)); } + private void newLine() { + final Markwon markwon = Markwon.create(this); + final MarkwonEditor editor = MarkwonEditor.create(markwon); + final TextWatcher textWatcher = MarkdownNewLine.wrap(MarkwonEditorTextWatcher.withProcess(editor)); + editText.addTextChangedListener(textWatcher); + } + private void plugin_require() { // usage of plugin from other plugins @@ -295,6 +311,8 @@ public void configure(@NonNull Registry registry) { }) .build(); + editText.setMovementMethod(LinkMovementMethod.getInstance()); + final MarkwonEditor editor = MarkwonEditor.create(markwon); editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender( diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java b/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java new file mode 100644 index 00000000..9552f2ba --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/editor/MarkdownNewLine.java @@ -0,0 +1,129 @@ +package io.noties.markwon.sample.editor; + +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; + +import androidx.annotation.NonNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.noties.debug.Debug; + +abstract class MarkdownNewLine { + + @NonNull + static TextWatcher wrap(@NonNull TextWatcher textWatcher) { + return new NewLineTextWatcher(textWatcher); + } + + private MarkdownNewLine() { + } + + private static class NewLineTextWatcher implements TextWatcher { + + // NB! matches only bullet lists + private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$"); + + private final TextWatcher wrapped; + + private boolean selfChange; + + // this content is pending to be inserted at the beginning + private String pendingNewLineContent; + private int pendingNewLineIndex; + + // mark current edited line for removal (range start/end) + private int clearLineStart; + private int clearLineEnd; + + NewLineTextWatcher(@NonNull TextWatcher wrapped) { + this.wrapped = wrapped; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // no op + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (selfChange) { + return; + } + + // just one new character added + if (before == 0 + && count == 1 + && '\n' == s.charAt(start)) { + int end = -1; + for (int i = start - 1; i >= 0; i--) { + if ('\n' == s.charAt(i)) { + end = i + 1; + break; + } + } + + // start at the very beginning + if (end < 0) { + end = 0; + } + + final String pendingNewLineContent; + + final int clearLineStart; + final int clearLineEnd; + + final Matcher matcher = RE.matcher(s.subSequence(end, start)); + if (matcher.matches()) { + // if second group is empty -> remove new line + final String content = matcher.group(2); + Debug.e("new line, content: '%s'", content); + if (TextUtils.isEmpty(content)) { + // another empty new line, remove this start + clearLineStart = end; + clearLineEnd = start; + pendingNewLineContent = null; + } else { + pendingNewLineContent = matcher.group(1); + clearLineStart = clearLineEnd = 0; + } + } else { + pendingNewLineContent = null; + clearLineStart = clearLineEnd = 0; + } + this.pendingNewLineContent = pendingNewLineContent; + this.pendingNewLineIndex = start + 1; + this.clearLineStart = clearLineStart; + this.clearLineEnd = clearLineEnd; + } + } + + @Override + public void afterTextChanged(Editable s) { + if (selfChange) { + return; + } + + if (pendingNewLineContent != null || clearLineStart < clearLineEnd) { + selfChange = true; + try { + if (pendingNewLineContent != null) { + s.insert(pendingNewLineIndex, pendingNewLineContent); + pendingNewLineContent = null; + } else { + s.replace(clearLineStart, clearLineEnd, ""); + clearLineStart = clearLineEnd = 0; + } + } finally { + selfChange = false; + } + } + + // NB, we assume MarkdownEditor text watcher that only listens for this event, + // other text-watchers must be interested in other events also + wrapped.afterTextChanged(s); + } + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java b/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java new file mode 100644 index 00000000..307ea834 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/html/ElegantUnderlineSpan.java @@ -0,0 +1,242 @@ +package io.noties.markwon.sample.html; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.Build; +import android.text.Layout; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.LineBackgroundSpan; +import android.text.style.MetricAffectingSpan; +import android.util.Log; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.annotation.RequiresApi; + +import io.noties.markwon.core.spans.TextLayoutSpan; +import io.noties.markwon.core.spans.TextViewSpan; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline) + *

+ * Failed attempt to create elegant underline as a span + *

    + *
  • in a `TextView` span is rendered, but `draw` method is invoked constantly which put pressure on CPU and memory + *
  • in an `EditText` only the first line draws this underline span (seems to be a weird + * issue between LineBackgroundSpan and EditText). Also, in `EditText` `draw` method is invoked + * constantly (for each drawing of the blinking cursor) + *
  • cannot reliably receive proper text, for example if underline is applied to a text range which has + * different typefaces applied to different words (underline cannot know that, which applied to which) + *
+ */ +// will apply other spans that 100% contain this one, so for example if +// an underline that inside some other spans (different typeface), they won't be applied and thus +// underline would be incorrect +// do not use in editor, due to some obscure thing, LineBackgroundSpan would be applied to the first line only +// also, in editor this span would be redrawn with each blink of the cursor +@RequiresApi(Build.VERSION_CODES.KITKAT) +class ElegantUnderlineSpan implements LineBackgroundSpan { + + private static final float DEFAULT_UNDERLINE_HEIGHT_DIP = 0.8F; + private static final float DEFAULT_UNDERLINE_CLEAR_GAP_DIP = 5.5F; + + @NonNull + public static ElegantUnderlineSpan create() { + return new ElegantUnderlineSpan(0, 0); + } + + @NonNull + public static ElegantUnderlineSpan create(@Px int underlineHeight) { + return new ElegantUnderlineSpan(underlineHeight, 0); + } + + @NonNull + public static ElegantUnderlineSpan create(@Px int underlineHeight, @Px int underlineClearGap) { + return new ElegantUnderlineSpan(underlineHeight, underlineClearGap); + } + + // TODO: underline color? + private final int underlineHeight; + private final int underlineClearGap; + + private final Path underline = new Path(); + private final Path outline = new Path(); + private final Paint stroke = new Paint(); + private final Path strokedOutline = new Path(); + + private final CharCache charCache = new CharCache(); + + private final TextPaint tempTextPaint = new TextPaint(); + + protected ElegantUnderlineSpan(@Px int underlineHeight, @Px int underlineClearGap) { + this.underlineHeight = underlineHeight; + this.underlineClearGap = underlineClearGap; + stroke.setStyle(Paint.Style.FILL_AND_STROKE); + stroke.setStrokeCap(Paint.Cap.BUTT); + } + + // is it possible that LineBackgroundSpan is not receiving proper spans? like typeface? + // it complicates things (like the need to have own copy of paint) + + // is it possible that LineBackgroundSpan is called constantly even in a TextView? + + @Override + public void drawBackground( + Canvas c, + Paint p, + int left, + int right, + int top, + int baseline, + int bottom, + CharSequence text, + int start, + int end, + int lnum + ) { + +// Debug.trace(); + + final Spanned spanned = (Spanned) text; + final TextView textView = TextViewSpan.textViewOf(spanned); + + if (textView == null) { + // TextView is required + Log.e("EU", "no text view"); + return; + } + + final Layout layout; + { + // check if there is dedicated layout, if not, use from textView + // (think tableRowSpan that uses own Layout) + final Layout layoutFromSpan = TextLayoutSpan.layoutOf(spanned); + if (layoutFromSpan != null) { + layout = layoutFromSpan; + } else { + layout = textView.getLayout(); + } + } + + if (layout == null) { + // we could call `p.setUnderlineText(true)` here a fallback, + // but this would make __all__ text in a TextView underlined, which is not + // what we want + Log.e("EU", "no layout"); + return; + } + + tempTextPaint.set((TextPaint) p); + + // we must use _selfStart_ because underline can start **not** at the beginning of a line. + // as we are using LineBackground `start` would indicate the start position of the line + // and not start of the span (self). The same goes for selfEnd (ended before line) + final int selfStart = spanned.getSpanStart(this); + final int selfEnd = spanned.getSpanEnd(this); + + final int s = max(selfStart, start); + + // all lines should use (end - 1) to receive proper line end coordinate X, + // unless it is last line in _layout_ + final boolean isLastLine = lnum == (layout.getLineCount() - 1); + final int e = min(selfEnd, end - (isLastLine ? 0 : 1)); + + if (true) { + Log.e("EU", String.format("lnum: %s, hash: %s, text: '%s'", + lnum, text.subSequence(s, e).hashCode(), text.subSequence(s, e))); + } + + final int leading; + final int trailing; + { + final int l = (int) (layout.getPrimaryHorizontal(s) + .5F); + final int r = (int) (layout.getPrimaryHorizontal(e) + .5F); + leading = min(l, r); + trailing = max(l, r); + } + + underline.rewind(); + + // middle between baseline and descent + final int diff = (int) (p.descent() / 2F + .5F); + + underline.addRect( + leading, baseline + diff, + trailing, baseline + diff + underlineHeight(textView), + Path.Direction.CW + ); + + outline.rewind(); + + final int charsLength = e - s; + final char[] chars = charCache.chars(charsLength); + TextUtils.getChars(spanned, s, e, chars, 0); + + if (true) { + final MetricAffectingSpan[] metricAffectingSpans = spanned.getSpans(s, e, MetricAffectingSpan.class); +// Log.e("EU", Arrays.toString(metricAffectingSpans)); + for (MetricAffectingSpan span : metricAffectingSpans) { + span.updateMeasureState(tempTextPaint); + } + } + + // todo: styleSpan + // todo all other spans (maybe UpdateMeasureSpans?) + tempTextPaint.getTextPath( + chars, + 0, charsLength, + leading, baseline, + outline + ); + + outline.op(underline, Path.Op.INTERSECT); + + strokedOutline.rewind(); + stroke.setStrokeWidth(underlineClearGap(textView)); + stroke.getFillPath(outline, strokedOutline); + + underline.op(strokedOutline, Path.Op.DIFFERENCE); + + c.drawPath(underline, p); + } + + private int underlineHeight(@NonNull TextView textView) { + if (underlineHeight > 0) { + return underlineHeight; + } + return (int) (DEFAULT_UNDERLINE_HEIGHT_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); + } + + private int underlineClearGap(@NonNull TextView textView) { + if (underlineClearGap > 0) { + return underlineClearGap; + } + return (int) (DEFAULT_UNDERLINE_CLEAR_GAP_DIP * textView.getResources().getDisplayMetrics().density + 0.5F); + } + + // primitive cache that grows internal array (never shrinks, nor clear buffer) + // TODO: but... each span has own instance, so not much of the memory saving + private static class CharCache { + + @NonNull + char[] chars(int ofLength) { + final char[] out; + if (chars == null || chars.length < ofLength) { + out = chars = new char[ofLength]; + } else { + out = chars; + } + return out; + } + + private char[] chars; + } +} + diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java index db5ca541..ca18d6c6 100644 --- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java @@ -1,18 +1,18 @@ package io.noties.markwon.sample.html; +import android.os.Build; import android.os.Bundle; import android.text.Layout; import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; -import org.commonmark.node.Paragraph; - import java.util.Collection; import java.util.Collections; import java.util.Random; @@ -23,6 +23,7 @@ import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.RenderProps; import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.html.HtmlEmptyTagReplacement; import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.html.HtmlTag; import io.noties.markwon.html.MarkwonHtmlRenderer; @@ -42,7 +43,10 @@ public MenuOptions menuOptions() { .add("align", this::align) .add("randomCharSize", this::randomCharSize) .add("enhance", this::enhance) - .add("image", this::image); + .add("image", this::image) + .add("elegantUnderline", this::elegantUnderline) + .add("iframe", this::iframe) + .add("emptyTagReplacement", this::emptyTagReplacement); } private TextView textView; @@ -57,7 +61,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { textView = findViewById(R.id.text_view); - align(); + elegantUnderline(); } // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content @@ -268,4 +272,86 @@ private void image() { markwon.setMarkdown(textView, md); } + + private void elegantUnderline() { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + Toast.makeText( + this, + "Elegant underline is supported on KitKat and up", + Toast.LENGTH_LONG + ).show(); + return; + } + + final String underline = "Well well wel, and now Gogogo, quite **perfect** yeah and nice and elegant"; + + final String md = "" + + underline + "\n\n" + + "" + underline + "\n\n" + + "" + underline + "\n\n" + + "" + underline + underline + underline + "\n\n" + + "" + underline + "\n\n" + + ""; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(HtmlPlugin.create(plugin -> plugin + .addHandler(new HtmlFontTagHandler()) + .addHandler(new HtmlElegantUnderlineTagHandler()))) + .build(); + + markwon.setMarkdown(textView, md); + } + + private void iframe() { + final String md = "" + + "# Hello iframe\n\n" + + "

\"JUMP

\n" + + "

 

\n" + + "

Switch owners will soon get to take part in the ultimate Shonen Jump rumble. Bandai Namco announced plans to bring Jump Force to Switch as Jump Force Deluxe Edition, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and Character Pass 2 is also in the works for all versions, starting with Shoto Todoroki from My Hero Academia.

\n" + + "

 

\n" + + "

Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from Hunter x Hunter, Yu Yu Hakusho, Bleach, and JoJo's Bizarre Adventure. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring. 

\n" + + "

 

\n" + + "

\n" + + "

 

\n" + + "

Character Pass 2 promo:

\n" + + "

 

\n" + + "

\n" + + "

 

\n" + + "

\"\"

\n" + + "

 

\n" + + "

-------

\n" + + "

Joseph Luster is the Games and Web editor at Otaku USA Magazine. You can read his webcomic, BIG DUMB FIGHTING IDIOTS at subhumanzoids. Follow him on Twitter @Moldilox. 

"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create()) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new IFrameHtmlPlugin()) + .build(); + + markwon.setMarkdown(textView, md); + } + + private void emptyTagReplacement() { + + final String md = "" + + " the `` is replaced?"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(HtmlPlugin.create(plugin -> { + plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() { + @Nullable + @Override + public String replace(@NonNull HtmlTag tag) { + if ("empty".equals(tag.name())) { + return "REPLACED_EMPTY_WITH_IT"; + } + return super.replace(tag); + } + }); + })) + .build(); + + markwon.setMarkdown(textView, md); + } } diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java new file mode 100644 index 00000000..76a2e5fb --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/html/HtmlElegantUnderlineTagHandler.java @@ -0,0 +1,38 @@ +package io.noties.markwon.sample.html; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.Collection; +import java.util.Collections; + +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.html.HtmlTag; +import io.noties.markwon.html.MarkwonHtmlRenderer; +import io.noties.markwon.html.TagHandler; + +@RequiresApi(Build.VERSION_CODES.KITKAT) +public class HtmlElegantUnderlineTagHandler extends TagHandler { + + @Override + public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { + if (tag.isBlock()) { + visitChildren(visitor, renderer, tag.getAsBlock()); + } + SpannableBuilder.setSpans( + visitor.builder(), + ElegantUnderlineSpan.create(), + tag.start(), + tag.end() + ); + } + + @NonNull + @Override + public Collection supportedTags() { + return Collections.singleton("u"); + } +} \ No newline at end of file diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java new file mode 100644 index 00000000..477cbd59 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/html/HtmlFontTagHandler.java @@ -0,0 +1,42 @@ +package io.noties.markwon.sample.html; + +import android.text.TextUtils; +import android.text.style.TypefaceSpan; + +import androidx.annotation.NonNull; + +import java.util.Collection; +import java.util.Collections; + +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.html.HtmlTag; +import io.noties.markwon.html.MarkwonHtmlRenderer; +import io.noties.markwon.html.TagHandler; + +public class HtmlFontTagHandler extends TagHandler { + + @Override + public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) { + + if (tag.isBlock()) { + visitChildren(visitor, renderer, tag.getAsBlock()); + } + + final String font = tag.attributes().get("name"); + if (!TextUtils.isEmpty(font)) { + SpannableBuilder.setSpans( + visitor.builder(), + new TypefaceSpan(font), + tag.start(), + tag.end() + ); + } + } + + @NonNull + @Override + public Collection supportedTags() { + return Collections.singleton("font"); + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java b/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java new file mode 100644 index 00000000..ddbbfce7 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/html/IFrameHtmlPlugin.java @@ -0,0 +1,48 @@ +package io.noties.markwon.sample.html; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.commonmark.node.Image; + +import java.util.Collection; +import java.util.Collections; + +import io.noties.debug.Debug; +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.RenderProps; +import io.noties.markwon.html.HtmlPlugin; +import io.noties.markwon.html.HtmlTag; +import io.noties.markwon.html.tag.SimpleTagHandler; +import io.noties.markwon.image.ImageProps; +import io.noties.markwon.image.ImageSize; + +public class IFrameHtmlPlugin extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + registry.require(HtmlPlugin.class, htmlPlugin -> { + // TODO: empty tag replacement + htmlPlugin.addHandler(new EmbedTagHandler()); + }); + } + + private static class EmbedTagHandler extends SimpleTagHandler { + + @Nullable + @Override + public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { + final ImageSize imageSize = new ImageSize(new ImageSize.Dimension(640, "px"), new ImageSize.Dimension(480, "px")); + ImageProps.IMAGE_SIZE.set(renderProps, imageSize); + ImageProps.DESTINATION.set(renderProps, "https://hey.com/1.png"); + return configuration.spansFactory().require(Image.class) + .getSpans(configuration, renderProps); + } + + @NonNull + @Override + public Collection supportedTags() { + return Collections.singleton("iframe"); + } + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java index 4e7c87da..718d40e7 100644 --- a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java @@ -6,10 +6,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.commonmark.internal.inline.AsteriskDelimiterProcessor; +import org.commonmark.internal.inline.UnderscoreDelimiterProcessor; import org.commonmark.node.Block; import org.commonmark.node.BlockQuote; +import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.Heading; import org.commonmark.node.HtmlBlock; +import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.ListBlock; import org.commonmark.node.ThematicBreak; import org.commonmark.parser.InlineParserFactory; @@ -22,7 +26,9 @@ import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.inlineparser.BackticksInlineProcessor; +import io.noties.markwon.inlineparser.BangInlineProcessor; import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; +import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.MarkwonInlineParser; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; @@ -41,7 +47,9 @@ public MenuOptions menuOptions() { .add("links_only", this::links_only) .add("disable_code", this::disable_code) .add("pluginWithDefaults", this::pluginWithDefaults) - .add("pluginNoDefaults", this::pluginNoDefaults); + .add("pluginNoDefaults", this::pluginNoDefaults) + .add("disableHtmlInlineParser", this::disableHtmlInlineParser) + .add("disableHtmlSanitize", this::disableHtmlSanitize); } @Override @@ -173,4 +181,67 @@ public void configure(@NonNull Registry registry) { markwon.setMarkdown(textView, md); } + private void disableHtmlInlineParser() { + final String md = "# Html disabled\n\n" + + "emphasis strong\n\n" + + "

paragraph

\n\n" + + "\n\n" + + ""; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(MarkwonInlineParserPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configure(@NonNull Registry registry) { + // NB! `AsteriskDelimiterProcessor` and `UnderscoreDelimiterProcessor` + // handles both emphasis and strong-emphasis nodes + registry.require(MarkwonInlineParserPlugin.class, plugin -> { + plugin.factoryBuilder() + .excludeInlineProcessor(HtmlInlineProcessor.class) + .excludeInlineProcessor(BangInlineProcessor.class) + .excludeInlineProcessor(OpenBracketInlineProcessor.class) + .excludeDelimiterProcessor(AsteriskDelimiterProcessor.class) + .excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class); + }); + } + + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.enabledBlockTypes(new HashSet<>(Arrays.asList( + Heading.class, +// HtmlBlock.class, + ThematicBreak.class, + FencedCodeBlock.class, + IndentedCodeBlock.class, + BlockQuote.class, + ListBlock.class + ))); + } + }) + .build(); + + markwon.setMarkdown(textView, md); + } + + private void disableHtmlSanitize() { + final String md = "# Html disabled\n\n" + + "emphasis strong\n\n" + + "

paragraph

\n\n" + + "\n\n" + + ""; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @NonNull + @Override + public String processMarkdown(@NonNull String markdown) { + return markdown + .replaceAll("<", "<") + .replaceAll(">", ">"); + } + }) + .build(); + + markwon.setMarkdown(textView, md); + } } diff --git a/settings.gradle b/settings.gradle index 8bf10dcb..4d91087c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,5 +16,6 @@ include ':app', ':sample', ':markwon-recycler', ':markwon-recycler-table', ':markwon-simple-ext', + ':markwon-spans-better', ':markwon-syntax-highlight', ':markwon-test-span' From bc38768539eb065899e56e127d452119f54ea0a3 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 17:58:41 +0300 Subject: [PATCH 04/17] Images support inside table cells --- CHANGELOG.md | 1 + .../markwon/ext/latex/JLatexMathPlugin.java | 15 ++- .../markwon/ext/tables/TableRowSpan.java | 108 ++++++++++++++---- .../markwon/sample/latex/LatexActivity.java | 15 ++- .../markwon/sample/table/TableActivity.java | 52 ++++++++- 5 files changed, 163 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff5b68b..0c0de41d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed) * `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) * `AsyncDrawableLoader` now uses `TextView` width without padding instead of width of canvas +* Support for images inside table cells (`ext-tables` module) [#235]: https://github.com/noties/Markwon/issues/235 diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java index 87456358..d6d561b0 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -530,7 +530,20 @@ private static class InlineImageSizeResolver extends ImageSizeResolver { @NonNull @Override public Rect resolveImageSize(@NonNull AsyncDrawable drawable) { - return drawable.getResult().getBounds(); + + // @since $nap; resolve inline size (scale down if exceed available width) + final Rect imageBounds = drawable.getResult().getBounds(); + final int canvasWidth = drawable.getLastKnownCanvasWidth(); + final int w = imageBounds.width(); + + if (w > canvasWidth) { + // here we must scale it down (keeping the ratio) + final float ratio = (float) w / imageBounds.height(); + final int h = (int) (canvasWidth / ratio + .5F); + return new Rect(0, 0, canvasWidth, h); + } + + return imageBounds; } } } diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java index 32abef40..d00c1850 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java @@ -4,6 +4,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; @@ -23,6 +24,8 @@ import java.util.List; import io.noties.markwon.core.spans.TextLayoutSpan; +import io.noties.markwon.image.AsyncDrawable; +import io.noties.markwon.image.AsyncDrawableSpan; import io.noties.markwon.utils.LeadingMarginUtils; import io.noties.markwon.utils.SpanUtils; @@ -71,7 +74,7 @@ public String toString() { private final TableTheme theme; private final List cells; - private final List layouts; + private final List layouts; private final TextPaint textPaint; private final boolean header; private final boolean odd; @@ -112,7 +115,7 @@ public int getSize( if (fm != null) { int max = 0; - for (StaticLayout layout : layouts) { + for (Layout layout : layouts) { final int height = layout.getHeight(); if (height > max) { max = height; @@ -240,7 +243,7 @@ public void draw( final int borderTop = isFirstTableRow ? borderWidth : 0; final int borderBottom = bottom - top - borderWidth; - StaticLayout layout; + Layout layout; for (int i = 0; i < size; i++) { layout = layouts.get(i); final int save = canvas.save(); @@ -297,34 +300,76 @@ private void makeNewLayouts() { final int w = (width / columns) - padding; this.layouts.clear(); - Cell cell; - StaticLayout layout; - Spannable spannable; for (int i = 0, size = cells.size(); i < size; i++) { + makeLayout(i, w, cells.get(i)); + } + } - cell = cells.get(i); + private void makeLayout(final int index, final int width, @NonNull final Cell cell) { - if (cell.text instanceof Spannable) { - spannable = (Spannable) cell.text; - } else { - spannable = new SpannableString(cell.text); + final Runnable recreate = new Runnable() { + @Override + public void run() { + final Invalidator invalidator = TableRowSpan.this.invalidator; + if (invalidator != null) { + layouts.remove(index); + makeLayout(index, width, cell); + invalidator.invalidate(); + } } + }; + + final Spannable spannable; + + if (cell.text instanceof Spannable) { + spannable = (Spannable) cell.text; + } else { + spannable = new SpannableString(cell.text); + } + + final Layout layout = new StaticLayout( + spannable, + textPaint, + width, + alignment(cell.alignment), + 1.0F, + 0.0F, + false + ); + + // @since $nap; + TextLayoutSpan.applyTo(spannable, layout); + + // @since $nap; + scheduleAsyncDrawables(spannable, recreate); + + layouts.add(index, layout); + } + + private void scheduleAsyncDrawables(@NonNull Spannable spannable, @NonNull final Runnable recreate) { + + final AsyncDrawableSpan[] spans = spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); + if (spans != null + && spans.length > 0) { + + for (AsyncDrawableSpan span : spans) { - layout = new StaticLayout( - spannable, - textPaint, - w, - alignment(cell.alignment), - 1.0F, - 0.0F, - false - ); + final AsyncDrawable drawable = span.getDrawable(); - // @since $nap; - TextLayoutSpan.applyTo(spannable, layout); + // it is absolutely crucial to check if drawable is already attached, + // otherwise we would end up with a loop + if (drawable.isAttached()) { + continue; + } - layouts.add(layout); + drawable.setCallback2(new CallbackAdapter() { + @Override + public void invalidateDrawable(@NonNull Drawable who) { + recreate.run(); + } + }); + } } } @@ -348,4 +393,21 @@ private static Layout.Alignment alignment(@Alignment int alignment) { public void invalidator(@Nullable Invalidator invalidator) { this.invalidator = invalidator; } + + private static abstract class CallbackAdapter implements Drawable.Callback { + @Override + public void invalidateDrawable(@NonNull Drawable who) { + + } + + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + + } + + @Override + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + + } + } } diff --git a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java index b8a7d9e3..d2cd69ac 100644 --- a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java @@ -68,7 +68,8 @@ public MenuOptions menuOptions() { .add("textColor", this::textColor) .add("defaultTextColor", this::defaultTextColor) .add("inlineAndBlock", this::inlineAndBlock) - .add("dark", this::dark); + .add("dark", this::dark) + .add("omega", this::omega); } @Override @@ -221,6 +222,18 @@ private void dark() { renderWithBlocksAndInlines(md); } + private void omega() { + final String md = "" + + "# Block\n\n" + + "$$\n" + + "\\Omega\n" + + "$$\n\n" + + "# Inline\n\n" + + "$$\\Omega$$"; + + renderWithBlocksAndInlines(md); + } + @NonNull private static String wrapLatexInSampleMarkdown(@NonNull String latex) { return "" + diff --git a/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java b/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java index d7153ec6..3dade55c 100644 --- a/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/table/TableActivity.java @@ -7,10 +7,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.noties.debug.Debug; import io.noties.markwon.Markwon; +import io.noties.markwon.ext.latex.JLatexMathPlugin; import io.noties.markwon.ext.tables.TablePlugin; -import io.noties.markwon.ext.tables.TableTheme; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.sample.ActivityWithMenuOptions; import io.noties.markwon.sample.MenuOptions; @@ -25,7 +26,9 @@ public class TableActivity extends ActivityWithMenuOptions { public MenuOptions menuOptions() { return MenuOptions.create() .add("customize", this::customize) - .add("tableAndLinkify", this::tableAndLinkify); + .add("tableAndLinkify", this::tableAndLinkify) + .add("withImages", this::withImages) + .add("withLatex", this::withLatex); } private TextView textView; @@ -86,4 +89,47 @@ private void tableAndLinkify() { markwon.setMarkdown(textView, md); } + + private void withImages() { + + final String md = "" + + "| HEADER | HEADER |\n" + + "|:----:|:----:|\n" + + "| ![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg) | Build |\n" + + "| Stable | ![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable) |\n" + + "| BIG | ![image](https://images.pexels.com/photos/41171/brussels-sprouts-sprouts-cabbage-grocery-41171.jpeg) |\n" + + "\n"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create()) + .usePlugin(TablePlugin.create(this)) + .build(); + + markwon.setMarkdown(textView, md); + } + + private void withLatex() { + + String latex = "\\begin{array}{cc}"; + latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; + latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; + latex += "\\end{array}"; + + final String md = "" + + "| HEADER | HEADER |\n" + + "|:----:|:----:|\n" + + "| ![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg) | Build |\n" + + "| Stable | ![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable) |\n" + + "| BIG | $$" + latex + "$$ |\n" + + "\n"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(MarkwonInlineParserPlugin.create()) + .usePlugin(ImagesPlugin.create()) + .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.inlinesEnabled(true))) + .usePlugin(TablePlugin.create(this)) + .build(); + + markwon.setMarkdown(textView, md); + } } From ab83dad618ea80229e7b67782b617f5afbbca18e Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 18:00:00 +0300 Subject: [PATCH 05/17] Update jlatexmath implicit dependency 0.1.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 51aa9d6c..64061d83 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'android-svg' : 'com.caverock:androidsvg:1.4', 'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', - 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.1', + 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.2', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'io.noties:prism4j:2.0.0', 'debug' : 'io.noties:debug:5.0.0@jar', From a135e07f16dd453487d0075ee78cf658e7b3ceb6 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 18:14:07 +0300 Subject: [PATCH 06/17] Expose enabled block types in CorePlugin --- CHANGELOG.md | 1 + .../io/noties/markwon/core/CorePlugin.java | 22 +++++++++++++ .../markwon/sample/core/CoreActivity.java | 33 ++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0de41d..ad07bb3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) * `AsyncDrawableLoader` now uses `TextView` width without padding instead of width of canvas * Support for images inside table cells (`ext-tables` module) +* expose `enabledBlockTypes` in `CorePlugin` [#235]: https://github.com/noties/Markwon/issues/235 diff --git a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java index 941ebacd..08c1c45a 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import org.commonmark.node.Block; import org.commonmark.node.BlockQuote; import org.commonmark.node.BulletList; import org.commonmark.node.Code; @@ -16,6 +17,7 @@ import org.commonmark.node.FencedCodeBlock; import org.commonmark.node.HardLineBreak; import org.commonmark.node.Heading; +import org.commonmark.node.HtmlBlock; import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; @@ -30,7 +32,10 @@ import org.commonmark.node.ThematicBreak; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.MarkwonConfiguration; @@ -90,6 +95,23 @@ public static CorePlugin create() { return new CorePlugin(); } + /** + * @return a set with enabled by default block types + * @since $nap; + */ + @NonNull + public static Set> enabledBlockTypes() { + return new HashSet<>(Arrays.asList( + BlockQuote.class, + Heading.class, + FencedCodeBlock.class, + HtmlBlock.class, + ThematicBreak.class, + ListBlock.class, + IndentedCodeBlock.class + )); + } + // @since 4.0.0 private final List onTextAddedListeners = new ArrayList<>(0); diff --git a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java index 19c6d3dd..7ce59464 100644 --- a/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/core/CoreActivity.java @@ -8,8 +8,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.commonmark.node.Block; +import org.commonmark.node.BlockQuote; import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import java.util.Set; + +import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.Markwon; import io.noties.markwon.core.CorePlugin; import io.noties.markwon.sample.ActivityWithMenuOptions; @@ -26,7 +32,8 @@ public MenuOptions menuOptions() { return MenuOptions.create() .add("simple", this::simple) .add("toast", this::toast) - .add("alreadyParsed", this::alreadyParsed); + .add("alreadyParsed", this::alreadyParsed) + .add("enabledBlockTypes", this::enabledBlockTypes); } @Override @@ -132,4 +139,28 @@ private void alreadyParsed() { // apply parsed markdown markwon.setParsedMarkdown(textView, spanned); } + + private void enabledBlockTypes() { + + final String md = "" + + "# Head\n\n" + + "> and disabled quote\n\n" + + "```\n" + + "yep\n" + + "```"; + + final Set> blocks = CorePlugin.enabledBlockTypes(); + blocks.remove(BlockQuote.class); + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureParser(@NonNull Parser.Builder builder) { + builder.enabledBlockTypes(blocks); + } + }) + .build(); + + markwon.setMarkdown(textView, md); + } } From 5451a2722e559d75308b4de3c3a9d469ab86ed03 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 18:22:37 +0300 Subject: [PATCH 07/17] Clean up the underline investigations --- markwon-spans-better/build.gradle | 20 -- markwon-spans-better/gradle.properties | 4 - .../src/main/AndroidManifest.xml | 1 - .../spans/better/BetterUnderlineSpan.java | 183 ------------------ settings.gradle | 1 - 5 files changed, 209 deletions(-) delete mode 100644 markwon-spans-better/build.gradle delete mode 100644 markwon-spans-better/gradle.properties delete mode 100644 markwon-spans-better/src/main/AndroidManifest.xml delete mode 100644 markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java diff --git a/markwon-spans-better/build.gradle b/markwon-spans-better/build.gradle deleted file mode 100644 index 5272858b..00000000 --- a/markwon-spans-better/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -android { - - compileSdkVersion config['compile-sdk'] - buildToolsVersion config['build-tools'] - - defaultConfig { - minSdkVersion config['min-sdk'] - targetSdkVersion config['target-sdk'] - versionCode 1 - versionName version - } -} - -dependencies { - api project(':markwon-core') -} - -registerArtifact(this) diff --git a/markwon-spans-better/gradle.properties b/markwon-spans-better/gradle.properties deleted file mode 100644 index 4d3a5f47..00000000 --- a/markwon-spans-better/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -POM_NAME=Spans Better -POM_ARTIFACT_ID=spans-better -POM_DESCRIPTION=Better spans -POM_PACKAGING=aar \ No newline at end of file diff --git a/markwon-spans-better/src/main/AndroidManifest.xml b/markwon-spans-better/src/main/AndroidManifest.xml deleted file mode 100644 index 92bb979a..00000000 --- a/markwon-spans-better/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java b/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java deleted file mode 100644 index 2de53b78..00000000 --- a/markwon-spans-better/src/main/java/io/noties/markwon/spans/better/BetterUnderlineSpan.java +++ /dev/null @@ -1,183 +0,0 @@ -package io.noties.markwon.spans.better; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.os.Build; -import android.text.Layout; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.LineBackgroundSpan; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import io.noties.markwon.core.spans.TextViewSpan; - -import static java.lang.Math.max; -import static java.lang.Math.min; - -/** - * Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline) - * - * @since $nap; - */ -public class BetterUnderlineSpan implements LineBackgroundSpan { - - public enum Type { - @RequiresApi(Build.VERSION_CODES.KITKAT) - PATH, - REGION - } - - private static final float UNDERLINE_CLEAR_GAP = 5.5F; - - private final Path underline = new Path(); - private final Path outline = new Path(); - private final Paint stroke = new Paint(); - private final Path strokedOutline = new Path(); - private char[] chars; - - BetterUnderlineSpan() { - stroke.setStyle(Paint.Style.FILL_AND_STROKE); - stroke.setStrokeCap(Paint.Cap.BUTT); - } - - @Override - public void drawBackground( - Canvas c, - Paint p, - int left, - int right, - int top, - int baseline, - int bottom, - CharSequence text, - int start, - int end, - int lnum - ) { - final Spanned spanned = (Spanned) text; - final TextView textView = TextViewSpan.textViewOf(spanned); - - if (textView == null) { - // no, cannot do it, the whole text will be changed -// p.setUnderlineText(true); - return; - } - - final Layout layout = textView.getLayout(); - - final int selfStart = spanned.getSpanStart(this); - final int selfEnd = spanned.getSpanEnd(this); - - // TODO: also doesn't mean that it is last line, imagine text after span is ended - final boolean isLastLine = end == selfEnd || (selfEnd == (end - 1)); - - final int s = max(selfStart, start); - - // e - 1, but only if not last? - // oh... layout line count != span lines.. - final int e = min(selfEnd, end) - (isLastLine ? 0 : 1); - - final int l = (int) (layout.getPrimaryHorizontal(s) + .5F); - final int r = (int) (layout.getPrimaryHorizontal(e) + .5F); - final int b = getLineBottom(layout, lnum, isLastLine); - - final float density = textView.getResources().getDisplayMetrics().density; - - underline.rewind(); - // TODO: proper baseline -// underline.addRect( -// l, b - (1.8F * density), -// r, b, -// Path.Direction.CW -// -// ); - - // TODO: this must be configured somehow... - final int diff = (int) (p.descent() / 2F + .5F); - - underline.addRect( - l, baseline + diff, - r, baseline + diff + (density * 0.8F), - Path.Direction.CW - ); - - - outline.rewind(); - - // reallocate only if less, otherwise re-use and then send actual indexes - // TODO: would this return proper array for the last line?! - chars = new char[e - s]; - TextUtils.getChars(spanned, s, e, chars, 0); - p.getTextPath( - chars, - 0, (e - s), - l, baseline, - outline - ); - - final Paint paint = new Paint(); - paint.setStyle(Paint.Style.STROKE); - paint.setColor(Color.GREEN); - c.drawPath(outline, paint); - - outline.op(underline, Path.Op.INTERSECT); - - strokedOutline.rewind(); - stroke.setStrokeWidth(UNDERLINE_CLEAR_GAP * density); - stroke.getFillPath(outline, strokedOutline); - - underline.op(strokedOutline, Path.Op.DIFFERENCE); - - c.drawPath(underline, p); - } - - private static final float DEFAULT_EXTRA = 0F; - private static final float DEFAULT_MULTIPLIER = 1F; - - private static int getLineBottom(@NonNull Layout layout, int line, boolean isLastLine) { - - final int bottom = layout.getLineBottom(line); - final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - - // TODO: layout line count != span occupied lines -// final boolean isLastLine = line == layout.getLineCount() - 1; - - final int lineBottom; - final float lineSpacingExtra = layout.getSpacingAdd(); - final float lineSpacingMultiplier = layout.getSpacingMultiplier(); - - // simplified check - final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA - || lineSpacingMultiplier != DEFAULT_MULTIPLIER; - - if (!hasLineSpacing - || (isLastLine && lastLineSpacingNotAdded)) { - lineBottom = bottom; - } else { - final float extra; - if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) { - final int lineHeight = getLineHeight(layout, line); - extra = lineHeight - - ((lineHeight - lineSpacingExtra) / lineSpacingMultiplier); - } else { - extra = lineSpacingExtra; - } - lineBottom = (int) (bottom - extra + .5F); - } - - if (isLastLine) { - return lineBottom - layout.getBottomPadding(); - } - - return lineBottom; - } - - private static int getLineHeight(@NonNull Layout layout, int line) { - return layout.getLineTop(line + 1) - layout.getLineTop(line); - } -} diff --git a/settings.gradle b/settings.gradle index 4d91087c..8bf10dcb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,5 @@ include ':app', ':sample', ':markwon-recycler', ':markwon-recycler-table', ':markwon-simple-ext', - ':markwon-spans-better', ':markwon-syntax-highlight', ':markwon-test-span' From 851172a785464df2e52418e4339958cbf574ad22 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 28 Apr 2020 19:19:38 +0300 Subject: [PATCH 08/17] Fix table row span width calculation --- build.gradle | 2 +- .../main/java/io/noties/markwon/ext/tables/TableRowSpan.java | 5 +++-- sample/src/main/res/layout/activity_text_view.xml | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 64061d83..89b122ad 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'android-svg' : 'com.caverock:androidsvg:1.4', 'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', - 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.2', + 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.2.0-SNAPSHOT', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'io.noties:prism4j:2.0.0', 'debug' : 'io.noties:debug:5.0.0@jar', diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java index d00c1850..0bf48626 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java @@ -151,8 +151,9 @@ public void draw( int bottom, @NonNull Paint p) { - if (recreateLayouts(SpanUtils.width(canvas, text))) { - width = canvas.getWidth(); + final int spanWidth = SpanUtils.width(canvas, text); + if (recreateLayouts(spanWidth)) { + width = spanWidth; // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc if (p instanceof TextPaint) { // there must be a reason why this method receives Paint instead of TextPaint... diff --git a/sample/src/main/res/layout/activity_text_view.xml b/sample/src/main/res/layout/activity_text_view.xml index e557a4bc..dcbe4d67 100644 --- a/sample/src/main/res/layout/activity_text_view.xml +++ b/sample/src/main/res/layout/activity_text_view.xml @@ -14,6 +14,7 @@ android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="#000" android:textSize="16sp" + android:padding="8dip" tools:text="whatever" /> \ No newline at end of file From c661eb486da9e40343e39e3232d90ed7c4ca8526 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 30 Apr 2020 16:30:33 +0300 Subject: [PATCH 09/17] Update jlatexmath-android dependency --- CHANGELOG.md | 7 +++++-- build.gradle | 2 +- .../java/io/noties/markwon/sample/html/HtmlActivity.java | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad07bb3b..ef2a326b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,14 @@ * `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`) * `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed) * `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) -* `AsyncDrawableLoader` now uses `TextView` width without padding instead of width of canvas +* `AsyncDrawable` now uses `TextView` width without padding instead of width of canvas * Support for images inside table cells (`ext-tables` module) -* expose `enabledBlockTypes` in `CorePlugin` +* Expose `enabledBlockTypes` in `CorePlugin` +* Update `jlatexmath-android` dependency ([#225]) [#235]: https://github.com/noties/Markwon/issues/235 +[#225]: https://github.com/noties/Markwon/issues/225 + # 4.3.1 * Fix DexGuard optimization issue ([#216])
Thanks [@francescocervone] diff --git a/build.gradle b/build.gradle index 89b122ad..fe9e32a0 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'android-svg' : 'com.caverock:androidsvg:1.4', 'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', - 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.2.0-SNAPSHOT', + 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.2.0', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'prism4j' : 'io.noties:prism4j:2.0.0', 'debug' : 'io.noties:debug:5.0.0@jar', diff --git a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java index ca18d6c6..1f289f79 100644 --- a/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/html/HtmlActivity.java @@ -44,7 +44,7 @@ public MenuOptions menuOptions() { .add("randomCharSize", this::randomCharSize) .add("enhance", this::enhance) .add("image", this::image) - .add("elegantUnderline", this::elegantUnderline) +// .add("elegantUnderline", this::elegantUnderline) .add("iframe", this::iframe) .add("emptyTagReplacement", this::emptyTagReplacement); } @@ -60,8 +60,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { // let's define some custom tag-handlers textView = findViewById(R.id.text_view); - - elegantUnderline(); + + emptyTagReplacement(); } // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content From 7d49afaac7edd9d10b416556e32f5af1154a8629 Mon Sep 17 00:00:00 2001 From: Tyler Wong Date: Sat, 9 May 2020 22:02:45 -0700 Subject: [PATCH 10/17] Update Coil --- build.gradle | 2 +- .../io/noties/markwon/image/coil/CoilImagesPlugin.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 51aa9d6c..98c4146c 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ ext { 'dagger' : "com.google.dagger:dagger:$daggerVersion", 'picasso' : 'com.squareup.picasso:picasso:2.71828', 'glide' : 'com.github.bumptech.glide:glide:4.9.0', - 'coil' : 'io.coil-kt:coil:0.8.0' + 'coil' : 'io.coil-kt:coil:0.10.1' ] deps['annotationProcessor'] = [ diff --git a/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java b/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java index 5d15dcfd..9f307f35 100644 --- a/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java +++ b/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java @@ -15,7 +15,6 @@ import coil.Coil; import coil.ImageLoader; -import coil.api.ImageLoaders; import coil.request.LoadRequest; import coil.request.RequestDisposable; import coil.target.Target; @@ -48,7 +47,7 @@ public static CoilImagesPlugin create(@NonNull final Context context) { @NonNull @Override public LoadRequest load(@NonNull AsyncDrawable drawable) { - return ImageLoaders.newLoadBuilder(Coil.loader(), context) + return LoadRequest.builder(context) .data(drawable.getDestination()) .build(); } @@ -57,7 +56,7 @@ public LoadRequest load(@NonNull AsyncDrawable drawable) { public void cancel(@NonNull RequestDisposable disposable) { disposable.dispose(); } - }, Coil.loader()); + }, Coil.imageLoader(context)); } @NonNull @@ -67,7 +66,7 @@ public static CoilImagesPlugin create(@NonNull final Context context, @NonNull @Override public LoadRequest load(@NonNull AsyncDrawable drawable) { - return ImageLoaders.newLoadBuilder(imageLoader, context) + return LoadRequest.builder(context) .data(drawable.getDestination()) .build(); } From e386880978db334e7517ba280fd79bbc15613d00 Mon Sep 17 00:00:00 2001 From: Tyler Wong Date: Sat, 9 May 2020 22:10:54 -0700 Subject: [PATCH 11/17] Fix deprecation --- .../java/io/noties/markwon/image/coil/CoilImagesPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java b/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java index 9f307f35..b71543a7 100644 --- a/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java +++ b/markwon-image-coil/src/main/java/io/noties/markwon/image/coil/CoilImagesPlugin.java @@ -128,7 +128,7 @@ public void load(@NonNull AsyncDrawable drawable) { LoadRequest request = coilStore.load(drawable).newBuilder() .target(target) .build(); - RequestDisposable disposable = imageLoader.load(request); + RequestDisposable disposable = imageLoader.execute(request); cache.put(drawable, disposable); } From 21152f368f006a2065fc11c403f2c7024e6dc147 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 12 May 2020 12:30:54 +0300 Subject: [PATCH 12/17] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2a326b..420c2215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,12 @@ * Support for images inside table cells (`ext-tables` module) * Expose `enabledBlockTypes` in `CorePlugin` * Update `jlatexmath-android` dependency ([#225]) +* Update `image-coil` module (Coil version `0.10.1`) ([#244])
Thanks to [@tylerbwong] [#235]: https://github.com/noties/Markwon/issues/235 [#225]: https://github.com/noties/Markwon/issues/225 +[#244]: https://github.com/noties/Markwon/pull/244 +[@tylerbwong]: https://github.com/tylerbwong # 4.3.1 From 171b6d40a00bce6d1f0183319a6108648d200602 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 12 May 2020 13:33:59 +0300 Subject: [PATCH 13/17] ImageDestinationProcessor (before UrlProcessor), limit usage to images only --- CHANGELOG.md | 1 + ...ageDestinationProcessorInitialReadme.java} | 10 ++-- .../noties/markwon/app/MarkdownRenderer.java | 12 ++-- docs/docs/v4/core/configuration.md | 27 +++++---- .../noties/markwon/MarkwonConfiguration.java | 31 ++++++---- .../io/noties/markwon/core/CorePlugin.java | 5 +- .../ImageDestinationProcessor.java | 27 +++++++++ .../ImageDestinationProcessorAssets.java | 59 +++++++++++++++++++ ...stinationProcessorRelativeToAbsolute.java} | 23 ++++++-- .../markwon/urlprocessor/UrlProcessor.java | 8 --- .../UrlProcessorAndroidAssets.java | 49 --------------- .../urlprocessor/UrlProcessorNoOp.java | 11 ---- .../ImageDestinationProcessorAssetsTest.java} | 10 ++-- ...ationProcessorRelativeToAbsoluteTest.java} | 16 ++--- .../noties/markwon/html/tag/ImageHandler.java | 2 +- .../noties/markwon/html/tag/LinkHandler.java | 3 +- .../markwon/image/file/FileSchemeHandler.java | 10 ++-- .../basicplugins/BasicPluginsActivity.java | 20 +++++-- .../sample/recycler/RecyclerActivity.java | 12 ++-- 19 files changed, 193 insertions(+), 143 deletions(-) rename app/src/main/java/io/noties/markwon/app/{UrlProcessorInitialReadme.java => ImageDestinationProcessorInitialReadme.java} (59%) create mode 100644 markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java create mode 100644 markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java rename markwon-core/src/main/java/io/noties/markwon/{urlprocessor/UrlProcessorRelativeToAbsolute.java => image/destination/ImageDestinationProcessorRelativeToAbsolute.java} (54%) delete mode 100644 markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessor.java delete mode 100644 markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java delete mode 100644 markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorNoOp.java rename markwon-core/src/test/java/io/noties/markwon/{urlprocessor/UrlProcessorAndroidAssetsTest.java => image/destination/ImageDestinationProcessorAssetsTest.java} (77%) rename markwon-core/src/test/java/io/noties/markwon/{urlprocessor/UrlProcessorRelativeToAbsoluteTest.java => image/destination/ImageDestinationProcessorRelativeToAbsoluteTest.java} (59%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 420c2215..293be7ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Expose `enabledBlockTypes` in `CorePlugin` * Update `jlatexmath-android` dependency ([#225]) * Update `image-coil` module (Coil version `0.10.1`) ([#244])
Thanks to [@tylerbwong] +* Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -> `io.noties.markwon.image.destination`) and limit its usage to process **only** destination URL of images (was used to also process links before) [#235]: https://github.com/noties/Markwon/issues/235 [#225]: https://github.com/noties/Markwon/issues/225 diff --git a/app/src/main/java/io/noties/markwon/app/UrlProcessorInitialReadme.java b/app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java similarity index 59% rename from app/src/main/java/io/noties/markwon/app/UrlProcessorInitialReadme.java rename to app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java index 571bb395..26653af9 100644 --- a/app/src/main/java/io/noties/markwon/app/UrlProcessorInitialReadme.java +++ b/app/src/main/java/io/noties/markwon/app/ImageDestinationProcessorInitialReadme.java @@ -5,15 +5,15 @@ import androidx.annotation.NonNull; -import io.noties.markwon.urlprocessor.UrlProcessor; -import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; +import io.noties.markwon.image.destination.ImageDestinationProcessor; +import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; -class UrlProcessorInitialReadme implements UrlProcessor { +class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; - private final UrlProcessorRelativeToAbsolute processor - = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); + private final ImageDestinationProcessorRelativeToAbsolute processor + = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); @NonNull @Override diff --git a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java b/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java index cc339083..d820733e 100644 --- a/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java +++ b/app/src/main/java/io/noties/markwon/app/MarkdownRenderer.java @@ -24,6 +24,8 @@ import io.noties.markwon.ext.tasklist.TaskListPlugin; import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.image.destination.ImageDestinationProcessor; +import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; import io.noties.markwon.image.file.FileSchemeHandler; import io.noties.markwon.image.gif.GifMediaDecoder; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; @@ -31,8 +33,6 @@ import io.noties.markwon.syntax.Prism4jThemeDarkula; import io.noties.markwon.syntax.Prism4jThemeDefault; import io.noties.markwon.syntax.SyntaxHighlightPlugin; -import io.noties.markwon.urlprocessor.UrlProcessor; -import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; import io.noties.prism4j.Prism4j; @ActivityScope @@ -86,11 +86,11 @@ public void run() { } private void execute() { - final UrlProcessor urlProcessor; + final ImageDestinationProcessor imageDestinationProcessor; if (uri == null) { - urlProcessor = new UrlProcessorInitialReadme(); + imageDestinationProcessor = new ImageDestinationProcessorInitialReadme(); } else { - urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString()); + imageDestinationProcessor = new ImageDestinationProcessorRelativeToAbsolute(uri.toString()); } final Prism4jTheme prism4jTheme = isLightTheme @@ -119,7 +119,7 @@ public void configureImages(@NonNull ImagesPlugin plugin) { .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.urlProcessor(urlProcessor); + builder.imageDestinationProcessor(imageDestinationProcessor); } }) .build(); diff --git a/docs/docs/v4/core/configuration.md b/docs/docs/v4/core/configuration.md index 23a32fb0..e012dd33 100644 --- a/docs/docs/v4/core/configuration.md +++ b/docs/docs/v4/core/configuration.md @@ -5,7 +5,7 @@ These are _configurable_ properties: * `AsyncDrawableLoader` (back here since ) * `SyntaxHighlight` * `LinkResolver` (since , before — `LinkSpan.Resolver`) -* `UrlProcessor` +* `ImageDestinationProcessor` (since , before — `UrlProcessor`) * `ImageSizeResolver` :::tip @@ -36,10 +36,11 @@ final Markwon markwon = Markwon.builder(context) .build(); ``` -Currently `Markwon` provides 3 implementations for loading images: +Currently `Markwon` provides 4 implementations for loading images: * [markwon implementation](/docs/v4/image/) with SVG, GIF, data uri and android_assets support * [based on Picasso](/docs/v4/image-picasso/) * [based on Glide](/docs/v4/image-glide/) +* [base on Coil](/docs/v4/image-coil/) ## SyntaxHighlight @@ -87,32 +88,32 @@ if there is none registered. if you wish to register own instance of a `Movement apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md) ::: -## UrlProcessor +## ImageDestinationProcessor -Process URLs in your markdown (for links and images). If not provided explicitly, +Process destinations (URLs) of images in your markdown. If not provided explicitly, default **no-op** implementation will be used, which does not modify URLs (keeping them as-is). `Markwon` provides 2 implementations of `UrlProcessor`: -* `UrlProcessorRelativeToAbsolute` -* `UrlProcessorAndroidAssets` +* `ImageDestinationProcessorRelativeToAbsolute` +* `ImageDestinationProcessorAssets` -### UrlProcessorRelativeToAbsolute +### ImageDestinationProcessorRelativeToAbsolute -`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is -defined like this: `![img](./art/image.JPG)` and `UrlProcessorRelativeToAbsolute` +`ImageDestinationProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is +defined like this: `![img](./art/image.JPG)` and `ImageDestinationProcessorRelativeToAbsolute` is created with `https://github.com/noties/Markwon/raw/master/` as the base: -`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, +`new ImageDestinationProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`, then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG` as the destination. -### UrlProcessorAndroidAssets +### ImageDestinationProcessorAssets -`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder. +`ImageDestinationProcessorAssets` can be used to make processed destinations to point to Android assets folder. So an image: `![img](./art/image.JPG)` will have `file:///android_asset/art/image.JPG` as the destination. :::tip -Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information, +Please note that `ImageDestinationProcessorAssets` will process only URLs that have no `scheme` information, so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png` will be kept as-is. ::: diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java index 5b22623f..e19c8228 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java @@ -6,15 +6,13 @@ import io.noties.markwon.image.AsyncDrawableLoader; import io.noties.markwon.image.ImageSizeResolver; import io.noties.markwon.image.ImageSizeResolverDef; +import io.noties.markwon.image.destination.ImageDestinationProcessor; import io.noties.markwon.syntax.SyntaxHighlight; import io.noties.markwon.syntax.SyntaxHighlightNoOp; -import io.noties.markwon.urlprocessor.UrlProcessor; -import io.noties.markwon.urlprocessor.UrlProcessorNoOp; /** * since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration` */ -@SuppressWarnings("WeakerAccess") public class MarkwonConfiguration { @NonNull @@ -26,7 +24,8 @@ public static Builder builder() { private final AsyncDrawableLoader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkResolver linkResolver; - private final UrlProcessor urlProcessor; + // @since $nap; + private final ImageDestinationProcessor imageDestinationProcessor; private final ImageSizeResolver imageSizeResolver; // @since 3.0.0 @@ -37,7 +36,7 @@ private MarkwonConfiguration(@NonNull Builder builder) { this.asyncDrawableLoader = builder.asyncDrawableLoader; this.syntaxHighlight = builder.syntaxHighlight; this.linkResolver = builder.linkResolver; - this.urlProcessor = builder.urlProcessor; + this.imageDestinationProcessor = builder.imageDestinationProcessor; this.imageSizeResolver = builder.imageSizeResolver; this.spansFactory = builder.spansFactory; } @@ -62,9 +61,12 @@ public LinkResolver linkResolver() { return linkResolver; } + /** + * @since $nap; + */ @NonNull - public UrlProcessor urlProcessor() { - return urlProcessor; + public ImageDestinationProcessor imageDestinationProcessor() { + return imageDestinationProcessor; } @NonNull @@ -87,7 +89,8 @@ public static class Builder { private AsyncDrawableLoader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkResolver linkResolver; - private UrlProcessor urlProcessor; + // @since $nap; + private ImageDestinationProcessor imageDestinationProcessor; private ImageSizeResolver imageSizeResolver; private MarkwonSpansFactory spansFactory; @@ -115,9 +118,12 @@ public Builder linkResolver(@NonNull LinkResolver linkResolver) { return this; } + /** + * @since $nap; + */ @NonNull - public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) { - this.urlProcessor = urlProcessor; + public Builder imageDestinationProcessor(@NonNull ImageDestinationProcessor imageDestinationProcessor) { + this.imageDestinationProcessor = imageDestinationProcessor; return this; } @@ -151,8 +157,9 @@ public MarkwonConfiguration build( linkResolver = new LinkResolverDef(); } - if (urlProcessor == null) { - urlProcessor = new UrlProcessorNoOp(); + // @since $nap; + if (imageDestinationProcessor == null) { + imageDestinationProcessor = ImageDestinationProcessor.noOp(); } if (imageSizeResolver == null) { diff --git a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java index 08c1c45a..36b8b277 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java @@ -320,7 +320,7 @@ public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { final boolean link = parent instanceof Link; final String destination = configuration - .urlProcessor() + .imageDestinationProcessor() .process(image.getDestination()); final RenderProps props = visitor.renderProps(); @@ -524,8 +524,7 @@ public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) { final int length = visitor.length(); visitor.visitChildren(link); - final MarkwonConfiguration configuration = visitor.configuration(); - final String destination = configuration.urlProcessor().process(link.getDestination()); + final String destination = link.getDestination(); CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination); diff --git a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java new file mode 100644 index 00000000..831c0b7c --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java @@ -0,0 +1,27 @@ +package io.noties.markwon.image.destination; + +import androidx.annotation.NonNull; + +/** + * Process destination of image nodes + * + * @since $nap; + */ +public abstract class ImageDestinationProcessor { + @NonNull + public abstract String process(@NonNull String destination); + + @NonNull + public static ImageDestinationProcessor noOp() { + return new NoOp(); + } + + private static class NoOp extends ImageDestinationProcessor { + + @NonNull + @Override + public String process(@NonNull String destination) { + return destination; + } + } +} diff --git a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java new file mode 100644 index 00000000..169d2527 --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java @@ -0,0 +1,59 @@ +package io.noties.markwon.image.destination; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * {@link ImageDestinationProcessor} that treats all destinations without scheme + * information as pointing to the {@code assets} folder of an application. Please note that this + * processor only adds required {@code file:///android_asset/} prefix to destinations and + * actual image loading must take that into account (implement this functionality). + *

+ * {@code FileSchemeHandler} from the {@code image} module supports asset images when created with + * {@code createWithAssets} factory method + * + * @since $nap; + */ +public class ImageDestinationProcessorAssets extends ImageDestinationProcessor { + + @NonNull + public static ImageDestinationProcessorAssets create(@Nullable ImageDestinationProcessor parent) { + return new ImageDestinationProcessorAssets(parent); + } + + static final String MOCK = "https://android.asset/"; + static final String BASE = "file:///android_asset/"; + + private final ImageDestinationProcessorRelativeToAbsolute assetsProcessor + = new ImageDestinationProcessorRelativeToAbsolute(MOCK); + + private final ImageDestinationProcessor processor; + + public ImageDestinationProcessorAssets() { + this(null); + } + + public ImageDestinationProcessorAssets(@Nullable ImageDestinationProcessor parent) { + this.processor = parent; + } + + @NonNull + @Override + public String process(@NonNull String destination) { + final String out; + final Uri uri = Uri.parse(destination); + if (TextUtils.isEmpty(uri.getScheme())) { + out = assetsProcessor.process(destination).replace(MOCK, BASE); + } else { + if (processor != null) { + out = processor.process(destination); + } else { + out = destination; + } + } + return out; + } +} diff --git a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java similarity index 54% rename from markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java rename to markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java index 99a19226..9e04e1aa 100644 --- a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsolute.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java @@ -1,4 +1,4 @@ -package io.noties.markwon.urlprocessor; +package io.noties.markwon.image.destination; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -6,15 +6,30 @@ import java.net.MalformedURLException; import java.net.URL; -@SuppressWarnings("WeakerAccess") -public class UrlProcessorRelativeToAbsolute implements UrlProcessor { +/** + * @since $nap; + */ +public class ImageDestinationProcessorRelativeToAbsolute extends ImageDestinationProcessor { + + @NonNull + public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull String base) { + return new ImageDestinationProcessorRelativeToAbsolute(base); + } + + public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull URL base) { + return new ImageDestinationProcessorRelativeToAbsolute(base); + } private final URL base; - public UrlProcessorRelativeToAbsolute(@NonNull String base) { + public ImageDestinationProcessorRelativeToAbsolute(@NonNull String base) { this.base = obtain(base); } + public ImageDestinationProcessorRelativeToAbsolute(@NonNull URL base) { + this.base = base; + } + @NonNull @Override public String process(@NonNull String destination) { diff --git a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessor.java b/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessor.java deleted file mode 100644 index b49585e5..00000000 --- a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.noties.markwon.urlprocessor; - -import androidx.annotation.NonNull; - -public interface UrlProcessor { - @NonNull - String process(@NonNull String destination); -} diff --git a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java b/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java deleted file mode 100644 index bd3c74cb..00000000 --- a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssets.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.noties.markwon.urlprocessor; - -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Processor that will assume that an URL without scheme points to android assets folder. - * URL with a scheme will be processed by {@link #processor} (if it is specified) or returned `as-is`. - */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public class UrlProcessorAndroidAssets implements UrlProcessor { - - - static final String MOCK = "https://android.asset/"; - static final String BASE = "file:///android_asset/"; - - private final UrlProcessorRelativeToAbsolute assetsProcessor - = new UrlProcessorRelativeToAbsolute(MOCK); - - private final UrlProcessor processor; - - public UrlProcessorAndroidAssets() { - this(null); - } - - public UrlProcessorAndroidAssets(@Nullable UrlProcessor parent) { - this.processor = parent; - } - - @NonNull - @Override - public String process(@NonNull String destination) { - final String out; - final Uri uri = Uri.parse(destination); - if (TextUtils.isEmpty(uri.getScheme())) { - out = assetsProcessor.process(destination).replace(MOCK, BASE); - } else { - if (processor != null) { - out = processor.process(destination); - } else { - out = destination; - } - } - return out; - } -} diff --git a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorNoOp.java b/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorNoOp.java deleted file mode 100644 index 1bc15a88..00000000 --- a/markwon-core/src/main/java/io/noties/markwon/urlprocessor/UrlProcessorNoOp.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.noties.markwon.urlprocessor; - -import androidx.annotation.NonNull; - -public class UrlProcessorNoOp implements UrlProcessor { - @NonNull - @Override - public String process(@NonNull String destination) { - return destination; - } -} diff --git a/markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java b/markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssetsTest.java similarity index 77% rename from markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java rename to markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssetsTest.java index 4129bfb8..4ab57e6b 100644 --- a/markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorAndroidAssetsTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssetsTest.java @@ -1,4 +1,4 @@ -package io.noties.markwon.urlprocessor; +package io.noties.markwon.image.destination; import org.junit.Before; import org.junit.Test; @@ -6,18 +6,18 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import static io.noties.markwon.image.destination.ImageDestinationProcessorAssets.BASE; import static org.junit.Assert.assertEquals; -import static io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets.BASE; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class UrlProcessorAndroidAssetsTest { +public class ImageDestinationProcessorAssetsTest { - private UrlProcessorAndroidAssets processor; + private ImageDestinationProcessorAssets processor; @Before public void before() { - processor = new UrlProcessorAndroidAssets(); + processor = new ImageDestinationProcessorAssets(); } @Test diff --git a/markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java b/markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsoluteTest.java similarity index 59% rename from markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java rename to markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsoluteTest.java index afa55b33..36560fb6 100644 --- a/markwon-core/src/test/java/io/noties/markwon/urlprocessor/UrlProcessorRelativeToAbsoluteTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsoluteTest.java @@ -1,4 +1,4 @@ -package io.noties.markwon.urlprocessor; +package io.noties.markwon.image.destination; import org.junit.Test; import org.junit.runner.RunWith; @@ -9,39 +9,39 @@ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class UrlProcessorRelativeToAbsoluteTest { +public class ImageDestinationProcessorRelativeToAbsoluteTest { @Test public void malformed_base_do_not_process() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("!@#$%^&*("); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("!@#$%^&*("); final String destination = "../hey.there.html"; assertEquals(destination, processor.process(destination)); } @Test public void access_root() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/"); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/"); final String url = "/index.html"; assertEquals("https://ro.ot/index.html", processor.process(url)); } @Test public void access_same_directory() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/"); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/"); final String url = "./.htaccess"; assertEquals("https://ro.ot/hello/.htaccess", processor.process(url)); } @Test public void asset_directory_up() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/second/"); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/second/"); final String url = "../cat.JPG"; assertEquals("http://ro.ot/first/cat.JPG", processor.process(url)); } @Test public void change_directory_inside_destination() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/"); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/"); final String url = "../first/../second/./thi.rd"; assertEquals( "http://ro.ot/second/thi.rd", @@ -51,7 +51,7 @@ public void change_directory_inside_destination() { @Test public void with_query_arguments() { - final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/"); + final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/"); final String url = "../index.php?ROOT=1"; assertEquals( "http://ro.ot/index.php?ROOT=1", diff --git a/markwon-html/src/main/java/io/noties/markwon/html/tag/ImageHandler.java b/markwon-html/src/main/java/io/noties/markwon/html/tag/ImageHandler.java index ad170b0e..833c4926 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/tag/ImageHandler.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/tag/ImageHandler.java @@ -62,7 +62,7 @@ public Object getSpans( return null; } - final String destination = configuration.urlProcessor().process(src); + final String destination = configuration.imageDestinationProcessor().process(src); final ImageSize imageSize = imageSizeParser.parse(tag.attributes()); // todo: replacement text is link... as we are not at block level diff --git a/markwon-html/src/main/java/io/noties/markwon/html/tag/LinkHandler.java b/markwon-html/src/main/java/io/noties/markwon/html/tag/LinkHandler.java index fdd830cc..d5e032f7 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/tag/LinkHandler.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/tag/LinkHandler.java @@ -27,7 +27,8 @@ public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull Ren CoreProps.LINK_DESTINATION.set( renderProps, - configuration.urlProcessor().process(destination)); + destination + ); return spanFactory.getSpans(configuration, renderProps); } diff --git a/markwon-image/src/main/java/io/noties/markwon/image/file/FileSchemeHandler.java b/markwon-image/src/main/java/io/noties/markwon/image/file/FileSchemeHandler.java index d7da6dc5..b29a67b6 100644 --- a/markwon-image/src/main/java/io/noties/markwon/image/file/FileSchemeHandler.java +++ b/markwon-image/src/main/java/io/noties/markwon/image/file/FileSchemeHandler.java @@ -3,11 +3,12 @@ import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -18,7 +19,6 @@ import java.util.Collections; import java.util.List; -import io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets; import io.noties.markwon.image.ImageItem; import io.noties.markwon.image.SchemeHandler; @@ -30,7 +30,7 @@ public class FileSchemeHandler extends SchemeHandler { public static final String SCHEME = "file"; /** - * @see UrlProcessorAndroidAssets + * @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets */ @NonNull public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { @@ -39,7 +39,7 @@ public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetMana /** * @see #createWithAssets(AssetManager) - * @see UrlProcessorAndroidAssets + * @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets * @since 4.0.0 */ @NonNull diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java index 02d3308a..21ba2bad 100644 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; +import android.view.View; import android.widget.ScrollView; import android.widget.TextView; @@ -20,6 +21,7 @@ import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.BlockHandlerDef; +import io.noties.markwon.LinkResolverDef; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonSpansFactory; @@ -153,7 +155,7 @@ public void configureTheme(@NonNull MarkwonTheme.Builder builder) { *

    *
  • SyntaxHighlight
  • *
  • LinkSpan.Resolver
  • - *
  • UrlProcessor
  • + *
  • ImageDestinationProcessor
  • *
  • ImageSizeResolver
  • *
*

@@ -173,12 +175,18 @@ private void linkWithMovementMethod() { public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { // for example if specified destination has no scheme info, we will // _assume_ that it's network request and append HTTPS scheme - builder.urlProcessor(destination -> { - final Uri uri = Uri.parse(destination); - if (TextUtils.isEmpty(uri.getScheme())) { - return "https://" + destination; + builder.linkResolver(new LinkResolverDef() { + @Override + public void resolve(@NonNull View view, @NonNull String link) { + final String destination; + final Uri uri = Uri.parse(link); + if (TextUtils.isEmpty(uri.getScheme())) { + destination = "https://" + link; + } else { + destination = link; + } + super.resolve(view, destination); } - return destination; }); } }) diff --git a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java index 84034ce9..50bcd696 100644 --- a/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/recycler/RecyclerActivity.java @@ -34,6 +34,8 @@ import io.noties.markwon.core.CorePlugin; import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.image.destination.ImageDestinationProcessor; +import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute; import io.noties.markwon.image.file.FileSchemeHandler; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; import io.noties.markwon.image.svg.SvgMediaDecoder; @@ -42,8 +44,6 @@ import io.noties.markwon.recycler.table.TableEntry; import io.noties.markwon.recycler.table.TableEntryPlugin; import io.noties.markwon.sample.R; -import io.noties.markwon.urlprocessor.UrlProcessor; -import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; public class RecyclerActivity extends Activity { @@ -100,7 +100,7 @@ private static Markwon markwon(@NonNull Context context) { .usePlugin(new AbstractMarkwonPlugin() { @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.urlProcessor(new UrlProcessorInitialReadme()); + builder.imageDestinationProcessor(new ImageDestinationProcessorInitialReadme()); } @Override @@ -182,12 +182,12 @@ private static String readStream(@Nullable InputStream inputStream) { return out; } - private static class UrlProcessorInitialReadme implements UrlProcessor { + private static class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor { private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; - private final UrlProcessorRelativeToAbsolute processor - = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); + private final ImageDestinationProcessorRelativeToAbsolute processor + = new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE); @NonNull @Override From 477078470baee3c4d5a3f08b71929d83a91c0284 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 12 May 2020 13:36:01 +0300 Subject: [PATCH 14/17] jlatex, do not use deprecated fitCanvas method --- .../java/io/noties/markwon/ext/latex/JLatexMathPlugin.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java index d6d561b0..abd399e3 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -458,8 +458,7 @@ private JLatexMathDrawable createBlockDrawable(@NonNull JLatextAsyncDrawable dra final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) .textSize(theme.blockTextSize()) - .align(theme.blockHorizontalAlignment()) - .fitCanvas(theme.blockFitCanvas()); + .align(theme.blockHorizontalAlignment()); if (backgroundProvider != null) { builder.background(backgroundProvider.provide()); @@ -489,8 +488,7 @@ private JLatexMathDrawable createInlineDrawable(@NonNull JLatextAsyncDrawable dr final int color = theme.inlineTextColor(); final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex) - .textSize(theme.inlineTextSize()) - .fitCanvas(false); + .textSize(theme.inlineTextSize()); if (backgroundProvider != null) { builder.background(backgroundProvider.provide()); From c450765ab4c554cf20ea136a9676362d84ac5c4c Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 12 May 2020 13:51:51 +0300 Subject: [PATCH 15/17] Add Senstone Portable Voice Assistant to awesome section --- docs/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3b80abb4..7fdda10c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -99,12 +99,7 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht ## # Awesome Markwon - -Applications using Markwon: - -* [Partico](https://partiko.app/) - Partiko is a censorship free social network. -* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. -* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds. +
+* [Partico](https://partiko.app/) - Partiko is a censorship free social network. +* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas. +* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds. +* [Senstone Portable Voice Assistant](https://play.google.com/store/apps/details?id=com.senstone) - Senstone is a tiny wearable personal assistant powered by this App. It lets you capture your ideas, notes and reminders handsfree without pulling out your phone. + Extension/plugins: From d42ae41409c82c01c7d7e009a06e39c9cfd427bc Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 12 May 2020 14:41:24 +0300 Subject: [PATCH 16/17] Add fallbackToRawInputWhenEmpty configuration --- CHANGELOG.md | 4 +- .../main/java/io/noties/markwon/Markwon.java | 11 +++ .../io/noties/markwon/MarkwonBuilderImpl.java | 13 ++- .../java/io/noties/markwon/MarkwonImpl.java | 23 +++++- .../io/noties/markwon/MarkwonImplTest.java | 79 ++++++++++++++++--- 5 files changed, 116 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293be7ea..a4607ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ * Expose `enabledBlockTypes` in `CorePlugin` * Update `jlatexmath-android` dependency ([#225]) * Update `image-coil` module (Coil version `0.10.1`) ([#244])
Thanks to [@tylerbwong] -* Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -> `io.noties.markwon.image.destination`) and limit its usage to process **only** destination URL of images (was used to also process links before) +* Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -> `io.noties.markwon.image.destination`) and limit its usage to process **only** destination URL of images (was used to also process links before) +* `fallbackToRawInputWhenEmpty` `Markwon.Builder` configuration to fallback to raw input if rendered markdown is empty ([#242]) [#235]: https://github.com/noties/Markwon/issues/235 [#225]: https://github.com/noties/Markwon/issues/225 [#244]: https://github.com/noties/Markwon/pull/244 +[#242]: https://github.com/noties/Markwon/issues/242 [@tylerbwong]: https://github.com/tylerbwong diff --git a/markwon-core/src/main/java/io/noties/markwon/Markwon.java b/markwon-core/src/main/java/io/noties/markwon/Markwon.java index cd277e03..1422bf3c 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -192,6 +192,17 @@ public interface Builder { @NonNull Builder usePlugins(@NonNull Iterable plugins); + /** + * Control if small chunks of non-finished markdown sentences (for example, a single `*` character) + * should be displayed/rendered as raw input instead of an empty string. + *

+ * Since $nap; {@code true} by default, versions prior - {@code false} + * + * @since $nap; + */ + @NonNull + Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty); + @NonNull Markwon build(); } diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java index 2ae70e18..9d470f87 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java @@ -27,6 +27,9 @@ class MarkwonBuilderImpl implements Markwon.Builder { private Markwon.TextSetter textSetter; + // @since $nap; + private boolean fallbackToRawInputWhenEmpty = true; + MarkwonBuilderImpl(@NonNull Context context) { this.context = context; } @@ -71,6 +74,13 @@ public Markwon.Builder usePlugins(@NonNull Iterable plu return this; } + @NonNull + @Override + public Markwon.Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty) { + this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty; + return this; + } + @NonNull @Override public Markwon build() { @@ -114,7 +124,8 @@ public Markwon build() { parserBuilder.build(), visitorFactory, configuration, - Collections.unmodifiableList(plugins) + Collections.unmodifiableList(plugins), + fallbackToRawInputWhenEmpty ); } diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java index 5aced55c..999e2f68 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java @@ -1,6 +1,8 @@ package io.noties.markwon; +import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.widget.TextView; import androidx.annotation.NonNull; @@ -28,19 +30,25 @@ class MarkwonImpl extends Markwon { @Nullable private final TextSetter textSetter; + // @since $nap; + private final boolean fallbackToRawInputWhenEmpty; + MarkwonImpl( @NonNull TextView.BufferType bufferType, @Nullable TextSetter textSetter, @NonNull Parser parser, @NonNull MarkwonVisitorFactory visitorFactory, @NonNull MarkwonConfiguration configuration, - @NonNull List plugins) { + @NonNull List plugins, + boolean fallbackToRawInputWhenEmpty + ) { this.bufferType = bufferType; this.textSetter = textSetter; this.parser = parser; this.visitorFactory = visitorFactory; this.configuration = configuration; this.plugins = plugins; + this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty; } @NonNull @@ -86,7 +94,18 @@ public Spanned render(@NonNull Node node) { @NonNull @Override public Spanned toMarkdown(@NonNull String input) { - return render(parse(input)); + final Spanned spanned = render(parse(input)); + + // @since $nap; + // if spanned is empty, we are configured to use raw input and input is not empty + if (TextUtils.isEmpty(spanned) + && fallbackToRawInputWhenEmpty + && !TextUtils.isEmpty(input)) { + // let's use SpannableStringBuilder in order to keep backward-compatibility + return new SpannableStringBuilder(input); + } + + return spanned; } @Override diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java index 0d9024d4..599e24d5 100644 --- a/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java @@ -1,6 +1,7 @@ package io.noties.markwon; import android.text.Spanned; +import android.text.TextUtils; import android.widget.TextView; import org.commonmark.node.Node; @@ -50,7 +51,9 @@ public void parse_calls_plugin_process_markdown() { mock(Parser.class), mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - Collections.singletonList(plugin)); + Collections.singletonList(plugin), + true + ); impl.parse("whatever"); @@ -74,7 +77,9 @@ public void parse_markwon_processed() { parser, mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - Arrays.asList(first, second)); + Arrays.asList(first, second), + true + ); impl.parse("zero"); @@ -102,7 +107,9 @@ public void render_calls_plugins() { mock(Parser.class), visitorFactory, mock(MarkwonConfiguration.class), - Collections.singletonList(plugin)); + Collections.singletonList(plugin), + true + ); when(visitorFactory.create()).thenReturn(visitor); when(visitor.builder()).thenReturn(builder); @@ -149,7 +156,9 @@ public void render_clears_visitor() { mock(Parser.class), visitorFactory, mock(MarkwonConfiguration.class), - Collections.emptyList()); + Collections.emptyList(), + true + ); impl.render(mock(Node.class)); @@ -185,7 +194,9 @@ public Object answer(InvocationOnMock invocation) { mock(Parser.class), visitorFactory, mock(MarkwonConfiguration.class), - Collections.singletonList(plugin)); + Collections.singletonList(plugin), + true + ); final AtomicBoolean flag = new AtomicBoolean(false); final Node node = mock(Node.class); @@ -224,7 +235,9 @@ public void set_parsed_markdown() { mock(Parser.class), mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), mock(MarkwonConfiguration.class), - Collections.singletonList(plugin)); + Collections.singletonList(plugin), + true + ); final TextView textView = mock(TextView.class); final AtomicBoolean flag = new AtomicBoolean(false); @@ -272,7 +285,9 @@ final class Second extends AbstractMarkwonPlugin { mock(Parser.class), mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - plugins); + plugins, + true + ); assertTrue("First", impl.hasPlugin(First.class)); assertFalse("Second", impl.hasPlugin(Second.class)); @@ -295,7 +310,9 @@ public void text_setter() { mock(Parser.class), mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - Collections.singletonList(plugin)); + Collections.singletonList(plugin), + true + ); final TextView textView = mock(TextView.class); final Spanned spanned = mock(Spanned.class); @@ -339,7 +356,9 @@ final class NotPresent extends AbstractMarkwonPlugin { mock(Parser.class), mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - plugins); + plugins, + true + ); // should be returned assertNotNull(impl.requirePlugin(MarkwonPlugin.class)); @@ -370,7 +389,9 @@ public void plugins_unmodifiable() { mock(Parser.class), mock(MarkwonVisitorFactory.class), mock(MarkwonConfiguration.class), - plugins); + plugins, + true + ); final List list = impl.getPlugins(); @@ -385,4 +406,42 @@ public void plugins_unmodifiable() { assertTrue(e.getMessage(), true); } } + + @Test + public void fallback_to_raw() { + final String md = "*"; + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + null, + mock(Parser.class, RETURNS_MOCKS), + // it must be sufficient to just return mocks and thus empty rendering result + mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), + mock(MarkwonConfiguration.class), + Collections.emptyList(), + true + ); + + final Spanned spanned = impl.toMarkdown(md); + assertEquals(md, spanned.toString()); + } + + @Test + public void fallback_to_raw_false() { + final String md = "*"; + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + null, + mock(Parser.class, RETURNS_MOCKS), + // it must be sufficient to just return mocks and thus empty rendering result + mock(MarkwonVisitorFactory.class, RETURNS_MOCKS), + mock(MarkwonConfiguration.class), + Collections.emptyList(), + false + ); + + final Spanned spanned = impl.toMarkdown(md); + assertTrue(spanned.toString(), TextUtils.isEmpty(spanned)); + } } \ No newline at end of file From c2c59041f5ef64becd9d8ec08b909b765c5f6245 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 14 May 2020 12:23:47 +0300 Subject: [PATCH 17/17] Prepare 4.4.0 release --- CHANGELOG.md | 2 +- gradle.properties | 2 +- .../src/main/java/io/noties/markwon/Markwon.java | 4 ++-- .../java/io/noties/markwon/MarkwonBuilderImpl.java | 2 +- .../java/io/noties/markwon/MarkwonConfiguration.java | 10 +++++----- .../src/main/java/io/noties/markwon/MarkwonImpl.java | 4 ++-- .../main/java/io/noties/markwon/core/CorePlugin.java | 4 ++-- .../io/noties/markwon/core/spans/TextLayoutSpan.java | 2 +- .../io/noties/markwon/core/spans/TextViewSpan.java | 2 +- .../io/noties/markwon/image/AsyncDrawableSpan.java | 2 +- .../image/destination/ImageDestinationProcessor.java | 2 +- .../destination/ImageDestinationProcessorAssets.java | 2 +- .../ImageDestinationProcessorRelativeToAbsolute.java | 2 +- .../main/java/io/noties/markwon/utils/LayoutUtils.java | 2 +- .../main/java/io/noties/markwon/utils/SpanUtils.java | 2 +- .../io/noties/markwon/ext/latex/JLatexMathPlugin.java | 2 +- .../io/noties/markwon/ext/tables/TableRowSpan.java | 4 ++-- .../noties/markwon/html/HtmlEmptyTagReplacement.java | 2 +- .../main/java/io/noties/markwon/html/HtmlPlugin.java | 4 ++-- release-management.md | 3 ++- 20 files changed, 30 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4607ed5..38b9e884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -# $nap; +# 4.4.0 * `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`) * `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed) * `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235]) diff --git a/gradle.properties b/gradle.properties index c1d97523..559ebd01 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.4.0-SNAPSHOT +VERSION_NAME=4.4.0 GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android diff --git a/markwon-core/src/main/java/io/noties/markwon/Markwon.java b/markwon-core/src/main/java/io/noties/markwon/Markwon.java index 1422bf3c..326a70c2 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -196,9 +196,9 @@ public interface Builder { * Control if small chunks of non-finished markdown sentences (for example, a single `*` character) * should be displayed/rendered as raw input instead of an empty string. *

- * Since $nap; {@code true} by default, versions prior - {@code false} + * Since 4.4.0 {@code true} by default, versions prior - {@code false} * - * @since $nap; + * @since 4.4.0 */ @NonNull Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty); diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java index 9d470f87..ae0f2f9d 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java @@ -27,7 +27,7 @@ class MarkwonBuilderImpl implements Markwon.Builder { private Markwon.TextSetter textSetter; - // @since $nap; + // @since 4.4.0 private boolean fallbackToRawInputWhenEmpty = true; MarkwonBuilderImpl(@NonNull Context context) { diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java index e19c8228..9b37e89f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java @@ -24,7 +24,7 @@ public static Builder builder() { private final AsyncDrawableLoader asyncDrawableLoader; private final SyntaxHighlight syntaxHighlight; private final LinkResolver linkResolver; - // @since $nap; + // @since 4.4.0 private final ImageDestinationProcessor imageDestinationProcessor; private final ImageSizeResolver imageSizeResolver; @@ -62,7 +62,7 @@ public LinkResolver linkResolver() { } /** - * @since $nap; + * @since 4.4.0 */ @NonNull public ImageDestinationProcessor imageDestinationProcessor() { @@ -89,7 +89,7 @@ public static class Builder { private AsyncDrawableLoader asyncDrawableLoader; private SyntaxHighlight syntaxHighlight; private LinkResolver linkResolver; - // @since $nap; + // @since 4.4.0 private ImageDestinationProcessor imageDestinationProcessor; private ImageSizeResolver imageSizeResolver; private MarkwonSpansFactory spansFactory; @@ -119,7 +119,7 @@ public Builder linkResolver(@NonNull LinkResolver linkResolver) { } /** - * @since $nap; + * @since 4.4.0 */ @NonNull public Builder imageDestinationProcessor(@NonNull ImageDestinationProcessor imageDestinationProcessor) { @@ -157,7 +157,7 @@ public MarkwonConfiguration build( linkResolver = new LinkResolverDef(); } - // @since $nap; + // @since 4.4.0 if (imageDestinationProcessor == null) { imageDestinationProcessor = ImageDestinationProcessor.noOp(); } diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java index 999e2f68..1e0d7930 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java @@ -30,7 +30,7 @@ class MarkwonImpl extends Markwon { @Nullable private final TextSetter textSetter; - // @since $nap; + // @since 4.4.0 private final boolean fallbackToRawInputWhenEmpty; MarkwonImpl( @@ -96,7 +96,7 @@ public Spanned render(@NonNull Node node) { public Spanned toMarkdown(@NonNull String input) { final Spanned spanned = render(parse(input)); - // @since $nap; + // @since 4.4.0 // if spanned is empty, we are configured to use raw input and input is not empty if (TextUtils.isEmpty(spanned) && fallbackToRawInputWhenEmpty diff --git a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java index 36b8b277..1c576e7f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java @@ -97,7 +97,7 @@ public static CorePlugin create() { /** * @return a set with enabled by default block types - * @since $nap; + * @since 4.4.0 */ @NonNull public static Set> enabledBlockTypes() { @@ -175,7 +175,7 @@ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { OrderedListItemSpan.measure(textView, markdown); - // @since $nap; + // @since 4.4.0 // we do not break API compatibility, instead we introduce the `instance of` check if (markdown instanceof Spannable) { final Spannable spannable = (Spannable) markdown; diff --git a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java index 679bd58c..e4e2e57b 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java @@ -10,7 +10,7 @@ import java.lang.ref.WeakReference; /** - * @since $nap; + * @since 4.4.0 */ public class TextLayoutSpan { diff --git a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java index 3e527a9f..71db81b8 100644 --- a/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java +++ b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java @@ -12,7 +12,7 @@ /** * A special span that allows to obtain {@code TextView} in which spans are displayed * - * @since $nap; + * @since 4.4.0 */ public class TextViewSpan { 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 fe34b4c9..27e43720 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 @@ -100,7 +100,7 @@ public void draw( int bottom, @NonNull Paint paint) { - // @since $nap; use SpanUtils instead of `canvas.getWidth` + // @since 4.4.0 use SpanUtils instead of `canvas.getWidth` drawable.initWithKnownDimensions( SpanUtils.width(canvas, text), paint.getTextSize() diff --git a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java index 831c0b7c..ec70f8f3 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java @@ -5,7 +5,7 @@ /** * Process destination of image nodes * - * @since $nap; + * @since 4.4.0 */ public abstract class ImageDestinationProcessor { @NonNull diff --git a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java index 169d2527..b52a04ca 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java @@ -15,7 +15,7 @@ * {@code FileSchemeHandler} from the {@code image} module supports asset images when created with * {@code createWithAssets} factory method * - * @since $nap; + * @since 4.4.0 */ public class ImageDestinationProcessorAssets extends ImageDestinationProcessor { diff --git a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java index 9e04e1aa..23f1c706 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java @@ -7,7 +7,7 @@ import java.net.URL; /** - * @since $nap; + * @since 4.4.0 */ public class ImageDestinationProcessorRelativeToAbsolute extends ImageDestinationProcessor { diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java index d1c4cb77..32c763e4 100644 --- a/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java +++ b/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; /** - * @since $nap; + * @since 4.4.0 */ public abstract class LayoutUtils { diff --git a/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java b/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java index 96396add..baf0449a 100644 --- a/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java +++ b/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java @@ -11,7 +11,7 @@ import io.noties.markwon.core.spans.TextViewSpan; /** - * @since $nap; + * @since 4.4.0 */ public abstract class SpanUtils { diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java index abd399e3..dd8a606e 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -529,7 +529,7 @@ private static class InlineImageSizeResolver extends ImageSizeResolver { @Override public Rect resolveImageSize(@NonNull AsyncDrawable drawable) { - // @since $nap; resolve inline size (scale down if exceed available width) + // @since 4.4.0 resolve inline size (scale down if exceed available width) final Rect imageBounds = drawable.getResult().getBounds(); final int canvasWidth = drawable.getLastKnownCanvasWidth(); final int w = imageBounds.width(); diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java index 0bf48626..c4fd8204 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java @@ -339,10 +339,10 @@ public void run() { false ); - // @since $nap; + // @since 4.4.0 TextLayoutSpan.applyTo(spannable, layout); - // @since $nap; + // @since 4.4.0 scheduleAsyncDrawables(spannable, recreate); layouts.add(index, layout); diff --git a/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java index 8ad5f05a..b074dcea 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/HtmlEmptyTagReplacement.java @@ -46,7 +46,7 @@ public String replace(@NonNull HtmlTag tag) { replacement = alt; } } else if ("iframe".equals(name)) { - // @since $nap; make iframe non-empty + // @since 4.4.0 make iframe non-empty replacement = IFRAME_REPLACEMENT; } else { replacement = null; diff --git a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java index 5e974ea0..f2c2ea54 100644 --- a/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java +++ b/markwon-html/src/main/java/io/noties/markwon/html/HtmlPlugin.java @@ -57,7 +57,7 @@ public static HtmlPlugin create(@NonNull HtmlConfigure configure) { private MarkwonHtmlParser htmlParser; private MarkwonHtmlRenderer htmlRenderer; - // @since $nap; + // @since 4.4.0 private HtmlEmptyTagReplacement emptyTagReplacement = new HtmlEmptyTagReplacement(); @SuppressWarnings("WeakerAccess") @@ -109,7 +109,7 @@ public HtmlPlugin excludeDefaults(boolean excludeDefaults) { /** * @param emptyTagReplacement {@link HtmlEmptyTagReplacement} - * @since $nap; + * @since 4.4.0 */ @NonNull public HtmlPlugin emptyTagReplacement(@NonNull HtmlEmptyTagReplacement emptyTagReplacement) { diff --git a/release-management.md b/release-management.md index ce66bb97..da2721d5 100644 --- a/release-management.md +++ b/release-management.md @@ -38,7 +38,8 @@ For example, `@since $nap` seems like a good candidate. For this a live template whenever a new API method/field/functionality-change is introduced (`snc`): ``` -@since $nap; +// semicolon with a space so this one is not accedentally replaced with release version +@since $nap ; ``` This live template would be possible to use in both inline comment and javadoc comment.