Merge pull request #191 from noties/v4.2.1

V4.2.1
This commit is contained in:
Dimitry 2020-02-02 16:59:13 +02:00 committed by GitHub
commit c939c0fa5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 596 additions and 45 deletions

View File

@ -1,5 +1,12 @@
# Changelog
# 4.2.1
* Fix SpannableBuilder `subSequence` method
* Introduce Nougat check in `BulletListItemSpan` to position bullet (for bullets to be
positioned correctly when nested inside other `LeadingMarginSpan`s)
* Reduced number of invalidations in AsyncDrawable when result is ready
* AsyncDrawable#hasKnownDimentions -> AsyncDrawable#hasKnownDimensions typo fix
# 4.2.0
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])

View File

@ -4,7 +4,6 @@
package="io.noties.markwon.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
<application
android:name=".App"

View File

@ -25,6 +25,7 @@ import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.file.FileSchemeHandler;
import io.noties.markwon.image.gif.GifMediaDecoder;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
@ -105,7 +106,8 @@ public class MarkdownRenderer {
// default-media-decoder is also added automatically
plugin
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()));
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()))
.addMediaDecoder(GifMediaDecoder.create(false));
}
}))
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))

View File

@ -4,8 +4,8 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
}
}
@ -55,6 +55,11 @@ ext {
final def commonMarkVersion = '0.13.0'
final def daggerVersion = '2.10'
// please note that `pl.droidsonroids.gif:android-gif-drawable:1.2.15` is used due to the minimum
// api level mismatch that Markwon supports (16) and later versions of AndroidGifDrawable (17).
// It should not be a problem as this dependency is used as `compileOnly` and users
// must specify version explicitly (until library's API changes...)
deps = [
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
@ -63,7 +68,7 @@ ext {
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
'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.14',
'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15',
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0',
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
'prism4j' : 'io.noties:prism4j:2.0.0',

View File

@ -32,7 +32,7 @@ export default {
margin: 0.25em;
border-radius: 0.25em;
box-shadow: 0 0 0.1em 0.1em #eee;
max-width: 30%;
max-width: 25%;
min-width: 100px;
display: flex;
align-items: center;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -6,7 +6,7 @@ title: 'Introduction'
<br><br>
[![markwon](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=markwon)](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20)
[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon)
[![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](https://github.com/noties/Markwon/actions)
**Markwon** is a markdown library for Android. It parses markdown following
<Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library
@ -79,7 +79,26 @@ and 2 themes included: Light &amp; Dark. It can be downloaded from [releases](ht
:::
## Awesome Markwon
<style>
.awesome-block {
background-color: #4CAF50;
height: 7rem;
display: flex;
justify-content: center;
align-items: center;
}
.awesome-block * {
border: 0
}
</style>
<div class="awesome-block">
## # Awesome Markwon
</div>
<u>Applications using Markwon</u>:
@ -88,9 +107,12 @@ and 2 themes included: Light &amp; Dark. It can be downloaded from [releases](ht
* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds.
<AwesomeGroup :apps="[
{name: 'Cinopsys: Movies and Shows', image: 'http://drive.google.com/uc?export=view&id=1rD0HLd8tDUCe8QcVEG_iGvsJbFyozRhC', link: 'https://play.google.com/store/apps/details?id=com.cinopsys.movieshows'}
{name: 'Nextcloud', image: $withBase(`/assets/apps/nextcloud.png`), link: 'https://github.com/nextcloud/android', description: 'A safe home for all your data. Access & share your files, calendars, contacts, mail & more from any device, on your terms.'},
{name: 'Habitica', image: $withBase(`/assets/apps/habitica.png`), link: 'https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', description: 'Treat your life like a game to stay motivated and organized! Habitica makes it simple to have fun while accomplishing goals.'},
{name: 'Cinopsys: Movies and Shows', image: $withBase(`/assets/apps/cinopsys.png`), link: 'https://play.google.com/store/apps/details?id=com.cinopsys.movieshows'}
]" />
<u>Extension/plugins</u>:
* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background.

View File

@ -184,11 +184,12 @@ imagesPlugin.addSchemeHandler(new SchemeHandler() {
:::warning
If you wish to add support for **SVG** or **GIF** you must explicitly add these dependencies
to your project:
* for `SVG`: `com.caverock:androidsvg:1.4`
* for `GIF`: `pl.droidsonroids.gif:android-gif-drawable:1.2.14`
* to support `SVG`: [com.caverock:androidsvg](https://github.com/BigBadaboom/androidsvg)
* to support `GIF`: [pl.droidsonroids.gif:android-gif-drawable](https://github.com/koral--/android-gif-drawable)
You can try more recent versions of these libraries, but make sure that they doesn't
introduce any unexpected behavior.
For [security reasons](https://github.com/noties/Markwon/issues/186) it's advisable to use latest
versions of these libraries. If you notice compilation and/or runtime issues when used with Markwon,
please [create an issue](https://github.com/noties/Markwon/issues/new) specifying library and library version used.
:::

View File

@ -8,7 +8,7 @@ android.enableJetifier=true
android.enableBuildCache=true
android.buildCacheDir=build/pre-dex-cache
VERSION_NAME=4.2.0
VERSION_NAME=4.2.1
GROUP=io.noties.markwon
POM_DESCRIPTION=Markwon markdown for Android

View File

@ -187,7 +187,7 @@ public class SpannableBuilder implements Appendable, CharSequence {
// if a span was fully including resulting subSequence it's start and
// end must be within 0..length bounds
s = Math.max(0, span.start - start);
e = Math.max(length, s + (span.end - span.start));
e = Math.min(length, s + (span.end - span.start));
builder.setSpan(
span.what,

View File

@ -4,6 +4,7 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
@ -15,6 +16,13 @@ import io.noties.markwon.utils.LeadingMarginUtils;
public class BulletListItemSpan implements LeadingMarginSpan {
private static final boolean IS_NOUGAT;
static {
final int sdk = Build.VERSION.SDK_INT;
IS_NOUGAT = Build.VERSION_CODES.N == sdk || Build.VERSION_CODES.N_MR1 == sdk;
}
private MarkwonTheme theme;
private final Paint paint = ObjectsPool.paint();
@ -62,28 +70,41 @@ public class BulletListItemSpan implements LeadingMarginSpan {
final int marginLeft = (width - side) / 2;
// @since 2.0.2
// There is a bug in Android Nougat, when this span receives an `x` that
// doesn't correspond to what it should be (text is placed correctly though).
// Let's make this a general rule -> manually calculate difference between expected/actual
// and add this difference to resulting left/right values. If everything goes well
// we do not encounter a bug -> this `diff` value will be 0
final int diff;
if (dir < 0) {
// rtl
diff = x - (layout.getWidth() - (width * level));
} else {
diff = (width * level) - x;
}
// in order to support RTL
final int l;
final int r;
{
final int left = x + (dir * marginLeft);
final int right = left + (dir * side);
l = Math.min(left, right) + (dir * diff);
r = Math.max(left, right) + (dir * diff);
// @since 4.2.1 to correctly position bullet
// when nested inside other LeadingMarginSpans (sorry, Nougat)
if (IS_NOUGAT) {
// @since 2.0.2
// There is a bug in Android Nougat, when this span receives an `x` that
// doesn't correspond to what it should be (text is placed correctly though).
// Let's make this a general rule -> manually calculate difference between expected/actual
// and add this difference to resulting left/right values. If everything goes well
// we do not encounter a bug -> this `diff` value will be 0
final int diff;
if (dir < 0) {
// rtl
diff = x - (layout.getWidth() - (width * level));
} else {
diff = (width * level) - x;
}
final int left = x + (dir * marginLeft);
final int right = left + (dir * side);
l = Math.min(left, right) + (dir * diff);
r = Math.max(left, right) + (dir * diff);
} else {
if (dir > 0) {
l = x + marginLeft;
} else {
l = x - width + marginLeft;
}
r = l + side;
}
}
final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2);

View File

@ -55,6 +55,7 @@ public class AsyncDrawable extends Drawable {
/**
* @since 4.0.0
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public ImageSize getImageSize() {
return imageSize;
@ -63,20 +64,33 @@ public class AsyncDrawable extends Drawable {
/**
* @since 4.0.0
*/
@SuppressWarnings("unused")
@NonNull
public ImageSizeResolver getImageSizeResolver() {
return imageSizeResolver;
}
/**
* @see #hasKnownDimensions()
* @since 4.0.0
* @deprecated 4.2.1
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@Deprecated
public boolean hasKnownDimentions() {
return canvasWidth > 0;
}
/**
* @see #hasKnownDimentions()
* @since 4.2.1
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public boolean hasKnownDimensions() {
return canvasWidth > 0;
}
/**
* @see #hasKnownDimensions()
* @since 4.0.0
*/
public int getLastKnownCanvasWidth() {
@ -84,9 +98,10 @@ public class AsyncDrawable extends Drawable {
}
/**
* @see #hasKnownDimentions()
* @see #hasKnownDimensions()
* @since 4.0.0
*/
@SuppressWarnings("WeakerAccess")
public float getLastKnowTextSize() {
return textSize;
}
@ -95,6 +110,7 @@ public class AsyncDrawable extends Drawable {
return result;
}
@SuppressWarnings("WeakerAccess")
public boolean hasResult() {
return result != null;
}
@ -104,10 +120,17 @@ public class AsyncDrawable extends Drawable {
}
// yeah
public void setCallback2(@Nullable Callback callback) {
@SuppressWarnings("WeakerAccess")
public void setCallback2(@Nullable Callback cb) {
this.callback = callback;
super.setCallback(callback);
// @since 4.2.1
// wrap callback so invalidation happens to this AsyncDrawable instance
// and not for wrapped result/placeholder
this.callback = cb == null
? null
: new WrappedCallback(cb);
super.setCallback(cb);
// if not null -> means we are attached
if (callback != null) {
@ -138,6 +161,7 @@ public class AsyncDrawable extends Drawable {
/**
* @since 3.0.1
*/
@SuppressWarnings("WeakerAccess")
protected void setPlaceholderResult(@NonNull Drawable placeholder) {
// okay, if placeholder has bounds -> use it, otherwise use original imageSize
@ -175,7 +199,6 @@ public class AsyncDrawable extends Drawable {
}
this.result = result;
this.result.setCallback(callback);
initBounds();
}
@ -210,6 +233,12 @@ public class AsyncDrawable extends Drawable {
final Rect bounds = resolveBounds();
result.setBounds(bounds);
// @since 4.2.1, we set callback after bounds are resolved
// to reduce number of invalidations
result.setCallback(callback);
// so, this method will check if there is previous bounds and call invalidate _BEFORE_
// applying new bounds. This is why it is important to have initial bounds empty.
setBounds(bounds);
invalidateSelf();
@ -291,6 +320,7 @@ public class AsyncDrawable extends Drawable {
return imageSizeResolver.resolveImageSize(this);
}
@NonNull
@Override
public String toString() {
return "AsyncDrawable{" +
@ -302,4 +332,30 @@ public class AsyncDrawable extends Drawable {
", waitingForDimensions=" + waitingForDimensions +
'}';
}
// @since 4.2.1
// Wrapped callback to trigger invalidation for this AsyncDrawable instance (and not result/placeholder)
private class WrappedCallback implements Callback {
private final Callback callback;
WrappedCallback(@NonNull Callback callback) {
this.callback = callback;
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
callback.invalidateDrawable(AsyncDrawable.this);
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
callback.scheduleDrawable(AsyncDrawable.this, what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
callback.unscheduleDrawable(AsyncDrawable.this, what);
}
}
}

View File

@ -42,11 +42,9 @@ public class AsyncDrawableSpan extends ReplacementSpan {
this.alignment = alignment;
this.replacementTextIsLink = replacementTextIsLink;
// additionally set intrinsic bounds if empty
final Rect rect = drawable.getBounds();
if (rect.isEmpty()) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
// @since 4.2.1 we do not set intrinsic bounds
// at this point they will always be 0,0-1,1, but this
// will trigger another invalidation when we will have bounds
}
@Override

View File

@ -34,6 +34,7 @@
android:windowSoftInputMode="adjustResize" />
<activity android:name=".inlineparser.InlineParserActivity" />
<activity android:name=".htmldetails.HtmlDetailsActivity" />
</application>

View File

@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
import io.noties.markwon.sample.editor.EditorActivity;
import io.noties.markwon.sample.html.HtmlActivity;
import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity;
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
@ -127,6 +128,10 @@ public class MainActivity extends Activity {
activity = InlineParserActivity.class;
break;
case HTML_DETAILS:
activity = HtmlDetailsActivity.class;
break;
default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
}

View File

@ -25,7 +25,9 @@ public enum Sample {
EDITOR(R.string.sample_editor),
INLINE_PARSER(R.string.sample_inline_parser);
INLINE_PARSER(R.string.sample_inline_parser),
HTML_DETAILS(R.string.sample_html_details);
private final int textResId;

View File

@ -0,0 +1,410 @@
package io.noties.markwon.sample.htmldetails;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.LeadingMarginSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.R;
import io.noties.markwon.utils.LeadingMarginUtils;
import io.noties.markwon.utils.NoCopySpannableFactory;
public class HtmlDetailsActivity extends Activity {
private ViewGroup content;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_html_details);
content = findViewById(R.id.content);
sample_details();
}
private void sample_details() {
final String md = "# Hello\n\n<details>\n" +
" <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" +
" <p>\n\n" +
"<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" +
"## *formatted* **heading** with [a](link)\n" +
"```java\n" +
"code block\n" +
"```\n" +
"\n" +
" <details>\n" +
" <summary><small>nested</small> stuff</summary><p>\n" +
"<!-- alternative placement of p shown above -->\n" +
"\n" +
"* list\n" +
"* with\n" +
"\n\n" +
"![img](https://raw.githubusercontent.com/noties/Markwon/master/art/markwon_logo.png)\n\n" +
" 1. nested\n" +
" 1. items\n" +
"\n" +
" ```java\n" +
" // including code\n" +
" ```\n" +
" 1. blocks\n" +
"\n" +
"<details><summary>The 3rd!</summary>\n\n" +
"**bold** _em_\n</details>" +
" </p></details>\n" +
"</p></details>\n\n" +
"and **this** *is* how...";
final Markwon markwon = Markwon.builder(this)
.usePlugin(HtmlPlugin.create(plugin ->
plugin.addHandler(new DetailsTagHandler())))
.usePlugin(ImagesPlugin.create())
.build();
final Spanned spanned = markwon.toMarkdown(md);
final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class);
// if we have no details, proceed as usual (single text-view)
if (spans == null || spans.length == 0) {
// no details
final TextView textView = appendTextView();
markwon.setParsedMarkdown(textView, spanned);
return;
}
final List<DetailsElement> list = new ArrayList<>();
for (DetailsParsingSpan span : spans) {
final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list);
if (e != null) {
list.add(e);
}
}
for (DetailsElement element : list) {
initDetails(element, spanned);
}
sort(list);
TextView textView;
int start = 0;
for (DetailsElement element : list) {
if (element.start != start) {
// subSequence and add new TextView
textView = appendTextView();
textView.setText(subSequenceTrimmed(spanned, start, element.start));
}
// now add details TextView
textView = appendTextView();
initDetailsTextView(markwon, textView, element);
start = element.end;
}
if (start != spanned.length()) {
// another textView with rest content
textView = appendTextView();
textView.setText(subSequenceTrimmed(spanned, start, spanned.length()));
}
}
@NonNull
private TextView appendTextView() {
final View view = getLayoutInflater().inflate(R.layout.view_html_details_text_view, content, false);
final TextView textView = view.findViewById(R.id.text);
content.addView(view);
return textView;
}
private void initDetailsTextView(
@NonNull Markwon markwon,
@NonNull TextView textView,
@NonNull DetailsElement element) {
// minor optimization
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
// so, each element with children is a details tag
// there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans
// final SpannableStringBuilder builder = new SpannableStringBuilder();
final SpannableBuilder builder = new SpannableBuilder();
append(builder, markwon, textView, element, element);
markwon.setParsedMarkdown(textView, builder.spannableStringBuilder());
}
private void append(
@NonNull SpannableBuilder builder,
@NonNull Markwon markwon,
@NonNull TextView textView,
@NonNull DetailsElement root,
@NonNull DetailsElement element) {
if (!element.children.isEmpty()) {
final int start = builder.length();
// builder.append(element.content);
builder.append(subSequenceTrimmed(element.content, 0, element.content.length()));
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
element.expanded = !element.expanded;
initDetailsTextView(markwon, textView, root);
}
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (element.expanded) {
for (DetailsElement child : element.children) {
append(builder, markwon, textView, root, child);
}
}
builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start);
} else {
builder.append(element.content);
}
}
// if null -> remove from where it was processed,
// else replace from where it was processed with a new one (can become expandable)
@Nullable
private static DetailsElement settle(
@NonNull DetailsElement element,
@NonNull List<? extends DetailsElement> elements) {
for (DetailsElement e : elements) {
if (element.start > e.start && element.end <= e.end) {
final DetailsElement settled = settle(element, e.children);
if (settled != null) {
// the thing is we must balance children if done like this
// let's just create a tree actually, so we are easier to modify
final Iterator<DetailsElement> iterator = e.children.iterator();
while (iterator.hasNext()) {
final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element));
if (balanced == null) {
iterator.remove();
}
}
// add to our children
e.children.add(element);
}
return null;
}
}
return element;
}
private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) {
int end = element.end;
for (int i = element.children.size() - 1; i >= 0; i--) {
final DetailsElement child = element.children.get(i);
if (child.end < end) {
element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end)));
}
initDetails(child, spanned);
end = child.start;
}
final int start = (element.start + element.content.length());
if (end != start) {
element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end)));
}
}
private static void sort(@NonNull List<DetailsElement> elements) {
Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start));
for (DetailsElement element : elements) {
sort(element.children);
}
}
@NonNull
private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) {
while (start < end) {
final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start));
final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1));
if (!isStartEmpty && !isEndEmpty) {
break;
}
if (isStartEmpty) {
start += 1;
}
if (isEndEmpty) {
end -= 1;
}
}
return cs.subSequence(start, end);
}
private static class DetailsElement {
final int start;
final int end;
final CharSequence content;
final List<DetailsElement> children = new ArrayList<>(0);
boolean expanded;
DetailsElement(int start, int end, @NonNull CharSequence content) {
this.start = start;
this.end = end;
this.content = content;
}
@Override
public String toString() {
return "DetailsElement{" +
"start=" + start +
", end=" + end +
", content=" + toStringContent(content) +
", children=" + children +
", expanded=" + expanded +
'}';
}
@NonNull
private static String toStringContent(@NonNull CharSequence cs) {
return cs.toString().replaceAll("\n", "\\n");
}
}
private static class DetailsTagHandler extends TagHandler {
@Override
public void handle(
@NonNull MarkwonVisitor visitor,
@NonNull MarkwonHtmlRenderer renderer,
@NonNull HtmlTag tag) {
int summaryEnd = -1;
for (HtmlTag child : tag.getAsBlock().children()) {
if (!child.isClosed()) {
continue;
}
if ("summary".equals(child.name())) {
summaryEnd = child.end();
}
final TagHandler tagHandler = renderer.tagHandler(child.name());
if (tagHandler != null) {
tagHandler.handle(visitor, renderer, child);
} else if (child.isBlock()) {
visitChildren(visitor, renderer, child.getAsBlock());
}
}
if (summaryEnd > -1) {
visitor.builder().setSpan(new DetailsParsingSpan(
subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd)
), tag.start(), tag.end());
}
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("details");
}
}
private static class DetailsParsingSpan {
final CharSequence summary;
DetailsParsingSpan(@NonNull CharSequence summary) {
this.summary = summary;
}
}
private static class DetailsSpan implements LeadingMarginSpan {
private final DetailsElement element;
private final int blockMargin;
private final int blockQuoteWidth;
private final Rect rect = new Rect();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) {
this.element = element;
this.blockMargin = theme.getBlockMargin();
this.blockQuoteWidth = theme.getBlockQuoteWidth();
this.paint.setStyle(Paint.Style.FILL);
}
@Override
public int getLeadingMargin(boolean first) {
return blockMargin;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
if (LeadingMarginUtils.selfStart(start, text, this)) {
rect.set(x, top, x + blockMargin, bottom);
if (element.expanded) {
paint.setColor(Color.GREEN);
} else {
paint.setColor(Color.RED);
}
paint.setStyle(Paint.Style.FILL);
c.drawRect(rect, paint);
} else {
if (element.expanded) {
final int l = (blockMargin - blockQuoteWidth) / 2;
rect.set(x + l, top, x + l + blockQuoteWidth, bottom);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.GRAY);
c.drawRect(rect, paint);
}
}
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000" />

View File

@ -29,4 +29,6 @@
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
<string name="sample_html_details"># \# HTML &lt;details> tag\n\n&lt;details> tag parsed and rendered</string>
</resources>