Merge branch 'develop' into f/sample-app
This commit is contained in:
commit
186390805a
12
CHANGELOG.md
12
CHANGELOG.md
@ -8,11 +8,23 @@
|
|||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
* `html` - `SimpleTagHandler` visits children tags if supplied tag is block one ([#235])
|
* `html` - `SimpleTagHandler` visits children tags if supplied tag is block one ([#235])
|
||||||
|
* `inline-parser` - `BangInlineProcessor` properly returns `null` if no image node is found (possible to define other inline parsers that use `!` as special character)
|
||||||
|
* `image` - `AsyncDrawable` won't trigger loading if it has result (aim: `RecyclerView` due to multiple attach/detach events of a View)
|
||||||
|
* `image` - `AsyncDrawable` will resume result if it is `Animatable` and was playing before detach event (aim: `RecyclerView`) ([#241])
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
* `image-glide` cache `RequestManager` in `GlideImagesPlugin#create(Context)` factory method ([#259])
|
||||||
|
|
||||||
#### Deprecated
|
#### Deprecated
|
||||||
* `core` - `MovementMethodPlugin.create()` use explicit `MovementMethodPlugin.link()` instead
|
* `core` - `MovementMethodPlugin.create()` use explicit `MovementMethodPlugin.link()` instead
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
* `image` - `AsyncDrawable#hasKnownDimentions` (deprecated in `4.2.1`)
|
||||||
|
|
||||||
[#235]: https://github.com/noties/Markwon/issues/235
|
[#235]: https://github.com/noties/Markwon/issues/235
|
||||||
|
[#241]: https://github.com/noties/Markwon/issues/241
|
||||||
|
[#259]: https://github.com/noties/Markwon/issues/259
|
||||||
|
|
||||||
|
|
||||||
# 4.4.0
|
# 4.4.0
|
||||||
* `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`)
|
* `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`)
|
||||||
|
@ -24,8 +24,9 @@ allprojects {
|
|||||||
version = VERSION_NAME
|
version = VERSION_NAME
|
||||||
group = GROUP
|
group = GROUP
|
||||||
|
|
||||||
|
// do we actually need javadoc any more?
|
||||||
tasks.withType(Javadoc) {
|
tasks.withType(Javadoc) {
|
||||||
options.addStringOption('Xdoclint:none', '-quiet')
|
enabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ public class AsyncDrawable extends Drawable {
|
|||||||
private final ImageSize imageSize;
|
private final ImageSize imageSize;
|
||||||
private final ImageSizeResolver imageSizeResolver;
|
private final ImageSizeResolver imageSizeResolver;
|
||||||
|
|
||||||
|
// @since $nap;
|
||||||
|
private final Drawable placeholder;
|
||||||
|
|
||||||
private Drawable result;
|
private Drawable result;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
|
||||||
@ -27,6 +30,10 @@ public class AsyncDrawable extends Drawable {
|
|||||||
// @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
|
// @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
|
||||||
private boolean waitingForDimensions;
|
private boolean waitingForDimensions;
|
||||||
|
|
||||||
|
// @since $nap; in case if result is Animatable and this drawable was detached, we
|
||||||
|
// keep the state to resume when we are going to be attached again (when used in RecyclerView)
|
||||||
|
private boolean wasPlayingBefore = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 1.0.1
|
* @since 1.0.1
|
||||||
*/
|
*/
|
||||||
@ -41,7 +48,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
this.imageSizeResolver = imageSizeResolver;
|
this.imageSizeResolver = imageSizeResolver;
|
||||||
this.imageSize = imageSize;
|
this.imageSize = imageSize;
|
||||||
|
|
||||||
final Drawable placeholder = loader.placeholder(this);
|
final Drawable placeholder = this.placeholder = loader.placeholder(this);
|
||||||
if (placeholder != null) {
|
if (placeholder != null) {
|
||||||
setPlaceholderResult(placeholder);
|
setPlaceholderResult(placeholder);
|
||||||
}
|
}
|
||||||
@ -70,17 +77,6 @@ public class AsyncDrawable extends Drawable {
|
|||||||
return imageSizeResolver;
|
return imageSizeResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see #hasKnownDimensions()
|
|
||||||
* @since 4.0.0
|
|
||||||
* @deprecated 4.2.1
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
|
||||||
@Deprecated
|
|
||||||
public boolean hasKnownDimentions() {
|
|
||||||
return canvasWidth > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 4.2.1
|
* @since 4.2.1
|
||||||
*/
|
*/
|
||||||
@ -110,7 +106,6 @@ public class AsyncDrawable extends Drawable {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public boolean hasResult() {
|
public boolean hasResult() {
|
||||||
return result != null;
|
return result != null;
|
||||||
}
|
}
|
||||||
@ -119,8 +114,6 @@ public class AsyncDrawable extends Drawable {
|
|||||||
return getCallback() != null;
|
return getCallback() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// yeah
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public void setCallback2(@Nullable Callback cb) {
|
public void setCallback2(@Nullable Callback cb) {
|
||||||
|
|
||||||
// @since 4.2.1
|
// @since 4.2.1
|
||||||
@ -143,7 +136,21 @@ public class AsyncDrawable extends Drawable {
|
|||||||
result.setCallback(callback);
|
result.setCallback(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @since $nap; we trigger loading only if we have no result (and result is not placeholder)
|
||||||
|
final boolean shouldLoad = result == null || result == placeholder;
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
result.setCallback(callback);
|
||||||
|
|
||||||
|
// @since $nap;
|
||||||
|
if (result instanceof Animatable && wasPlayingBefore) {
|
||||||
|
((Animatable) result).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldLoad) {
|
||||||
loader.load(this);
|
loader.load(this);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
|
||||||
@ -151,9 +158,14 @@ public class AsyncDrawable extends Drawable {
|
|||||||
|
|
||||||
// let's additionally stop if it Animatable
|
// let's additionally stop if it Animatable
|
||||||
if (result instanceof Animatable) {
|
if (result instanceof Animatable) {
|
||||||
((Animatable) result).stop();
|
final Animatable animatable = (Animatable) result;
|
||||||
|
final boolean isPlaying = wasPlayingBefore = animatable.isRunning();
|
||||||
|
if (isPlaying) {
|
||||||
|
animatable.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loader.cancel(this);
|
loader.cancel(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,6 +229,9 @@ public class AsyncDrawable extends Drawable {
|
|||||||
|
|
||||||
public void setResult(@NonNull Drawable result) {
|
public void setResult(@NonNull Drawable result) {
|
||||||
|
|
||||||
|
// @since $nap; revert this flag when we have new source
|
||||||
|
wasPlayingBefore = false;
|
||||||
|
|
||||||
// if we have previous one, detach it
|
// if we have previous one, detach it
|
||||||
if (this.result != null) {
|
if (this.result != null) {
|
||||||
this.result.setCallback(null);
|
this.result.setCallback(null);
|
||||||
@ -261,6 +276,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
waitingForDimensions = false;
|
waitingForDimensions = false;
|
||||||
|
|
||||||
final Rect bounds = resolveBounds();
|
final Rect bounds = resolveBounds();
|
||||||
|
|
||||||
result.setBounds(bounds);
|
result.setBounds(bounds);
|
||||||
// @since 4.2.1, we set callback after bounds are resolved
|
// @since 4.2.1, we set callback after bounds are resolved
|
||||||
// to reduce number of invalidations
|
// to reduce number of invalidations
|
||||||
|
@ -44,20 +44,10 @@ public class GlideImagesPlugin extends AbstractMarkwonPlugin {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static GlideImagesPlugin create(@NonNull final Context context) {
|
public static GlideImagesPlugin create(@NonNull final Context context) {
|
||||||
return create(new GlideStore() {
|
// @since $nap; cache RequestManager
|
||||||
@NonNull
|
// sometimes `cancel` would be called after activity is destroyed,
|
||||||
@Override
|
// so `Glide.with(context)` will throw an exception
|
||||||
public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
|
return create(Glide.with(context));
|
||||||
return Glide.with(context)
|
|
||||||
.load(drawable.getDestination());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cancel(@NonNull Target<?> target) {
|
|
||||||
Glide.with(context)
|
|
||||||
.clear(target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -29,7 +29,11 @@ public class BangInlineProcessor extends InlineProcessor {
|
|||||||
|
|
||||||
return node;
|
return node;
|
||||||
} else {
|
} else {
|
||||||
return text("!");
|
// @since $nap; return null in case no match (multiple inline
|
||||||
|
// processors can define `!` as _special_ character, so let them handle it)
|
||||||
|
// NB! do not forget to reset index
|
||||||
|
index = startIndex;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
|||||||
import io.noties.markwon.core.CoreProps;
|
import io.noties.markwon.core.CoreProps;
|
||||||
import io.noties.markwon.core.MarkwonTheme;
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
||||||
|
import io.noties.markwon.ext.tables.TablePlugin;
|
||||||
import io.noties.markwon.image.ImageItem;
|
import io.noties.markwon.image.ImageItem;
|
||||||
import io.noties.markwon.image.ImagesPlugin;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
import io.noties.markwon.image.SchemeHandler;
|
import io.noties.markwon.image.SchemeHandler;
|
||||||
@ -61,7 +62,8 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
|
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
|
||||||
.add("anchor", this::anchor)
|
.add("anchor", this::anchor)
|
||||||
.add("letterOrderedList", this::letterOrderedList)
|
.add("letterOrderedList", this::letterOrderedList)
|
||||||
.add("tableOfContents", this::tableOfContents);
|
.add("tableOfContents", this::tableOfContents)
|
||||||
|
.add("readMore", this::readMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -463,4 +465,16 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
// .build();
|
// .build();
|
||||||
// markwon.setMarkdown(textView, md);
|
// markwon.setMarkdown(textView, md);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
private void readMore() {
|
||||||
|
final String md = "" +
|
||||||
|
"Lorem **ipsum**  sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n\n" +
|
||||||
|
"here we ";
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(TablePlugin.create(this))
|
||||||
|
.usePlugin(new ReadMorePlugin())
|
||||||
|
.build();
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
package io.noties.markwon.sample.basicplugins;
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.ext.tables.TablePlugin;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read more plugin based on text length. It is easier to implement than lines (need to adjust
|
||||||
|
* last line to include expand/collapse text).
|
||||||
|
*/
|
||||||
|
public class ReadMorePlugin extends AbstractMarkwonPlugin {
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final int maxLength = 150;
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final String labelMore = "Show more...";
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final String labelLess = "...Show less";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
// establish connections with all _dynamic_ content that your markdown supports,
|
||||||
|
// like images, tables, latex, etc
|
||||||
|
registry.require(ImagesPlugin.class);
|
||||||
|
registry.require(TablePlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterSetText(@NonNull TextView textView) {
|
||||||
|
final CharSequence text = textView.getText();
|
||||||
|
if (text.length() < maxLength) {
|
||||||
|
// everything is OK, no need to ellipsize)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int breakAt = breakTextAt(text, 0, maxLength);
|
||||||
|
final CharSequence cs = createCollapsedString(text, 0, breakAt);
|
||||||
|
textView.setText(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
@NonNull
|
||||||
|
private CharSequence createCollapsedString(@NonNull CharSequence text, int start, int end) {
|
||||||
|
final SpannableStringBuilder builder = new SpannableStringBuilder(text, start, end);
|
||||||
|
|
||||||
|
// NB! each table row is represented as a space character and new-line (so length=2) no
|
||||||
|
// matter how many characters are inside table cells
|
||||||
|
|
||||||
|
// we can _clean_ this builder, for example remove all dynamic content (like images and tables,
|
||||||
|
// but keep them in full/expanded version)
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (true) {
|
||||||
|
// it is an implementation detail but _mostly_ dynamic content is implemented as
|
||||||
|
// ReplacementSpans
|
||||||
|
final ReplacementSpan[] spans = builder.getSpans(0, builder.length(), ReplacementSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
for (ReplacementSpan span : spans) {
|
||||||
|
builder.removeSpan(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a
|
||||||
|
// space and new-line
|
||||||
|
trim(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence fullText = createFullText(text, builder);
|
||||||
|
|
||||||
|
builder.append(' ');
|
||||||
|
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(labelMore);
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
((TextView) widget).setText(fullText);
|
||||||
|
}
|
||||||
|
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private CharSequence createFullText(@NonNull CharSequence text, @NonNull CharSequence collapsedText) {
|
||||||
|
// full/expanded text can also be different,
|
||||||
|
// for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse)
|
||||||
|
// or can contain collapse feature
|
||||||
|
final CharSequence fullText;
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (true) {
|
||||||
|
// for example let's allow collapsing
|
||||||
|
final SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||||
|
builder.append(' ');
|
||||||
|
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(labelLess);
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
((TextView) widget).setText(collapsedText);
|
||||||
|
}
|
||||||
|
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
fullText = builder;
|
||||||
|
} else {
|
||||||
|
fullText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trim(@NonNull SpannableStringBuilder builder) {
|
||||||
|
|
||||||
|
// NB! tables use `\u00a0` (non breaking space) which is not reported as white-space
|
||||||
|
|
||||||
|
char c;
|
||||||
|
|
||||||
|
for (int i = 0, length = builder.length(); i < length; i++) {
|
||||||
|
c = builder.charAt(i);
|
||||||
|
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||||
|
if (i > 0) {
|
||||||
|
builder.replace(0, i, "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = builder.length() - 1; i >= 0; i--) {
|
||||||
|
c = builder.charAt(i);
|
||||||
|
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||||
|
if (i < builder.length() - 1) {
|
||||||
|
builder.replace(i, builder.length(), "");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// depending on your locale these can be different
|
||||||
|
// There is a BreakIterator in Android, but it is not reliable, still theoretically
|
||||||
|
// it should work better than hand-written and hardcoded rules
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private static int breakTextAt(@NonNull CharSequence text, int start, int max) {
|
||||||
|
|
||||||
|
int last = start;
|
||||||
|
|
||||||
|
// no need to check for _start_ (anyway will be ignored)
|
||||||
|
for (int i = start + max - 1; i > start; i--) {
|
||||||
|
final char c = text.charAt(i);
|
||||||
|
if (Character.isWhitespace(c)
|
||||||
|
|| c == '.'
|
||||||
|
|| c == ','
|
||||||
|
|| c == '!'
|
||||||
|
|| c == '?') {
|
||||||
|
// include this special character
|
||||||
|
last = i - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last <= start) {
|
||||||
|
// when used in subSequence last index is exclusive,
|
||||||
|
// so given max=150 would result in 0-149 subSequence
|
||||||
|
return start + max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ package io.noties.markwon.sample.core;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@ -10,14 +12,20 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.commonmark.node.Block;
|
import org.commonmark.node.Block;
|
||||||
import org.commonmark.node.BlockQuote;
|
import org.commonmark.node.BlockQuote;
|
||||||
|
import org.commonmark.node.Link;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.LinkResolver;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
import io.noties.markwon.core.CorePlugin;
|
import io.noties.markwon.core.CorePlugin;
|
||||||
|
import io.noties.markwon.core.CoreProps;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.core.spans.LinkSpan;
|
||||||
import io.noties.markwon.movement.MovementMethodPlugin;
|
import io.noties.markwon.movement.MovementMethodPlugin;
|
||||||
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
||||||
import io.noties.markwon.sample.MenuOptions;
|
import io.noties.markwon.sample.MenuOptions;
|
||||||
@ -37,7 +45,8 @@ public class CoreActivity extends ActivityWithMenuOptions {
|
|||||||
.add("enabledBlockTypes", this::enabledBlockTypes)
|
.add("enabledBlockTypes", this::enabledBlockTypes)
|
||||||
.add("implicitMovementMethod", this::implicitMovementMethod)
|
.add("implicitMovementMethod", this::implicitMovementMethod)
|
||||||
.add("explicitMovementMethod", this::explicitMovementMethod)
|
.add("explicitMovementMethod", this::explicitMovementMethod)
|
||||||
.add("explicitMovementMethodPlugin", this::explicitMovementMethodPlugin);
|
.add("explicitMovementMethodPlugin", this::explicitMovementMethodPlugin)
|
||||||
|
.add("linkTitle", this::linkTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -212,4 +221,59 @@ public class CoreActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void linkTitle() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Links\n\n" +
|
||||||
|
"[link title](#)";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
builder.setFactory(Link.class, (configuration, props) ->
|
||||||
|
new ClickSelfSpan(
|
||||||
|
configuration.theme(),
|
||||||
|
CoreProps.LINK_DESTINATION.require(props),
|
||||||
|
configuration.linkResolver()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClickSelfSpan extends LinkSpan {
|
||||||
|
|
||||||
|
ClickSelfSpan(
|
||||||
|
@NonNull MarkwonTheme theme,
|
||||||
|
@NonNull String link,
|
||||||
|
@NonNull LinkResolver resolver) {
|
||||||
|
super(theme, link, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) {
|
||||||
|
Log.e("CLICK", "title: '" + linkTitle(widget) + "'");
|
||||||
|
super.onClick(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private CharSequence linkTitle(@NonNull View widget) {
|
||||||
|
if (!(widget instanceof TextView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Spanned spanned = (Spanned) ((TextView) widget).getText();
|
||||||
|
final int start = spanned.getSpanStart(this);
|
||||||
|
final int end = spanned.getSpanEnd(this);
|
||||||
|
|
||||||
|
if (start < 0 || end < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spanned.subSequence(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
package io.noties.markwon.sample.inlineparser;
|
package io.noties.markwon.sample.inlineparser;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Point;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -10,11 +20,13 @@ import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
|||||||
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
||||||
import org.commonmark.node.Block;
|
import org.commonmark.node.Block;
|
||||||
import org.commonmark.node.BlockQuote;
|
import org.commonmark.node.BlockQuote;
|
||||||
|
import org.commonmark.node.CustomNode;
|
||||||
import org.commonmark.node.FencedCodeBlock;
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
import org.commonmark.node.Heading;
|
import org.commonmark.node.Heading;
|
||||||
import org.commonmark.node.HtmlBlock;
|
import org.commonmark.node.HtmlBlock;
|
||||||
import org.commonmark.node.IndentedCodeBlock;
|
import org.commonmark.node.IndentedCodeBlock;
|
||||||
import org.commonmark.node.ListBlock;
|
import org.commonmark.node.ListBlock;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.node.ThematicBreak;
|
import org.commonmark.node.ThematicBreak;
|
||||||
import org.commonmark.parser.InlineParserFactory;
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
@ -22,13 +34,17 @@ import org.commonmark.parser.Parser;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
|
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
|
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.InlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||||
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
|
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
|
||||||
@ -49,7 +65,8 @@ public class InlineParserActivity extends ActivityWithMenuOptions {
|
|||||||
.add("pluginWithDefaults", this::pluginWithDefaults)
|
.add("pluginWithDefaults", this::pluginWithDefaults)
|
||||||
.add("pluginNoDefaults", this::pluginNoDefaults)
|
.add("pluginNoDefaults", this::pluginNoDefaults)
|
||||||
.add("disableHtmlInlineParser", this::disableHtmlInlineParser)
|
.add("disableHtmlInlineParser", this::disableHtmlInlineParser)
|
||||||
.add("disableHtmlSanitize", this::disableHtmlSanitize);
|
.add("disableHtmlSanitize", this::disableHtmlSanitize)
|
||||||
|
.add("tooltip", this::tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -244,4 +261,165 @@ public class InlineParserActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void tooltip() {
|
||||||
|
// NB! tooltip contents cannot have new lines
|
||||||
|
final String md = "" +
|
||||||
|
"\n" +
|
||||||
|
"\n" +
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n" +
|
||||||
|
"\n" +
|
||||||
|
"Aenean at urna leo. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla facilisi. Integer lectus elit, congue a orci sed, dignissim sagittis sem. Aenean et pretium magna, nec ornare justo. Sed quis nunc blandit, luctus justo eget, pellentesque arcu. Pellentesque porta semper tristique. Donec et odio arcu. Nullam ultrices gravida congue. Praesent vel leo sed orci tempor luctus. Vivamus eget tortor arcu. Nullam sapien nulla, iaculis sit amet semper in, mattis nec metus. In porttitor augue id elit euismod mattis. Ut est justo, dapibus suscipit erat eu, pellentesque porttitor magna.\n" +
|
||||||
|
"\n" +
|
||||||
|
"Nunc porta orci eget dictum malesuada. Donec vehicula felis sit amet leo tincidunt placerat. Cras quis elit faucibus, porta elit at, sodales tortor. Donec elit mi, eleifend et maximus vitae, pretium varius velit. Integer maximus egestas urna, at semper augue egestas vitae. Phasellus arcu tellus, tincidunt eget tellus nec, hendrerit mollis mauris. Pellentesque commodo urna quis nisi ultrices, quis vehicula felis ultricies. Vivamus eu feugiat leo.\n" +
|
||||||
|
"\n" +
|
||||||
|
"Etiam sit amet lorem et eros suscipit rhoncus a a tellus. Sed pharetra dui purus, quis molestie leo congue nec. Suspendisse sed scelerisque quam. Vestibulum non laoreet felis. Fusce interdum euismod purus at scelerisque. Vivamus tempus varius nibh, sed accumsan nisl interdum non. Pellentesque rutrum egestas eros sit amet sollicitudin. Vivamus ultrices est erat. Curabitur gravida justo non felis euismod mollis. Ut porta finibus nulla, sed pellentesque purus euismod ac.\n" +
|
||||||
|
"\n" +
|
||||||
|
"Aliquam erat volutpat. Nullam suscipit sit amet tortor vel fringilla. Nulla facilisi. Nullam lacinia ex lacus, sit amet scelerisque justo semper a. Nullam ullamcorper, erat ac malesuada porta, augue erat sagittis mi, in auctor turpis mauris nec orci. Nunc sit amet felis placerat, pharetra diam nec, dapibus metus. Proin nulla orci, iaculis vitae vulputate vel, placerat ac erat. Morbi sit amet blandit velit. Cras consectetur vehicula lacus vel sagittis. Nunc tincidunt lacus in blandit faucibus. Curabitur vestibulum auctor vehicula. Sed quis ligula sit amet quam venenatis venenatis eget id felis. Maecenas feugiat nisl elit, facilisis tempus risus malesuada quis. " +
|
||||||
|
"# Hello tooltip!\n\n" +
|
||||||
|
"This is the !{tooltip label}(and actual content comes here)\n\n" +
|
||||||
|
"what if it is !{here}(The contents can be blocks, limited though) instead?\n\n" +
|
||||||
|
" anyway";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(MarkwonInlineParserPlugin.create(factoryBuilder ->
|
||||||
|
factoryBuilder.addInlineProcessor(new TooltipInlineProcessor())))
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||||
|
builder.on(TooltipNode.class, (visitor, tooltipNode) -> {
|
||||||
|
final int start = visitor.length();
|
||||||
|
visitor.builder().append(tooltipNode.label);
|
||||||
|
visitor.setSpans(start, new TooltipSpan(tooltipNode.contents));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TooltipInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
// NB! without bang
|
||||||
|
// `\\{` is required (although marked as redundant), without it - runtime crash
|
||||||
|
@SuppressWarnings("RegExpRedundantEscape")
|
||||||
|
private static final Pattern RE = Pattern.compile("\\{(.+?)\\}\\((.+?)\\)");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected Node parse() {
|
||||||
|
final String match = match(RE);
|
||||||
|
if (match == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Matcher matcher = RE.matcher(match);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
final String label = matcher.group(1);
|
||||||
|
final String contents = matcher.group(2);
|
||||||
|
return new TooltipNode(label, contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TooltipNode extends CustomNode {
|
||||||
|
final String label;
|
||||||
|
final String contents;
|
||||||
|
|
||||||
|
TooltipNode(@NonNull String label, @NonNull String contents) {
|
||||||
|
this.label = label;
|
||||||
|
this.contents = contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TooltipSpan extends ClickableSpan {
|
||||||
|
final String contents;
|
||||||
|
|
||||||
|
TooltipSpan(@NonNull String contents) {
|
||||||
|
this.contents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
// just to be safe
|
||||||
|
if (!(widget instanceof TextView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TextView textView = (TextView) widget;
|
||||||
|
final Spannable spannable = (Spannable) textView.getText();
|
||||||
|
|
||||||
|
// find self ending position (can also obtain start)
|
||||||
|
// final int start = spannable.getSpanStart(this);
|
||||||
|
final int end = spannable.getSpanEnd(this);
|
||||||
|
|
||||||
|
// weird, didn't find self
|
||||||
|
if (/*start < 0 ||*/ end < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Layout layout = textView.getLayout();
|
||||||
|
if (layout == null) {
|
||||||
|
// also weird
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int line = layout.getLineForOffset(end);
|
||||||
|
|
||||||
|
// position inside TextView, these values must also be adjusted to parent widget
|
||||||
|
// also note that container can
|
||||||
|
final int y = layout.getLineBottom(line);
|
||||||
|
final int x = (int) (layout.getPrimaryHorizontal(end) + 0.5F);
|
||||||
|
|
||||||
|
final Window window = ((Activity) widget.getContext()).getWindow();
|
||||||
|
final View decor = window.getDecorView();
|
||||||
|
final Point point = relativeTo(decor, widget);
|
||||||
|
|
||||||
|
// new Tooltip.Builder(widget.getContext())
|
||||||
|
// .anchor(x + point.x, y + point.y)
|
||||||
|
// .text(contents)
|
||||||
|
// .create()
|
||||||
|
// .show(widget, Tooltip.Gravity.TOP, false);
|
||||||
|
|
||||||
|
// Toast is not reliable when tried to position on the screen
|
||||||
|
// but anyway, this is to showcase only
|
||||||
|
final Toast toast = Toast.makeText(widget.getContext(), contents, Toast.LENGTH_LONG);
|
||||||
|
toast.setGravity(Gravity.TOP | Gravity.START, x + point.x, y + point.y);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
|
// can customize appearance here as spans will be rendered as links
|
||||||
|
super.updateDrawState(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static Point relativeTo(@NonNull View parent, @NonNull View who) {
|
||||||
|
return relativeTo(parent, who, new Point());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static Point relativeTo(@NonNull View parent, @NonNull View who, @NonNull Point point) {
|
||||||
|
// NB! the scroll adjustments (we are interested in screen position,
|
||||||
|
// not real position inside parent)
|
||||||
|
point.x += who.getLeft();
|
||||||
|
point.y += who.getTop();
|
||||||
|
point.x -= who.getScrollX();
|
||||||
|
point.y -= who.getScrollY();
|
||||||
|
if (who != parent
|
||||||
|
&& who.getParent() instanceof View) {
|
||||||
|
relativeTo(parent, (View) who.getParent(), point);
|
||||||
|
}
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,14 @@ package io.noties.markwon.sample.recycler;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.CharacterStyle;
|
import android.text.style.CharacterStyle;
|
||||||
import android.text.style.UpdateAppearance;
|
import android.text.style.UpdateAppearance;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -87,8 +89,11 @@ public class RecyclerActivity extends Activity {
|
|||||||
plugin
|
plugin
|
||||||
.addSchemeHandler(FileSchemeHandler.createWithAssets(context))
|
.addSchemeHandler(FileSchemeHandler.createWithAssets(context))
|
||||||
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
|
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
|
||||||
.addMediaDecoder(SvgMediaDecoder.create())
|
.placeholderProvider(drawable -> {
|
||||||
.placeholderProvider(drawable -> new ColorDrawable(0xFFff0000));
|
final Drawable placeholder = new ColorDrawable(0xFFff0000);
|
||||||
|
placeholder.setBounds(0, 0, 100, 100);
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
}))
|
}))
|
||||||
// .usePlugin(PicassoImagesPlugin.create(context))
|
// .usePlugin(PicassoImagesPlugin.create(context))
|
||||||
// .usePlugin(GlideImagesPlugin.create(context))
|
// .usePlugin(GlideImagesPlugin.create(context))
|
||||||
@ -122,6 +127,8 @@ public class RecyclerActivity extends Activity {
|
|||||||
// `RemoveUnderlineSpan` will be added AFTER original, thus it will remove underline applied by original
|
// `RemoveUnderlineSpan` will be added AFTER original, thus it will remove underline applied by original
|
||||||
builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
|
builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@
|
|||||||
android:id="@+id/text_view"
|
android:id="@+id/text_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dip"
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
android:textColor="#000"
|
android:textColor="#000"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:padding="8dip"
|
|
||||||
tools:text="whatever" />
|
tools:text="whatever" />
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
Loading…
x
Reference in New Issue
Block a user