diff --git a/CHANGELOG.md b/CHANGELOG.md
index 886bd169..38b9e884 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+# 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])
+* `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`
+* 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)
+* `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
+
+
# 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/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/build.gradle b/build.gradle
index 51aa9d6c..b92f8dd9 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.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',
@@ -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/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:
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/gradle.properties b/gradle.properties
index 0e222c26..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.3.1
+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 cd277e03..326a70c2 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 extends MarkwonPlugin> 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 4.4.0 {@code true} by default, versions prior - {@code false}
+ *
+ * @since 4.4.0
+ */
+ @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..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,6 +27,9 @@ class MarkwonBuilderImpl implements Markwon.Builder {
private Markwon.TextSetter textSetter;
+ // @since 4.4.0
+ private boolean fallbackToRawInputWhenEmpty = true;
+
MarkwonBuilderImpl(@NonNull Context context) {
this.context = context;
}
@@ -71,6 +74,13 @@ public Markwon.Builder usePlugins(@NonNull Iterable extends MarkwonPlugin> 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/MarkwonConfiguration.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java
index 5b22623f..9b37e89f 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 4.4.0
+ 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 4.4.0
+ */
@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 4.4.0
+ private ImageDestinationProcessor imageDestinationProcessor;
private ImageSizeResolver imageSizeResolver;
private MarkwonSpansFactory spansFactory;
@@ -115,9 +118,12 @@ public Builder linkResolver(@NonNull LinkResolver linkResolver) {
return this;
}
+ /**
+ * @since 4.4.0
+ */
@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 4.4.0
+ if (imageDestinationProcessor == null) {
+ imageDestinationProcessor = ImageDestinationProcessor.noOp();
}
if (imageSizeResolver == null) {
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..1e0d7930 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 4.4.0
+ 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 4.4.0
+ // 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/main/java/io/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java
index 29c63a2a..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
@@ -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;
@@ -8,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;
@@ -15,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;
@@ -29,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;
@@ -48,6 +54,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;
/**
@@ -88,6 +95,23 @@ public static CorePlugin create() {
return new CorePlugin();
}
+ /**
+ * @return a set with enabled by default block types
+ * @since 4.4.0
+ */
+ @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);
@@ -150,6 +174,13 @@ public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder)
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
OrderedListItemSpan.measure(textView, markdown);
+
+ // @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;
+ TextViewSpan.applyTo(spannable, textView);
+ }
}
@Override
@@ -289,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();
@@ -493,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/core/spans/TextLayoutSpan.java b/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java
new file mode 100644
index 00000000..e4e2e57b
--- /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 4.4.0
+ */
+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..71db81b8
--- /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 4.4.0
+ */
+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..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
@@ -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 4.4.0 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/image/destination/ImageDestinationProcessor.java b/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java
new file mode 100644
index 00000000..ec70f8f3
--- /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 4.4.0
+ */
+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..b52a04ca
--- /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 4.4.0
+ */
+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..23f1c706 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 4.4.0
+ */
+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/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..32c763e4
--- /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 4.4.0
+ */
+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..baf0449a
--- /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 4.4.0
+ */
+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-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 extends MarkwonPlugin> 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
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-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..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
@@ -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());
@@ -530,7 +528,20 @@ private static class InlineImageSizeResolver extends ImageSizeResolver {
@NonNull
@Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
- return drawable.getResult().getBounds();
+
+ // @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();
+
+ 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 0248c4ef..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
@@ -4,7 +4,10 @@
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;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
@@ -20,7 +23,11 @@
import java.util.ArrayList;
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;
public class TableRowSpan extends ReplacementSpan {
@@ -67,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;
@@ -108,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;
@@ -144,8 +151,9 @@ public void draw(
int bottom,
@NonNull Paint p) {
- if (recreateLayouts(canvas.getWidth())) {
- 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...
@@ -236,7 +244,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();
@@ -293,20 +301,76 @@ private void makeNewLayouts() {
final int w = (width / columns) - padding;
this.layouts.clear();
- Cell cell;
- StaticLayout layout;
+
for (int i = 0, size = cells.size(); i < size; i++) {
- cell = cells.get(i);
- layout = new StaticLayout(
- cell.text,
- textPaint,
- w,
- alignment(cell.alignment),
- 1.F,
- .0F,
- false
- );
- layouts.add(layout);
+ makeLayout(i, w, cells.get(i));
+ }
+ }
+
+ private void makeLayout(final int index, final int width, @NonNull final Cell cell) {
+
+ 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 4.4.0
+ TextLayoutSpan.applyTo(spannable, layout);
+
+ // @since 4.4.0
+ 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) {
+
+ final AsyncDrawable drawable = span.getDrawable();
+
+ // it is absolutely crucial to check if drawable is already attached,
+ // otherwise we would end up with a loop
+ if (drawable.isAttached()) {
+ continue;
+ }
+
+ drawable.setCallback2(new CallbackAdapter() {
+ @Override
+ public void invalidateDrawable(@NonNull Drawable who) {
+ recreate.run();
+ }
+ });
+ }
}
}
@@ -330,4 +394,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/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..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
@@ -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 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 c2c13c12..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
@@ -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 4.4.0
+ 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 4.4.0
+ */
+ @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-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-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..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
@@ -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();
}
@@ -129,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);
}
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/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.
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..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;
});
}
})
@@ -434,4 +442,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/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);
+ }
}
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..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
@@ -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;
@@ -56,8 +60,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
// let's define some custom tag-handlers
textView = findViewById(R.id.text_view);
-
- align();
+
+ emptyTagReplacement();
}
// 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" +
+ "\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.