Merge pull request #215 from noties/v.4.3.0

V4.3.0
This commit is contained in:
Dimitry 2020-03-18 14:31:28 +02:00 committed by GitHub
commit 7baa70b15e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 3581 additions and 469 deletions

View File

@ -1,5 +1,57 @@
# Changelog # Changelog
# 4.3.0-SNAPSHOT
* add `MarkwonInlineParserPlugin` in `inline-parser` module
* `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin`
dependency (must be explicitly added to `Markwon` whilst configuring)
* `JLatexMathPlugin`: add `theme` (to customize both inlines and blocks)
* add `JLatexMathPlugin.ErrorHandler` to catch latex rendering errors and (optionally) display error drawable ([#204])
* `JLatexMathPlugin` add text color customization ([#207])
* `JLatexMathPlugin` will use text color of widget in which it is displayed **if color is not set explicitly**
* add `SoftBreakAddsNewLinePlugin` plugin (`core` module)
* `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75])
* add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu
* non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189])
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])
<br>Thanks to [@drakeet]
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them
```java
// default usage: new blocks parser, no inlines
final Markwon markwon = Markwon.builder(this)
.usePlugin(JLatexMathPlugin.create(textSize))
.build();
```
```java
// legacy blocks (pre `4.3.0`) parsing, no inlines
final Markwon markwon = Markwon.builder(this)
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.blocksLegacy(true)))
.build();
```
```java
// new blocks parsing and inline parsing
final Markwon markwon = Markwon.builder(this)
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
// blocksEnabled and blocksLegacy can be omitted
builder
.blocksEnabled(true)
.blocksLegacy(false)
.inlinesEnabled(true);
}))
.build();
```
[#189]: https://github.com/noties/Markwon/issues/189
[#75]: https://github.com/noties/Markwon/issues/75
[#204]: https://github.com/noties/Markwon/issues/204
[#207]: https://github.com/noties/Markwon/issues/207
[#201]: https://github.com/noties/Markwon/issues/201
[@drakeet]: https://github.com/drakeet
# 4.2.2 # 4.2.2
* Fixed `AsyncDrawable` display when it has placeholder with empty bounds ([#189]) * Fixed `AsyncDrawable` display when it has placeholder with empty bounds ([#189])
* Fixed `syntax-highlight` where code input is empty string ([#192]) * Fixed `syntax-highlight` where code input is empty string ([#192])
@ -84,7 +136,7 @@ use `Markwon#builderNoCore()` to obtain a builder without `CorePlugin`
* Added `MarkwonPlugin.Registry` and `MarkwonPlugin#configure(Registry)` method * Added `MarkwonPlugin.Registry` and `MarkwonPlugin#configure(Registry)` method
* `CorePlugin#addOnTextAddedListener` (process raw text added) * `CorePlugin#addOnTextAddedListener` (process raw text added)
* `ImageSizeResolver` signature change (accept `AsyncDrawable`) * `ImageSizeResolver` signature change (accept `AsyncDrawable`)
* `LinkResolver` is now an independent entity (previously part of `LinkSpan`) * `LinkResolver` is now an independent entity (previously part of the `LinkSpan`), `LinkSpan.Resolver` -&gt; `LinkResolver`
* `AsyncDrawableScheduler` can now be called multiple times without performance penalty * `AsyncDrawableScheduler` can now be called multiple times without performance penalty
* `AsyncDrawable` now exposes its destination, image-size, last known dimensions (canvas, text-size) * `AsyncDrawable` now exposes its destination, image-size, last known dimensions (canvas, text-size)
* `AsyncDrawableLoader` signature change (accept `AsyncDrawable`) * `AsyncDrawableLoader` signature change (accept `AsyncDrawable`)

View File

@ -4,7 +4,8 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' // on `3.5.3` tests are not run from CLI
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
} }
} }
@ -16,6 +17,7 @@ allprojects {
} }
google() google()
jcenter() jcenter()
// maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
} }
version = VERSION_NAME version = VERSION_NAME
group = GROUP group = GROUP
@ -69,7 +71,7 @@ ext {
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
'android-svg' : 'com.caverock:androidsvg:1.4', 'android-svg' : 'com.caverock:androidsvg:1.4',
'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15', 'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15',
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0', 'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.1',
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
'prism4j' : 'io.noties:prism4j:2.0.0', 'prism4j' : 'io.noties:prism4j:2.0.0',
'debug' : 'io.noties:debug:5.0.0@jar', 'debug' : 'io.noties:debug:5.0.0@jar',

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -109,7 +109,8 @@ and 2 themes included: Light &amp; Dark. It can be downloaded from [releases](ht
<AwesomeGroup :apps="[ <AwesomeGroup :apps="[
{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: '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: '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'} {name: 'Cinopsys: Movies and Shows', image: $withBase(`/assets/apps/cinopsys.png`), link: 'https://play.google.com/store/apps/details?id=com.cinopsys.movieshows'},
{name: 'Pure Writer', image: $withBase(`/assets/apps/purewriter.png`), link: 'https://play.google.com/store/apps/details?id=com.drakeet.purewriter', description: 'Never lose content editor & Markdown'}
]" /> ]" />

View File

@ -81,10 +81,15 @@ More information about props can be found [here](/docs/v4/core/render-props.md)
--- ---
:::tip Soft line break :::tip Soft line break
Since <Badge text="3.0.0" /> Markwon core does not give an option to Since <Badge text="4.3.0" /> there is a dedicated plugin to insert a new line for
insert a new line when there is a soft line break in markdown. Instead a markdown soft breaks - `SoftBreakAddsNewLinePlugin`:
custom plugin can be used: ```java
final Markwon markwon = Markwon.builder(this)
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.build();
```
It is still possible to do it manually with a custom visitor:
```java ```java
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AbstractMarkwonPlugin() {

View File

@ -71,3 +71,32 @@ public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
}); });
} }
``` ```
### BlockHandler <Badge text="4.3.0" />
Since <Badge text="4.3.0" /> there is class to control insertions of new lines after markdown blocks
`BlockHandler` (`MarkwonVisitor.BlockHandler`) and its default implementation `BlockHandlerDef`. For example,
to disable an empty new line after `Heading`:
```java
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerDef() {
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (node instanceof Heading) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// ensure new line but do not force insert one
}
} else {
super.blockEnd(visitor, node);
}
}
});
}
})
.build();
```

View File

@ -2,51 +2,130 @@
<MavenBadge4 :artifact="'ext-latex'" /> <MavenBadge4 :artifact="'ext-latex'" />
This is an extension that will help you display LaTeX formulas in your markdown. This is an extension that will help you display LaTeX content in your markdown.
Syntax is pretty simple: pre-fix and post-fix your latex with `$$` (double dollar sign). Since <Badge text="4.3.0" /> supports both blocks and inlines markdown structures (blocks only before `4.3.0`).
`$$` should be the first characters in a line.
## Blocks
Start a line with 2 (or more) `$` symbols followed by a new line:
```markdown ```markdown
$$ $$
\\text{A long division \\longdiv{12345}{13} \\text{A long division \\longdiv{12345}{13}
$$ $$
``` ```
LaTeX block content will be considered ended when a starting sequence of `$` is found on
a new line. If block was started with `$$$` it must be ended with `$$$` symbols.
## Inline
Exactly `$$` before and after _inline_ LaTeX content:
```markdown ```markdown
$$\\text{A long division \\longdiv{12345}{13}$$ $$\\text{A long division \\longdiv{12345}{13}$$
``` ```
:::warning
By default inline nodes are disabled and must be enabled explicitly:
```java ```java
Markwon.builder(context) final Markwon markwon = Markwon.builder(this)
.use(JLatexMathPlugin.create(textSize)) // required plugin to support inline parsing
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
@Override
public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
// ENABLE inlines
builder.inlinesEnabled(true);
}
}))
.build(); .build();
``` ```
Please note that usage of inline nodes **require** [MarkwonInlineParserPlugin](../inline-parser/)
:::
This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable. This extension uses [jlatexmath-android](https://github.com/noties/jlatexmath-android) artifact to create LaTeX drawable.
## Config ## Config
```java ```java
final Markwon markwon = Markwon.builder(context) // create default instance of plugin and use specified text size for both blocks and inlines
.usePlugin(JLatexMathPlugin.create(textSize, new BuilderConfigure() { JLatexMathPlugin.create(textView.getTextSize());
// create default instance of plugin and use specified text sizes
JLatexMathPlugin.create(inlineTextSize, blockTextSize);
JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
@Override @Override
public void configureBuilder(@NonNull Builder builder) { public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
builder // enable inlines (require `MarkwonInlineParserPlugin`), by default `false`
.align(JLatexMathDrawable.ALIGN_CENTER) builder.inlinesEnabled(true);
.fitCanvas(true)
.padding(paddingPx) // use pre-4.3.0 LaTeX block parsing (by default `false`)
// @since 4.0.0 - horizontal and vertical padding builder.blocksLegacy(true);
.padding(paddingHorizontalPx, paddingVerticalPx)
// @since 4.0.0 - change to provider // by default true
.backgroundProvider(() -> new MyDrawable())) builder.blocksEnabled(true);
// @since 4.0.0 - optional, by default cached-thread-pool will be used
.executorService(Executors.newCachedThreadPool()); // @since 4.3.0
builder.errorHandler(new JLatexMathPlugin.ErrorHandler() {
@Nullable
@Override
public Drawable handleError(@NonNull String latex, @NonNull Throwable error) {
// Receive error and optionally return drawable to be displayed instead
return null;
} }
})) });
.build();
// executor on which parsing of LaTeX is done (by default `Executors.newCachedThreadPool()`)
builder.executorService(Executors.newCachedThreadPool());
}
});
``` ```
## Theme
```java
JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
@Override
public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
// background provider for both inlines and blocks
// or more specific: `inlineBackgroundProvider` & `blockBackgroundProvider`
builder.theme().backgroundProvider(new JLatexMathTheme.BackgroundProvider() {
@NonNull
@Override
public Drawable provide() {
return new ColorDrawable(0xFFff0000);
}
});
// should block fit the whole canvas width, by default true
builder.theme().blockFitCanvas(true);
// horizontal alignment for block, by default ALIGN_CENTER
builder.theme().blockHorizontalAlignment(JLatexMathDrawable.ALIGN_CENTER);
// padding for both inlines and blocks
builder.theme().padding(JLatexMathTheme.Padding.all(8));
// padding for inlines
builder.theme().inlinePadding(JLatexMathTheme.Padding.symmetric(16, 8));
// padding for blocks
builder.theme().blockPadding(new JLatexMathTheme.Padding(0, 1, 2, 3));
// text color of LaTeX content for both inlines and blocks
// or more specific: `inlineTextColor` & `blockTextColor`
builder.theme().textColor(Color.RED);
}
});
```
:::tip :::tip
Since <Badge text="4.0.0" /> `JLatexMathPlugin` operates independently of `ImagesPlugin` Sometimes it is enough to use rendered to an image LaTeX formula and
inline it directly in your markdown document. For this markdown references can be useful. For example:
```markdown
<!-- your mardown -->
![markdown-reference] of a solution...
<!-- then reference prerendered and converted to base64 SVG/PNG/GIF/etc -->
[markdown-reference]: data:image/svg+xml;base64,base64encodeddata==
```
For this to work an image loader that supports data uri and base64 must be used. Default `Markwon` [image-loader](../image/) supports it out of box (including SVG support)
::: :::

View File

@ -3,6 +3,16 @@
**Experimental** commonmark-java inline parser that allows customizing **Experimental** commonmark-java inline parser that allows customizing
core features and/or extend with own. core features and/or extend with own.
:::tip
Since <Badge text="4.3.0" /> there is also `MarkwonInlineParserPlugin` which can be used
to allow other plugins to customize inline parser
```java
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create())
.build();
```
:::
Usage of _internal_ classes: Usage of _internal_ classes:
```java ```java
import org.commonmark.internal.Bracket; import org.commonmark.internal.Bracket;

View File

@ -5,8 +5,18 @@ next: /docs/v4/core/getting-started.md
# Installation # Installation
![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable) <table>
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot) <tbody>
<tr>
<td><img alt="stable" src="https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable"></td>
<td><a href="https://github.com/noties/Markwon/blob/master/CHANGELOG.md">changelog<OutboundLink/></a></td>
</tr>
<tr>
<td><img alt="snapshot" src="https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot"></td>
<td><a href="https://github.com/noties/Markwon/blob/develop/CHANGELOG.md">changelog<OutboundLink/></a></td>
</tr>
</tbody>
</table>
<ArtifactPicker4 /> <ArtifactPicker4 />

View File

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

View File

@ -0,0 +1,23 @@
package io.noties.markwon;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
/**
* @since 4.3.0
*/
public class BlockHandlerDef implements MarkwonVisitor.BlockHandler {
@Override
public void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
visitor.ensureNewLine();
}
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
visitor.forceNewLine();
}
}
}

View File

@ -5,22 +5,41 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.provider.Browser; import android.provider.Browser;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
public class LinkResolverDef implements LinkResolver { public class LinkResolverDef implements LinkResolver {
// @since 4.3.0
private static final String DEFAULT_SCHEME = "https";
@Override @Override
public void resolve(@NonNull View view, @NonNull String link) { public void resolve(@NonNull View view, @NonNull String link) {
final Uri uri = Uri.parse(link); final Uri uri = parseLink(link);
final Context context = view.getContext(); final Context context = view.getContext();
final Intent intent = new Intent(Intent.ACTION_VIEW, uri); final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
try { try {
context.startActivity(intent); context.startActivity(intent);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Log.w("LinkResolverDef", "Actvity was not found for intent, " + intent.toString()); Log.w("LinkResolverDef", "Actvity was not found for the link: '" + link + "'");
} }
} }
/**
* @since 4.3.0
*/
@NonNull
private static Uri parseLink(@NonNull String link) {
final Uri uri = Uri.parse(link);
if (TextUtils.isEmpty(uri.getScheme())) {
return uri.buildUpon()
.scheme(DEFAULT_SCHEME)
.build();
}
return uri;
}
} }

View File

@ -23,6 +23,19 @@ public interface MarkwonVisitor extends Visitor {
void visit(@NonNull MarkwonVisitor visitor, @NonNull N n); void visit(@NonNull MarkwonVisitor visitor, @NonNull N n);
} }
/**
* Primary purpose is to control the spacing applied before/after certain blocks, which
* visitors are created elsewhere
*
* @since 4.3.0
*/
interface BlockHandler {
void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node);
void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node);
}
interface Builder { interface Builder {
/** /**
@ -33,6 +46,16 @@ public interface MarkwonVisitor extends Visitor {
@NonNull @NonNull
<N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor); <N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor);
/**
* @param blockHandler to handle block start/end
* @see BlockHandler
* @see BlockHandlerDef
* @since 4.3.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
Builder blockHandler(@NonNull BlockHandler blockHandler);
@NonNull @NonNull
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps); MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps);
} }
@ -133,4 +156,14 @@ public interface MarkwonVisitor extends Visitor {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
<N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start); <N extends Node> void setSpansForNodeOptional(@NonNull Class<N> node, int start);
/**
* @since 4.3.0
*/
void blockStart(@NonNull Node node);
/**
* @since 4.3.0
*/
void blockEnd(@NonNull Node node);
} }

View File

@ -45,15 +45,20 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes; private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes;
// @since 4.3.0
private final BlockHandler blockHandler;
MarkwonVisitorImpl( MarkwonVisitorImpl(
@NonNull MarkwonConfiguration configuration, @NonNull MarkwonConfiguration configuration,
@NonNull RenderProps renderProps, @NonNull RenderProps renderProps,
@NonNull SpannableBuilder builder, @NonNull SpannableBuilder builder,
@NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) { @NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes,
@NonNull BlockHandler blockHandler) {
this.configuration = configuration; this.configuration = configuration;
this.renderProps = renderProps; this.renderProps = renderProps;
this.builder = builder; this.builder = builder;
this.nodes = nodes; this.nodes = nodes;
this.blockHandler = blockHandler;
} }
@Override @Override
@ -268,9 +273,20 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
} }
} }
@Override
public void blockStart(@NonNull Node node) {
blockHandler.blockStart(this, node);
}
@Override
public void blockEnd(@NonNull Node node) {
blockHandler.blockEnd(this, node);
}
static class BuilderImpl implements Builder { static class BuilderImpl implements Builder {
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes = new HashMap<>(); private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes = new HashMap<>();
private BlockHandler blockHandler;
@NonNull @NonNull
@Override @Override
@ -290,14 +306,28 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
return this; return this;
} }
@NonNull
@Override
public Builder blockHandler(@NonNull BlockHandler blockHandler) {
this.blockHandler = blockHandler;
return this;
}
@NonNull @NonNull
@Override @Override
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) {
// @since 4.3.0
BlockHandler blockHandler = this.blockHandler;
if (blockHandler == null) {
blockHandler = new BlockHandlerDef();
}
return new MarkwonVisitorImpl( return new MarkwonVisitorImpl(
configuration, configuration,
renderProps, renderProps,
new SpannableBuilder(), new SpannableBuilder(),
Collections.unmodifiableMap(nodes)); Collections.unmodifiableMap(nodes),
blockHandler);
} }
} }
} }

View File

@ -0,0 +1,26 @@
package io.noties.markwon;
import androidx.annotation.NonNull;
import org.commonmark.node.SoftLineBreak;
/**
* @since 4.3.0
*/
public class SoftBreakAddsNewLinePlugin extends AbstractMarkwonPlugin {
@NonNull
public static SoftBreakAddsNewLinePlugin create() {
return new SoftBreakAddsNewLinePlugin();
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor<SoftLineBreak>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
visitor.ensureNewLine();
}
});
}
}

View File

@ -210,17 +210,14 @@ public class CorePlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) {
visitor.ensureNewLine(); visitor.blockStart(blockQuote);
final int length = visitor.length(); final int length = visitor.length();
visitor.visitChildren(blockQuote); visitor.visitChildren(blockQuote);
visitor.setSpansForNodeOptional(blockQuote, length); visitor.setSpansForNodeOptional(blockQuote, length);
if (visitor.hasNext(blockQuote)) { visitor.blockEnd(blockQuote);
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
}); });
} }
@ -316,7 +313,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
@NonNull String code, @NonNull String code,
@NonNull Node node) { @NonNull Node node) {
visitor.ensureNewLine(); visitor.blockStart(node);
final int length = visitor.length(); final int length = visitor.length();
@ -333,10 +330,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
visitor.setSpansForNodeOptional(node, length); visitor.setSpansForNodeOptional(node, length);
if (visitor.hasNext(node)) { visitor.blockEnd(node);
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
private static void bulletList(@NonNull MarkwonVisitor.Builder builder) { private static void bulletList(@NonNull MarkwonVisitor.Builder builder) {
@ -402,7 +396,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) {
visitor.ensureNewLine(); visitor.blockStart(thematicBreak);
final int length = visitor.length(); final int length = visitor.length();
@ -411,10 +405,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
visitor.setSpansForNodeOptional(thematicBreak, length); visitor.setSpansForNodeOptional(thematicBreak, length);
if (visitor.hasNext(thematicBreak)) { visitor.blockEnd(thematicBreak);
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
}); });
} }
@ -424,7 +415,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
visitor.ensureNewLine(); visitor.blockStart(heading);
final int length = visitor.length(); final int length = visitor.length();
visitor.visitChildren(heading); visitor.visitChildren(heading);
@ -433,10 +424,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
visitor.setSpansForNodeOptional(heading, length); visitor.setSpansForNodeOptional(heading, length);
if (visitor.hasNext(heading)) { visitor.blockEnd(heading);
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
}); });
} }
@ -467,7 +455,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
final boolean inTightList = isInTightList(paragraph); final boolean inTightList = isInTightList(paragraph);
if (!inTightList) { if (!inTightList) {
visitor.ensureNewLine(); visitor.blockStart(paragraph);
} }
final int length = visitor.length(); final int length = visitor.length();
@ -478,9 +466,8 @@ public class CorePlugin extends AbstractMarkwonPlugin {
// @since 1.1.1 apply paragraph span // @since 1.1.1 apply paragraph span
visitor.setSpansForNodeOptional(paragraph, length); visitor.setSpansForNodeOptional(paragraph, length);
if (!inTightList && visitor.hasNext(paragraph)) { if (!inTightList) {
visitor.ensureNewLine(); visitor.blockEnd(paragraph);
visitor.forceNewLine();
} }
} }
}); });

View File

@ -17,19 +17,16 @@ public class SimpleBlockNodeVisitor implements MarkwonVisitor.NodeVisitor<Node>
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
visitor.blockStart(node);
// @since 3.0.1 we keep track of start in order to apply spans (optionally) // @since 3.0.1 we keep track of start in order to apply spans (optionally)
final int length = visitor.length(); final int length = visitor.length();
visitor.ensureNewLine();
visitor.visitChildren(node); visitor.visitChildren(node);
// @since 3.0.1 we apply optional spans // @since 3.0.1 we apply optional spans
visitor.setSpansForNodeOptional(node, length); visitor.setSpansForNodeOptional(node, length);
if (visitor.hasNext(node)) { visitor.blockEnd(node);
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
} }

View File

@ -223,6 +223,7 @@ public class AsyncDrawable extends Drawable {
} }
this.result = result; this.result = result;
// this.result.setCallback(callback);
initBounds(); initBounds();
} }
@ -250,6 +251,10 @@ public class AsyncDrawable extends Drawable {
if (canvasWidth == 0) { if (canvasWidth == 0) {
// we still have no bounds - wait for them // we still have no bounds - wait for them
waitingForDimensions = true; waitingForDimensions = true;
// we cannot have empty bounds - otherwise in case if text contains
// a single AsyncDrawableSpan, it won't be displayed
setBounds(noDimensionsBounds(result));
return; return;
} }
@ -268,6 +273,24 @@ public class AsyncDrawable extends Drawable {
invalidateSelf(); invalidateSelf();
} }
/**
* @since 4.3.0
*/
@NonNull
private static Rect noDimensionsBounds(@Nullable Drawable result) {
if (result != null) {
final Rect bounds = result.getBounds();
if (!bounds.isEmpty()) {
return bounds;
}
final Rect intrinsicBounds = DrawableUtils.intrinsicBounds(result);
if (!intrinsicBounds.isEmpty()) {
return intrinsicBounds;
}
}
return new Rect(0, 0, 1, 1);
}
/** /**
* @since 1.0.1 * @since 1.0.1
*/ */

View File

@ -12,7 +12,8 @@ public class AbstractMarkwonVisitorImpl extends MarkwonVisitorImpl {
@NonNull MarkwonConfiguration configuration, @NonNull MarkwonConfiguration configuration,
@NonNull RenderProps renderProps, @NonNull RenderProps renderProps,
@NonNull SpannableBuilder spannableBuilder, @NonNull SpannableBuilder spannableBuilder,
@NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) { @NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes,
super(configuration, renderProps, spannableBuilder, nodes); @NonNull BlockHandler blockHandler) {
super(configuration, renderProps, spannableBuilder, nodes, blockHandler);
} }
} }

View File

@ -0,0 +1,79 @@
package io.noties.markwon;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import androidx.annotation.NonNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class LinkResolverDefTest {
@Test
public void no_scheme_https() {
// when supplied url doesn't have scheme fallback to `https`
// must be => `https://www.markw.on
final String link = "www.markw.on";
final Uri uri = resolve(link);
final String scheme = uri.getScheme();
assertNotNull(uri.toString(), scheme);
assertEquals(uri.toString(), "https", scheme);
}
@Test
public void scheme_present() {
// when scheme is present, it won't be touched
final String link = "whatnot://hey/ho";
final Uri uri = resolve(link);
final String scheme = uri.getScheme();
assertEquals(uri.toString(), "whatnot", scheme);
assertEquals(Uri.parse(link), uri);
}
// we could call `parseLink` directly, but this doesn't mean LinkResolverDef uses it
@NonNull
private Uri resolve(@NonNull String link) {
final View view = mock(View.class);
final Context context = mock(Context.class);
when(view.getContext()).thenReturn(context);
final LinkResolverDef def = new LinkResolverDef();
def.resolve(view, link);
final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(context, times(1))
.startActivity(captor.capture());
final Intent intent = captor.getValue();
assertNotNull(intent);
final Uri uri = intent.getData();
assertNotNull(uri);
return uri;
}
}

View File

@ -43,7 +43,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
renderProps, renderProps,
spannableBuilder, spannableBuilder,
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
impl.clear(); impl.clear();
@ -61,7 +62,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
mock(RenderProps.class), mock(RenderProps.class),
builder, builder,
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
// at the start - won't add anything // at the start - won't add anything
impl.ensureNewLine(); impl.ensureNewLine();
@ -92,7 +94,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
mock(RenderProps.class), mock(RenderProps.class),
builder, builder,
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
assertEquals(0, builder.length()); assertEquals(0, builder.length());
@ -144,7 +147,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
mock(RenderProps.class), mock(RenderProps.class),
mock(SpannableBuilder.class), mock(SpannableBuilder.class),
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
final BlockQuote node = mock(BlockQuote.class); final BlockQuote node = mock(BlockQuote.class);
final Node child = mock(Node.class); final Node child = mock(Node.class);
@ -163,7 +167,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
mock(RenderProps.class), mock(RenderProps.class),
mock(SpannableBuilder.class), mock(SpannableBuilder.class),
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
final Node noNext = mock(Node.class); final Node noNext = mock(Node.class);
assertFalse(impl.hasNext(noNext)); assertFalse(impl.hasNext(noNext));
@ -195,7 +200,8 @@ public class MarkwonVisitorImplTest {
mock(MarkwonConfiguration.class), mock(MarkwonConfiguration.class),
mock(RenderProps.class), mock(RenderProps.class),
builder, builder,
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
for (int i = 0; i < 13; i++) { for (int i = 0; i < 13; i++) {
builder.setLength(i); builder.setLength(i);
@ -221,7 +227,8 @@ public class MarkwonVisitorImplTest {
configuration, configuration,
mock(RenderProps.class), mock(RenderProps.class),
mock(SpannableBuilder.class), mock(SpannableBuilder.class),
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
impl.setSpansForNode(Node.class, 0); impl.setSpansForNode(Node.class, 0);
@ -252,7 +259,8 @@ public class MarkwonVisitorImplTest {
configuration, configuration,
mock(RenderProps.class), mock(RenderProps.class),
builder, builder,
Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap()); Collections.<Class<? extends Node>, MarkwonVisitor.NodeVisitor<? extends Node>>emptyMap(),
mock(MarkwonVisitor.BlockHandler.class));
// append something // append something
builder.append("no-spans-test"); builder.append("no-spans-test");

View File

@ -107,6 +107,12 @@ public class CorePluginTest {
return this; return this;
} }
@NonNull
@Override
public MarkwonVisitor.Builder blockHandler(@NonNull MarkwonVisitor.BlockHandler blockHandler) {
throw new RuntimeException();
}
@NonNull @NonNull
@Override @Override
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) { public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) {

View File

@ -91,7 +91,8 @@ public class SyntaxHighlightTest {
configuration, configuration,
mock(RenderProps.class), mock(RenderProps.class),
new SpannableBuilder(), new SpannableBuilder(),
visitorMap); visitorMap,
mock(MarkwonVisitor.BlockHandler.class));
final SpannableBuilder builder = visitor.builder(); final SpannableBuilder builder = visitor.builder();

View File

@ -16,6 +16,7 @@ android {
dependencies { dependencies {
api project(':markwon-core') api project(':markwon-core')
api project(':markwon-inline-parser')
api deps['jlatexmath-android'] api deps['jlatexmath-android']

View File

@ -0,0 +1,62 @@
package io.noties.markwon.ext.latex;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import org.scilab.forge.jlatexmath.TeXIcon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawableSpan;
import ru.noties.jlatexmath.JLatexMathDrawable;
import ru.noties.jlatexmath.awt.Color;
/**
* @since 4.3.0
*/
public class JLatexAsyncDrawableSpan extends AsyncDrawableSpan {
private final JLatextAsyncDrawable drawable;
private final int color;
private boolean appliedTextColor;
public JLatexAsyncDrawableSpan(
@NonNull MarkwonTheme theme,
@NonNull JLatextAsyncDrawable drawable,
@ColorInt int color) {
super(theme, drawable, ALIGN_CENTER, false);
this.drawable = drawable;
this.color = color;
// if color is not 0 -> then no need to apply text color
this.appliedTextColor = color != 0;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
if (!appliedTextColor && drawable.hasResult()) {
// it is important to check for type (in case of an error, or custom placeholder or whatever
// this result can be of other type)
final Drawable drawableResult = drawable.getResult();
if (drawableResult instanceof JLatexMathDrawable) {
final JLatexMathDrawable result = (JLatexMathDrawable) drawableResult;
final TeXIcon icon = result.icon();
icon.setForeground(new Color(paint.getColor()));
appliedTextColor = true;
}
}
super.draw(canvas, text, start, end, x, top, y, bottom, paint);
}
@NonNull
public JLatextAsyncDrawable drawable() {
return drawable;
}
@ColorInt
public int color() {
return color;
}
}

View File

@ -0,0 +1,50 @@
package io.noties.markwon.ext.latex;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.ImageSizeResolver;
// we must make drawable fit canvas (if specified), but do not keep the ratio whilst scaling up
// @since 4.0.0
class JLatexBlockImageSizeResolver extends ImageSizeResolver {
private final boolean fitCanvas;
JLatexBlockImageSizeResolver(boolean fitCanvas) {
this.fitCanvas = fitCanvas;
}
@NonNull
@Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
final Rect imageBounds = drawable.getResult().getBounds();
final int canvasWidth = drawable.getLastKnownCanvasWidth();
if (fitCanvas) {
// we modify bounds only if `fitCanvas` is true
final int w = imageBounds.width();
if (w < canvasWidth) {
// increase width and center formula (keep height as-is)
return new Rect(0, 0, canvasWidth, imageBounds.height());
}
// @since 4.0.2 we additionally scale down the resulting formula (keeping the ratio)
// the thing is - JLatexMathDrawable will do it anyway, but it will modify its own
// bounds (which AsyncDrawable won't catch), thus leading to an empty space after the formula
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;
}
}

View File

@ -0,0 +1,61 @@
package io.noties.markwon.ext.latex;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawable;
/**
* @since 4.3.0
*/
class JLatexInlineAsyncDrawableSpan extends JLatexAsyncDrawableSpan {
private final AsyncDrawable drawable;
JLatexInlineAsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull JLatextAsyncDrawable drawable, @ColorInt int color) {
super(theme, drawable, color);
this.drawable = drawable;
}
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
// if we have no async drawable result - we will just render text
final int size;
if (drawable.hasResult()) {
final Rect rect = drawable.getBounds();
if (fm != null) {
final int half = rect.bottom / 2;
fm.ascent = -half;
fm.descent = half;
fm.top = fm.ascent;
fm.bottom = 0;
}
size = rect.right;
} else {
// NB, no specific text handling (no new lines, etc)
size = (int) (paint.measureText(text, start, end) + .5F);
}
return size;
}
}

View File

@ -1,5 +1,8 @@
package io.noties.markwon.ext.latex; package io.noties.markwon.ext.latex;
import androidx.annotation.NonNull;
import org.commonmark.internal.util.Parsing;
import org.commonmark.node.Block; import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory; import org.commonmark.parser.block.AbstractBlockParserFactory;
@ -8,13 +11,24 @@ import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser; import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState; import org.commonmark.parser.block.ParserState;
public class JLatexMathBlockParser extends AbstractBlockParser { /**
* @since 4.3.0 (although there was a class with the same name,
* which is renamed now to {@link JLatexMathBlockParserLegacy})
*/
class JLatexMathBlockParser extends AbstractBlockParser {
private static final char DOLLAR = '$';
private static final char SPACE = ' ';
private final JLatexMathBlock block = new JLatexMathBlock(); private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder(); private final StringBuilder builder = new StringBuilder();
private boolean isClosed; private final int signs;
JLatexMathBlockParser(int signs) {
this.signs = signs;
}
@Override @Override
public Block getBlock() { public Block getBlock() {
@ -23,31 +37,28 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
@Override @Override
public BlockContinue tryContinue(ParserState parserState) { public BlockContinue tryContinue(ParserState parserState) {
final int nextNonSpaceIndex = parserState.getNextNonSpaceIndex();
final CharSequence line = parserState.getLine();
final int length = line.length();
if (isClosed) { // check for closing
if (parserState.getIndent() < Parsing.CODE_BLOCK_INDENT) {
if (consume(DOLLAR, line, nextNonSpaceIndex, length) == signs) {
// okay, we have our number of signs
// let's consume spaces until the end
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) == length) {
return BlockContinue.finished(); return BlockContinue.finished();
} }
}
}
return BlockContinue.atIndex(parserState.getIndex()); return BlockContinue.atIndex(parserState.getIndex());
} }
@Override @Override
public void addLine(CharSequence line) { public void addLine(CharSequence line) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(line); builder.append(line);
builder.append('\n');
final int length = builder.length();
if (length > 1) {
isClosed = '$' == builder.charAt(length - 1)
&& '$' == builder.charAt(length - 2);
if (isClosed) {
builder.replace(length - 2, length, "");
}
}
} }
@Override @Override
@ -60,20 +71,49 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
@Override @Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
final CharSequence line = state.getLine(); // let's define the spec:
final int length = line != null // * 0-3 spaces before are allowed (Parsing.CODE_BLOCK_INDENT = 4)
? line.length() // * 2+ subsequent `$` signs
: 0; // * any optional amount of spaces
// * new line
// * block is closed when the same amount of opening signs is met
if (length > 1) { final int indent = state.getIndent();
if ('$' == line.charAt(0)
&& '$' == line.charAt(1)) {
return BlockStart.of(new JLatexMathBlockParser())
.atIndex(state.getIndex() + 2);
}
}
// check if it's an indented code block
if (indent >= Parsing.CODE_BLOCK_INDENT) {
return BlockStart.none(); return BlockStart.none();
} }
final int nextNonSpaceIndex = state.getNextNonSpaceIndex();
final CharSequence line = state.getLine();
final int length = line.length();
final int signs = consume(DOLLAR, line, nextNonSpaceIndex, length);
// 2 is minimum
if (signs < 2) {
return BlockStart.none();
}
// consume spaces until the end of the line, if any other content is found -> NONE
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) != length) {
return BlockStart.none();
}
return BlockStart.of(new JLatexMathBlockParser(signs))
.atIndex(length + 1);
}
}
@SuppressWarnings("SameParameterValue")
private static int consume(char c, @NonNull CharSequence line, int start, int end) {
for (int i = start; i < end; i++) {
if (c != line.charAt(i)) {
return i - start;
}
}
// all consumed
return end - start;
} }
} }

View File

@ -0,0 +1,82 @@
package io.noties.markwon.ext.latex;
import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
/**
* @since 4.3.0 (although it is just renamed parser from previous versions)
*/
class JLatexMathBlockParserLegacy extends AbstractBlockParser {
private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder();
private boolean isClosed;
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState parserState) {
if (isClosed) {
return BlockContinue.finished();
}
return BlockContinue.atIndex(parserState.getIndex());
}
@Override
public void addLine(CharSequence line) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(line);
final int length = builder.length();
if (length > 1) {
isClosed = '$' == builder.charAt(length - 1)
&& '$' == builder.charAt(length - 2);
if (isClosed) {
builder.replace(length - 2, length, "");
}
}
}
@Override
public void closeBlock() {
block.latex(builder.toString());
}
public static class Factory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
final CharSequence line = state.getLine();
final int length = line != null
? line.length()
: 0;
if (length > 1) {
if ('$' == line.charAt(0)
&& '$' == line.charAt(1)) {
return BlockStart.of(new JLatexMathBlockParserLegacy())
.atIndex(state.getIndex() + 2);
}
}
return BlockStart.none();
}
}
}

View File

@ -0,0 +1,36 @@
package io.noties.markwon.ext.latex;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import java.util.regex.Pattern;
import io.noties.markwon.inlineparser.InlineProcessor;
/**
* @since 4.3.0
*/
class JLatexMathInlineProcessor extends InlineProcessor {
private static final Pattern RE = Pattern.compile("(\\${2})([\\s\\S]+?)\\1");
@Override
public char specialCharacter() {
return '$';
}
@Nullable
@Override
protected Node parse() {
final String latex = match(RE);
if (latex == null) {
return null;
}
final JLatexMathNode node = new JLatexMathNode();
node.latex(latex.substring(2, latex.length() - 2));
return node;
}
}

View File

@ -0,0 +1,19 @@
package io.noties.markwon.ext.latex;
import org.commonmark.node.CustomNode;
/**
* @since 4.3.0
*/
public class JLatexMathNode extends CustomNode {
private String latex;
public String latex() {
return latex;
}
public void latex(String latex) {
this.latex = latex;
}
}

View File

@ -29,7 +29,9 @@ import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.AsyncDrawableLoader; import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.AsyncDrawableScheduler; import io.noties.markwon.image.AsyncDrawableScheduler;
import io.noties.markwon.image.AsyncDrawableSpan; import io.noties.markwon.image.AsyncDrawableSpan;
import io.noties.markwon.image.DrawableUtils;
import io.noties.markwon.image.ImageSizeResolver; import io.noties.markwon.image.ImageSizeResolver;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import ru.noties.jlatexmath.JLatexMathDrawable; import ru.noties.jlatexmath.JLatexMathDrawable;
/** /**
@ -38,11 +40,18 @@ import ru.noties.jlatexmath.JLatexMathDrawable;
public class JLatexMathPlugin extends AbstractMarkwonPlugin { public class JLatexMathPlugin extends AbstractMarkwonPlugin {
/** /**
* @since 4.0.0 * @since 4.3.0
*/ */
public interface BackgroundProvider { public interface ErrorHandler {
@NonNull
Drawable provide(); /**
* @param latex that caused the error
* @param error occurred
* @return (optional) error drawable that will be used instead (if drawable will have bounds
* it will be used, if not intrinsic bounds will be set)
*/
@Nullable
Drawable handleError(@NonNull String latex, @NonNull Throwable error);
} }
public interface BuilderConfigure { public interface BuilderConfigure {
@ -54,52 +63,74 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
return new JLatexMathPlugin(builder(textSize).build()); return new JLatexMathPlugin(builder(textSize).build());
} }
/**
* @since 4.3.0
*/
@NonNull
public static JLatexMathPlugin create(@Px float inlineTextSize, @Px float blockTextSize) {
return new JLatexMathPlugin(builder(inlineTextSize, blockTextSize).build());
}
@NonNull @NonNull
public static JLatexMathPlugin create(@NonNull Config config) { public static JLatexMathPlugin create(@NonNull Config config) {
return new JLatexMathPlugin(config); return new JLatexMathPlugin(config);
} }
@NonNull @NonNull
public static JLatexMathPlugin create(float textSize, @NonNull BuilderConfigure builderConfigure) { public static JLatexMathPlugin create(@Px float textSize, @NonNull BuilderConfigure builderConfigure) {
final Builder builder = new Builder(textSize); final Builder builder = builder(textSize);
builderConfigure.configureBuilder(builder);
return new JLatexMathPlugin(builder.build());
}
/**
* @since 4.3.0
*/
@NonNull
public static JLatexMathPlugin create(
@Px float inlineTextSize,
@Px float blockTextSize,
@NonNull BuilderConfigure builderConfigure) {
final Builder builder = builder(inlineTextSize, blockTextSize);
builderConfigure.configureBuilder(builder); builderConfigure.configureBuilder(builder);
return new JLatexMathPlugin(builder.build()); return new JLatexMathPlugin(builder.build());
} }
@NonNull @NonNull
public static JLatexMathPlugin.Builder builder(float textSize) { public static JLatexMathPlugin.Builder builder(@Px float textSize) {
return new Builder(textSize); return new Builder(JLatexMathTheme.builder(textSize));
} }
public static class Config { /**
* @since 4.3.0
*/
@NonNull
public static JLatexMathPlugin.Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
return new Builder(JLatexMathTheme.builder(inlineTextSize, blockTextSize));
}
private final float textSize; @VisibleForTesting
static class Config {
// @since 4.0.0 // @since 4.3.0
private final BackgroundProvider backgroundProvider; final JLatexMathTheme theme;
@JLatexMathDrawable.Align // @since 4.3.0
private final int align; final boolean blocksEnabled;
final boolean blocksLegacy;
final boolean inlinesEnabled;
private final boolean fitCanvas; // @since 4.3.0
final ErrorHandler errorHandler;
// @since 4.0.0 final ExecutorService executorService;
private final int paddingHorizontal;
// @since 4.0.0
private final int paddingVertical;
// @since 4.0.0
private final ExecutorService executorService;
Config(@NonNull Builder builder) { Config(@NonNull Builder builder) {
this.textSize = builder.textSize; this.theme = builder.theme.build();
this.backgroundProvider = builder.backgroundProvider; this.blocksEnabled = builder.blocksEnabled;
this.align = builder.align; this.blocksLegacy = builder.blocksLegacy;
this.fitCanvas = builder.fitCanvas; this.inlinesEnabled = builder.inlinesEnabled;
this.paddingHorizontal = builder.paddingHorizontal; this.errorHandler = builder.errorHandler;
this.paddingVertical = builder.paddingVertical;
// @since 4.0.0 // @since 4.0.0
ExecutorService executorService = builder.executorService; ExecutorService executorService = builder.executorService;
if (executorService == null) { if (executorService == null) {
@ -109,26 +140,59 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
} }
} }
@VisibleForTesting
final Config config;
private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader; private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader;
private final JLatexImageSizeResolver jLatexImageSizeResolver; private final JLatexBlockImageSizeResolver jLatexBlockImageSizeResolver;
private final ImageSizeResolver inlineImageSizeResolver;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
JLatexMathPlugin(@NonNull Config config) { JLatexMathPlugin(@NonNull Config config) {
this.config = config;
this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config); this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config);
this.jLatexImageSizeResolver = new JLatexImageSizeResolver(config.fitCanvas); this.jLatexBlockImageSizeResolver = new JLatexBlockImageSizeResolver(config.theme.blockFitCanvas());
this.inlineImageSizeResolver = new InlineImageSizeResolver();
}
@Override
public void configure(@NonNull Registry registry) {
if (config.inlinesEnabled) {
registry.require(MarkwonInlineParserPlugin.class)
.factoryBuilder()
.addInlineProcessor(new JLatexMathInlineProcessor());
}
} }
@Override @Override
public void configureParser(@NonNull Parser.Builder builder) { public void configureParser(@NonNull Parser.Builder builder) {
// @since 4.3.0
if (config.blocksEnabled) {
if (config.blocksLegacy) {
builder.customBlockParserFactory(new JLatexMathBlockParserLegacy.Factory());
} else {
builder.customBlockParserFactory(new JLatexMathBlockParser.Factory()); builder.customBlockParserFactory(new JLatexMathBlockParser.Factory());
} }
}
}
@Override @Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
addBlockVisitor(builder);
addInlineVisitor(builder);
}
private void addBlockVisitor(@NonNull MarkwonVisitor.Builder builder) {
if (!config.blocksEnabled) {
return;
}
builder.on(JLatexMathBlock.class, new MarkwonVisitor.NodeVisitor<JLatexMathBlock>() { builder.on(JLatexMathBlock.class, new MarkwonVisitor.NodeVisitor<JLatexMathBlock>() {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) {
visitor.blockStart(jLatexMathBlock);
final String latex = jLatexMathBlock.latex(); final String latex = jLatexMathBlock.latex();
final int length = visitor.length(); final int length = visitor.length();
@ -140,15 +204,54 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
final MarkwonConfiguration configuration = visitor.configuration(); final MarkwonConfiguration configuration = visitor.configuration();
final AsyncDrawableSpan span = new AsyncDrawableSpan( final AsyncDrawableSpan span = new JLatexAsyncDrawableSpan(
configuration.theme(), configuration.theme(),
new AsyncDrawable( new JLatextAsyncDrawable(
latex, latex,
jLatextAsyncDrawableLoader, jLatextAsyncDrawableLoader,
jLatexImageSizeResolver, jLatexBlockImageSizeResolver,
null), null,
AsyncDrawableSpan.ALIGN_BOTTOM, true),
false); config.theme.blockTextColor()
);
visitor.setSpans(length, span);
visitor.blockEnd(jLatexMathBlock);
}
});
}
private void addInlineVisitor(@NonNull MarkwonVisitor.Builder builder) {
if (!config.inlinesEnabled) {
return;
}
builder.on(JLatexMathNode.class, new MarkwonVisitor.NodeVisitor<JLatexMathNode>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathNode jLatexMathNode) {
final String latex = jLatexMathNode.latex();
final int length = visitor.length();
// @since 4.0.2 we cannot append _raw_ latex as a placeholder-text,
// because Android will draw formula for each line of text, thus
// leading to formula duplicated (drawn on each line of text)
visitor.builder().append(prepareLatexTextPlaceholder(latex));
final MarkwonConfiguration configuration = visitor.configuration();
final AsyncDrawableSpan span = new JLatexInlineAsyncDrawableSpan(
configuration.theme(),
new JLatextAsyncDrawable(
latex,
jLatextAsyncDrawableLoader,
inlineImageSizeResolver,
null,
false),
config.theme.inlineTextColor()
);
visitor.setSpans(length, span); visitor.setSpans(length, span);
} }
@ -172,69 +275,72 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
return latex.replace('\n', ' ').trim(); return latex.replace('\n', ' ').trim();
} }
@SuppressWarnings({"unused", "UnusedReturnValue"})
public static class Builder { public static class Builder {
private final float textSize; // @since 4.3.0
private final JLatexMathTheme.Builder theme;
// @since 4.0.0 // @since 4.3.0
private BackgroundProvider backgroundProvider; private boolean blocksEnabled = true;
private boolean blocksLegacy;
private boolean inlinesEnabled;
@JLatexMathDrawable.Align // @since 4.3.0
private int align = JLatexMathDrawable.ALIGN_CENTER; private ErrorHandler errorHandler;
private boolean fitCanvas = true;
// @since 4.0.0
private int paddingHorizontal;
// @since 4.0.0
private int paddingVertical;
// @since 4.0.0 // @since 4.0.0
private ExecutorService executorService; private ExecutorService executorService;
Builder(float textSize) { Builder(@NonNull JLatexMathTheme.Builder builder) {
this.textSize = textSize; this.theme = builder;
} }
@NonNull @NonNull
public Builder backgroundProvider(@NonNull BackgroundProvider backgroundProvider) { public JLatexMathTheme.Builder theme() {
this.backgroundProvider = backgroundProvider; return theme;
}
/**
* @since 4.3.0
*/
@NonNull
public Builder blocksEnabled(boolean blocksEnabled) {
this.blocksEnabled = blocksEnabled;
return this;
}
/**
* @param blocksLegacy indicates if blocks should be handled in legacy mode ({@code pre 4.3.0})
* @since 4.3.0
*/
@NonNull
public Builder blocksLegacy(boolean blocksLegacy) {
this.blocksLegacy = blocksLegacy;
return this;
}
/**
* @param inlinesEnabled indicates if inline parsing should be enabled.
* NB, this requires `MarkwonInlineParserPlugin` to be used when creating `MarkwonInstance`
* @since 4.3.0
*/
@NonNull
public Builder inlinesEnabled(boolean inlinesEnabled) {
this.inlinesEnabled = inlinesEnabled;
return this; return this;
} }
@NonNull @NonNull
public Builder align(@JLatexMathDrawable.Align int align) { public Builder errorHandler(@Nullable ErrorHandler errorHandler) {
this.align = align; this.errorHandler = errorHandler;
return this;
}
@NonNull
public Builder fitCanvas(boolean fitCanvas) {
this.fitCanvas = fitCanvas;
return this;
}
@NonNull
public Builder padding(@Px int padding) {
this.paddingHorizontal = padding;
this.paddingVertical = padding;
return this;
}
/**
* @since 4.0.0
*/
@NonNull
public Builder builder(@Px int paddingHorizontal, @Px int paddingVertical) {
this.paddingHorizontal = paddingHorizontal;
this.paddingVertical = paddingVertical;
return this; return this;
} }
/** /**
* @since 4.0.0 * @since 4.0.0
*/ */
@SuppressWarnings("WeakerAccess")
@NonNull @NonNull
public Builder executorService(@NonNull ExecutorService executorService) { public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService; this.executorService = executorService;
@ -248,7 +354,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
} }
// @since 4.0.0 // @since 4.0.0
private static class JLatextAsyncDrawableLoader extends AsyncDrawableLoader { static class JLatextAsyncDrawableLoader extends AsyncDrawableLoader {
private final Config config; private final Config config;
private final Handler handler = new Handler(Looper.getMainLooper()); private final Handler handler = new Handler(Looper.getMainLooper());
@ -278,46 +384,41 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
try { try {
execute(); execute();
} catch (Throwable t) { } catch (Throwable t) {
// @since 4.3.0 add error handling
final ErrorHandler errorHandler = config.errorHandler;
if (errorHandler == null) {
// as before
Log.e( Log.e(
"JLatexMathPlugin", "JLatexMathPlugin",
"Error displaying latex: `" + drawable.getDestination() + "`", "Error displaying latex: `" + drawable.getDestination() + "`",
t); t);
} else {
// just call `getDestination` without casts and checks
final Drawable errorDrawable = errorHandler.handleError(
drawable.getDestination(),
t
);
if (errorDrawable != null) {
DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable);
setResult(drawable, errorDrawable);
}
}
} }
} }
private void execute() { private void execute() {
// @since 4.0.1 (background provider can be null) final JLatexMathDrawable jLatexMathDrawable;
final BackgroundProvider backgroundProvider = config.backgroundProvider;
// create JLatexMathDrawable final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable;
//noinspection ConstantConditions
final JLatexMathDrawable jLatexMathDrawable =
JLatexMathDrawable.builder(drawable.getDestination())
.textSize(config.textSize)
.background(backgroundProvider != null ? backgroundProvider.provide() : null)
.align(config.align)
.fitCanvas(config.fitCanvas)
.padding(
config.paddingHorizontal,
config.paddingVertical,
config.paddingHorizontal,
config.paddingVertical)
.build();
// we must post to handler, but also have a way to identify the drawable if (jLatextAsyncDrawable.isBlock()) {
// for which we are posting (in case of cancellation) jLatexMathDrawable = createBlockDrawable(jLatextAsyncDrawable);
handler.postAtTime(new Runnable() { } else {
@Override jLatexMathDrawable = createInlineDrawable(jLatextAsyncDrawable);
public void run() {
// remove entry from cache (it will be present if task is not cancelled)
if (cache.remove(drawable) != null
&& drawable.isAttached()) {
drawable.setResult(jLatexMathDrawable);
} }
} setResult(drawable, jLatexMathDrawable);
}, drawable, SystemClock.uptimeMillis());
} }
})); }));
} }
@ -342,47 +443,94 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
public Drawable placeholder(@NonNull AsyncDrawable drawable) { public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return null; return null;
} }
// @since 4.3.0
@NonNull
private JLatexMathDrawable createBlockDrawable(@NonNull JLatextAsyncDrawable drawable) {
final String latex = drawable.getDestination();
final JLatexMathTheme theme = config.theme;
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.blockBackgroundProvider();
final JLatexMathTheme.Padding padding = theme.blockPadding();
final int color = theme.blockTextColor();
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
.textSize(theme.blockTextSize())
.align(theme.blockHorizontalAlignment())
.fitCanvas(theme.blockFitCanvas());
if (backgroundProvider != null) {
builder.background(backgroundProvider.provide());
} }
// we must make drawable fit canvas (if specified), but do not keep the ratio whilst scaling up if (padding != null) {
// @since 4.0.0 builder.padding(padding.left, padding.top, padding.right, padding.bottom);
private static class JLatexImageSizeResolver extends ImageSizeResolver {
private final boolean fitCanvas;
JLatexImageSizeResolver(boolean fitCanvas) {
this.fitCanvas = fitCanvas;
} }
if (color != 0) {
builder.color(color);
}
return builder.build();
}
// @since 4.3.0
@NonNull
private JLatexMathDrawable createInlineDrawable(@NonNull JLatextAsyncDrawable drawable) {
final String latex = drawable.getDestination();
final JLatexMathTheme theme = config.theme;
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.inlineBackgroundProvider();
final JLatexMathTheme.Padding padding = theme.inlinePadding();
final int color = theme.inlineTextColor();
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
.textSize(theme.inlineTextSize())
.fitCanvas(false);
if (backgroundProvider != null) {
builder.background(backgroundProvider.provide());
}
if (padding != null) {
builder.padding(padding.left, padding.top, padding.right, padding.bottom);
}
if (color != 0) {
builder.color(color);
}
return builder.build();
}
// @since 4.3.0
private void setResult(@NonNull final AsyncDrawable drawable, @NonNull final Drawable result) {
// we must post to handler, but also have a way to identify the drawable
// for which we are posting (in case of cancellation)
handler.postAtTime(new Runnable() {
@Override
public void run() {
// remove entry from cache (it will be present if task is not cancelled)
if (cache.remove(drawable) != null
&& drawable.isAttached()) {
drawable.setResult(result);
}
}
}, drawable, SystemClock.uptimeMillis());
}
}
private static class InlineImageSizeResolver extends ImageSizeResolver {
@NonNull @NonNull
@Override @Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) { public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
return drawable.getResult().getBounds();
final Rect imageBounds = drawable.getResult().getBounds();
final int canvasWidth = drawable.getLastKnownCanvasWidth();
if (fitCanvas) {
// we modify bounds only if `fitCanvas` is true
final int w = imageBounds.width();
if (w < canvasWidth) {
// increase width and center formula (keep height as-is)
return new Rect(0, 0, canvasWidth, imageBounds.height());
}
// @since 4.0.2 we additionally scale down the resulting formula (keeping the ratio)
// the thing is - JLatexMathDrawable will do it anyway, but it will modify its own
// bounds (which AsyncDrawable won't catch), thus leading to an empty space after the formula
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;
} }
} }
} }

View File

@ -0,0 +1,351 @@
package io.noties.markwon.ext.latex;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import ru.noties.jlatexmath.JLatexMathDrawable;
/**
* @since 4.3.0
*/
public abstract class JLatexMathTheme {
@NonNull
public static JLatexMathTheme create(@Px float textSize) {
return builder(textSize).build();
}
@NonNull
public static JLatexMathTheme create(@Px float inlineTextSize, @Px float blockTextSize) {
return builder(inlineTextSize, blockTextSize).build();
}
@NonNull
public static JLatexMathTheme.Builder builder(@Px float textSize) {
return new JLatexMathTheme.Builder(textSize, 0F, 0F);
}
@NonNull
public static JLatexMathTheme.Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
return new Builder(0F, inlineTextSize, blockTextSize);
}
/**
* Moved from {@link JLatexMathPlugin} in {@code 4.3.0} version
*
* @since 4.0.0
*/
public interface BackgroundProvider {
@NonNull
Drawable provide();
}
/**
* Special immutable class to hold padding information
*/
@SuppressWarnings("WeakerAccess")
public static class Padding {
public final int left;
public final int top;
public final int right;
public final int bottom;
public Padding(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
@NonNull
@Override
public String toString() {
return "Padding{" +
"left=" + left +
", top=" + top +
", right=" + right +
", bottom=" + bottom +
'}';
}
@NonNull
public static Padding all(int value) {
return new Padding(value, value, value, value);
}
@NonNull
public static Padding symmetric(int vertical, int horizontal) {
return new Padding(horizontal, vertical, horizontal, vertical);
}
}
/**
* @return text size in pixels for <strong>inline LaTeX</strong>
* @see #blockTextSize()
*/
@Px
public abstract float inlineTextSize();
/**
* @return text size in pixels for <strong>block LaTeX</strong>
* @see #inlineTextSize()
*/
@Px
public abstract float blockTextSize();
@Nullable
public abstract BackgroundProvider inlineBackgroundProvider();
@Nullable
public abstract BackgroundProvider blockBackgroundProvider();
/**
* @return boolean if <strong>block LaTeX</strong> must fit the width of canvas
*/
public abstract boolean blockFitCanvas();
/**
* @return horizontal alignment of <strong>block LaTeX</strong> if {@link #blockFitCanvas()}
* is enabled (thus space for alignment is available)
*/
@JLatexMathDrawable.Align
public abstract int blockHorizontalAlignment();
@Nullable
public abstract Padding inlinePadding();
@Nullable
public abstract Padding blockPadding();
@ColorInt
public abstract int inlineTextColor();
@ColorInt
public abstract int blockTextColor();
@SuppressWarnings({"unused", "UnusedReturnValue"})
public static class Builder {
private final float textSize;
private final float inlineTextSize;
private final float blockTextSize;
private BackgroundProvider backgroundProvider;
private BackgroundProvider inlineBackgroundProvider;
private BackgroundProvider blockBackgroundProvider;
private boolean blockFitCanvas = true;
// horizontal alignment (when there is additional horizontal space)
private int blockHorizontalAlignment = JLatexMathDrawable.ALIGN_CENTER;
private Padding padding;
private Padding inlinePadding;
private Padding blockPadding;
private int textColor;
private int inlineTextColor;
private int blockTextColor;
Builder(float textSize, float inlineTextSize, float blockTextSize) {
this.textSize = textSize;
this.inlineTextSize = inlineTextSize;
this.blockTextSize = blockTextSize;
}
@NonNull
public Builder backgroundProvider(@Nullable BackgroundProvider backgroundProvider) {
this.backgroundProvider = backgroundProvider;
this.inlineBackgroundProvider = backgroundProvider;
this.blockBackgroundProvider = backgroundProvider;
return this;
}
@NonNull
public Builder inlineBackgroundProvider(@Nullable BackgroundProvider inlineBackgroundProvider) {
this.inlineBackgroundProvider = inlineBackgroundProvider;
return this;
}
@NonNull
public Builder blockBackgroundProvider(@Nullable BackgroundProvider blockBackgroundProvider) {
this.blockBackgroundProvider = blockBackgroundProvider;
return this;
}
@NonNull
public Builder blockFitCanvas(boolean blockFitCanvas) {
this.blockFitCanvas = blockFitCanvas;
return this;
}
@NonNull
public Builder blockHorizontalAlignment(@JLatexMathDrawable.Align int blockHorizontalAlignment) {
this.blockHorizontalAlignment = blockHorizontalAlignment;
return this;
}
@NonNull
public Builder padding(@Nullable Padding padding) {
this.padding = padding;
this.inlinePadding = padding;
this.blockPadding = padding;
return this;
}
@NonNull
public Builder inlinePadding(@Nullable Padding inlinePadding) {
this.inlinePadding = inlinePadding;
return this;
}
@NonNull
public Builder blockPadding(@Nullable Padding blockPadding) {
this.blockPadding = blockPadding;
return this;
}
@NonNull
public Builder textColor(@ColorInt int textColor) {
this.textColor = textColor;
return this;
}
@NonNull
public Builder inlineTextColor(@ColorInt int inlineTextColor) {
this.inlineTextColor = inlineTextColor;
return this;
}
@NonNull
public Builder blockTextColor(@ColorInt int blockTextColor) {
this.blockTextColor = blockTextColor;
return this;
}
@NonNull
public JLatexMathTheme build() {
return new Impl(this);
}
}
static class Impl extends JLatexMathTheme {
private final float textSize;
private final float inlineTextSize;
private final float blockTextSize;
private final BackgroundProvider backgroundProvider;
private final BackgroundProvider inlineBackgroundProvider;
private final BackgroundProvider blockBackgroundProvider;
private final boolean blockFitCanvas;
// horizontal alignment (when there is additional horizontal space)
private int blockHorizontalAlignment;
private final Padding padding;
private final Padding inlinePadding;
private final Padding blockPadding;
private final int textColor;
private final int inlineTextColor;
private final int blockTextColor;
Impl(@NonNull Builder builder) {
this.textSize = builder.textSize;
this.inlineTextSize = builder.inlineTextSize;
this.blockTextSize = builder.blockTextSize;
this.backgroundProvider = builder.backgroundProvider;
this.inlineBackgroundProvider = builder.inlineBackgroundProvider;
this.blockBackgroundProvider = builder.blockBackgroundProvider;
this.blockFitCanvas = builder.blockFitCanvas;
this.blockHorizontalAlignment = builder.blockHorizontalAlignment;
this.padding = builder.padding;
this.inlinePadding = builder.inlinePadding;
this.blockPadding = builder.blockPadding;
this.textColor = builder.textColor;
this.inlineTextColor = builder.inlineTextColor;
this.blockTextColor = builder.blockTextColor;
}
@Override
public float inlineTextSize() {
if (inlineTextSize > 0F) {
return inlineTextSize;
}
return textSize;
}
@Override
public float blockTextSize() {
if (blockTextSize > 0F) {
return blockTextSize;
}
return textSize;
}
@Nullable
@Override
public BackgroundProvider inlineBackgroundProvider() {
if (inlineBackgroundProvider != null) {
return inlineBackgroundProvider;
}
return backgroundProvider;
}
@Nullable
@Override
public BackgroundProvider blockBackgroundProvider() {
if (blockBackgroundProvider != null) {
return blockBackgroundProvider;
}
return backgroundProvider;
}
@Override
public boolean blockFitCanvas() {
return blockFitCanvas;
}
@Override
public int blockHorizontalAlignment() {
return blockHorizontalAlignment;
}
@Nullable
@Override
public Padding inlinePadding() {
if (inlinePadding != null) {
return inlinePadding;
}
return padding;
}
@Nullable
@Override
public Padding blockPadding() {
if (blockPadding != null) {
return blockPadding;
}
return padding;
}
@Override
public int inlineTextColor() {
if (inlineTextColor != 0) {
return inlineTextColor;
}
return textColor;
}
@Override
public int blockTextColor() {
if (blockTextColor != 0) {
return blockTextColor;
}
return textColor;
}
}
}

View File

@ -0,0 +1,32 @@
package io.noties.markwon.ext.latex;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.ImageSize;
import io.noties.markwon.image.ImageSizeResolver;
/**
* @since 4.3.0
*/
class JLatextAsyncDrawable extends AsyncDrawable {
private final boolean isBlock;
JLatextAsyncDrawable(
@NonNull String destination,
@NonNull AsyncDrawableLoader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize,
boolean isBlock
) {
super(destination, loader, imageSizeResolver, imageSize);
this.isBlock = isBlock;
}
public boolean isBlock() {
return isBlock;
}
}

View File

@ -0,0 +1,173 @@
package io.noties.markwon.ext.latex;
import androidx.annotation.NonNull;
import org.commonmark.internal.BlockContinueImpl;
import org.commonmark.internal.BlockStartImpl;
import org.commonmark.internal.util.Parsing;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.ParserState;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JLatexMathBlockParserTest {
private static final String[] NO_MATCH = {
" ",
" ",
" ",
"$ ",
" $ $",
"-$$",
" -$$",
"$$-",
" $$ -",
" $$ -",
"$$$ -"
};
private static final String[] MATCH = {
"$$",
" $$",
" $$",
" $$",
"$$ ",
" $$ ",
" $$ ",
" $$ ",
"$$$",
" $$$",
" $$$",
"$$$$",
" $$$$",
"$$$$$$$$$$$$$$$$$$$$$",
" $$$$$$$$$$$$$$$$$$$$$",
" $$$$$$$$$$$$$$$$$$$$$",
" $$$$$$$$$$$$$$$$$$$$$"
};
private JLatexMathBlockParser.Factory factory;
@Before
public void before() {
factory = new JLatexMathBlockParser.Factory();
}
@Test
public void factory_indentBlock() {
// when state indent is greater than block -> nono
final ParserState state = mock(ParserState.class);
when(state.getIndent()).thenReturn(Parsing.CODE_BLOCK_INDENT);
// hm, interesting, `BlockStart.none()` actually returns null
final BlockStart start = factory.tryStart(state, null);
assertNull(start);
}
@Test
public void factory_noMatch() {
for (String line : NO_MATCH) {
final ParserState state = createState(line);
assertNull(factory.tryStart(state, null));
}
}
@Test
public void factory_match() {
for (String line : MATCH) {
final ParserState state = createState(line);
final BlockStart start = factory.tryStart(state, null);
assertNotNull(start);
// hm...
final BlockStartImpl impl = (BlockStartImpl) start;
assertEquals(quote(line), line.length() + 1, impl.getNewIndex());
}
}
@Test
public void finish() {
for (String line : MATCH) {
final ParserState state = createState(line);
// we will have 2 checks here:
// * must pass for correct length
// * must fail for incorrect
final int count = countDollarSigns(line);
// pass
{
final JLatexMathBlockParser parser = new JLatexMathBlockParser(count);
final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state);
assertTrue(quote(line), impl.isFinalize());
}
// fail (in terms of closing, not failing test)
{
final JLatexMathBlockParser parser = new JLatexMathBlockParser(count + 1);
final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state);
assertFalse(quote(line), impl.isFinalize());
}
}
}
@Test
public void finish_noMatch() {
for (String line : NO_MATCH) {
final ParserState state = createState(line);
// doesn't matter
final int count = 2;
final JLatexMathBlockParser parser = new JLatexMathBlockParser(count);
final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state);
assertFalse(quote(line), impl.isFinalize());
}
}
@NonNull
private static ParserState createState(@NonNull String line) {
final ParserState state = mock(ParserState.class);
int i = 0;
for (int length = line.length(); i < length; i++) {
if (' ' != line.charAt(i)) {
// previous is the last space
break;
}
}
when(state.getIndent()).thenReturn(i);
when(state.getNextNonSpaceIndex()).thenReturn(i);
when(state.getLine()).thenReturn(line);
return state;
}
private static int countDollarSigns(@NonNull String line) {
int count = 0;
for (int i = 0, length = line.length(); i < length; i++) {
if ('$' == line.charAt(i)) count += 1;
}
return count;
}
@NonNull
private static String quote(@NonNull String s) {
return '\'' + s + '\'';
}
}

View File

@ -10,17 +10,24 @@ import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder; import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.inlineparser.InlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -110,4 +117,114 @@ public class JLatexMathPluginTest {
verify(visitor, times(1)).setSpans(eq(0), any()); verify(visitor, times(1)).setSpans(eq(0), any());
} }
@Test
public void legacy() {
// if render mode is legacy:
// - no inline plugin is required,
// - parser has legacy block parser factory
// - no inline node is registered (node)
final JLatexMathPlugin plugin = JLatexMathPlugin.create(1, new JLatexMathPlugin.BuilderConfigure() {
@Override
public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
builder.blocksLegacy(true);
builder.inlinesEnabled(false);
}
});
// registry
{
final MarkwonPlugin.Registry registry = mock(MarkwonPlugin.Registry.class);
plugin.configure(registry);
verify(registry, never()).require(any(Class.class));
}
// parser
{
final Parser.Builder builder = mock(Parser.Builder.class);
plugin.configureParser(builder);
final ArgumentCaptor<BlockParserFactory> captor =
ArgumentCaptor.forClass(BlockParserFactory.class);
verify(builder, times(1)).customBlockParserFactory(captor.capture());
final BlockParserFactory factory = captor.getValue();
assertTrue(factory.getClass().getName(), factory instanceof JLatexMathBlockParserLegacy.Factory);
}
// visitor
{
final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class);
plugin.configureVisitor(builder);
final ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(builder, times(1)).on(captor.capture(), any(MarkwonVisitor.NodeVisitor.class));
assertEquals(JLatexMathBlock.class, captor.getValue());
}
}
@Test
public void blocks_inlines_implicit() {
final JLatexMathPlugin plugin = JLatexMathPlugin.create(1);
final JLatexMathPlugin.Config config = plugin.config;
assertTrue("blocksEnabled", config.blocksEnabled);
assertFalse("blocksLegacy", config.blocksLegacy);
assertFalse("inlinesEnabled", config.inlinesEnabled);
}
@Test
public void blocks_inlines() {
final JLatexMathPlugin plugin = JLatexMathPlugin.create(12, new JLatexMathPlugin.BuilderConfigure() {
@Override
public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
builder.inlinesEnabled(true);
}
});
// registry
{
final MarkwonInlineParser.FactoryBuilder factoryBuilder = mock(MarkwonInlineParser.FactoryBuilder.class);
final MarkwonInlineParserPlugin inlineParserPlugin = mock(MarkwonInlineParserPlugin.class);
final MarkwonPlugin.Registry registry = mock(MarkwonPlugin.Registry.class);
when(inlineParserPlugin.factoryBuilder()).thenReturn(factoryBuilder);
when(registry.require(eq(MarkwonInlineParserPlugin.class))).thenReturn(inlineParserPlugin);
plugin.configure(registry);
verify(registry, times(1)).require(eq(MarkwonInlineParserPlugin.class));
verify(inlineParserPlugin, times(1)).factoryBuilder();
final ArgumentCaptor<InlineProcessor> captor = ArgumentCaptor.forClass(InlineProcessor.class);
verify(factoryBuilder, times(1)).addInlineProcessor(captor.capture());
final InlineProcessor inlineProcessor = captor.getValue();
assertTrue(inlineParserPlugin.getClass().getName(), inlineProcessor instanceof JLatexMathInlineProcessor);
}
// parser
{
final Parser.Builder builder = mock(Parser.Builder.class);
plugin.configureParser(builder);
final ArgumentCaptor<BlockParserFactory> captor =
ArgumentCaptor.forClass(BlockParserFactory.class);
verify(builder, times(1)).customBlockParserFactory(captor.capture());
final BlockParserFactory factory = captor.getValue();
assertTrue(factory.getClass().getName(), factory instanceof JLatexMathBlockParser.Factory);
}
// visitor
{
final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class);
plugin.configureVisitor(builder);
final ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(builder, times(2)).on(captor.capture(), any(MarkwonVisitor.NodeVisitor.class));
final List<Class> nodes = captor.getAllValues();
assertEquals(2, nodes.size());
assertTrue(nodes.toString(), nodes.contains(JLatexMathNode.class));
assertTrue(nodes.toString(), nodes.contains(JLatexMathBlock.class));
}
}
} }

View File

@ -121,12 +121,15 @@ public class TablePlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBlock tableBlock) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull TableBlock tableBlock) {
visitor.blockStart(tableBlock);
visitor.visitChildren(tableBlock); visitor.visitChildren(tableBlock);
if (visitor.hasNext(tableBlock)) { // if (visitor.hasNext(tableBlock)) {
visitor.ensureNewLine(); // visitor.ensureNewLine();
visitor.forceNewLine(); // visitor.forceNewLine();
} // }
visitor.blockEnd(tableBlock);
} }
}) })
.on(TableBody.class, new MarkwonVisitor.NodeVisitor<TableBody>() { .on(TableBody.class, new MarkwonVisitor.NodeVisitor<TableBody>() {

View File

@ -14,6 +14,7 @@ android {
} }
dependencies { dependencies {
api project(':markwon-core')
api deps['x-annotations'] api deps['x-annotations']
api deps['commonmark'] api deps['commonmark']

View File

@ -0,0 +1,59 @@
package io.noties.markwon.inlineparser;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import io.noties.markwon.AbstractMarkwonPlugin;
/**
* @since 4.3.0
*/
public class MarkwonInlineParserPlugin extends AbstractMarkwonPlugin {
public interface BuilderConfigure<B extends MarkwonInlineParser.FactoryBuilder> {
void configureBuilder(@NonNull B factoryBuilder);
}
@NonNull
public static MarkwonInlineParserPlugin create() {
return create(MarkwonInlineParser.factoryBuilder());
}
@NonNull
public static MarkwonInlineParserPlugin create(@NonNull BuilderConfigure<MarkwonInlineParser.FactoryBuilder> configure) {
final MarkwonInlineParser.FactoryBuilder factoryBuilder = MarkwonInlineParser.factoryBuilder();
configure.configureBuilder(factoryBuilder);
return new MarkwonInlineParserPlugin(factoryBuilder);
}
@NonNull
public static MarkwonInlineParserPlugin create(@NonNull MarkwonInlineParser.FactoryBuilder factoryBuilder) {
return new MarkwonInlineParserPlugin(factoryBuilder);
}
@NonNull
public static <B extends MarkwonInlineParser.FactoryBuilder> MarkwonInlineParserPlugin create(
@NonNull B factoryBuilder,
@NonNull BuilderConfigure<B> configure) {
configure.configureBuilder(factoryBuilder);
return new MarkwonInlineParserPlugin(factoryBuilder);
}
private final MarkwonInlineParser.FactoryBuilder factoryBuilder;
@SuppressWarnings("WeakerAccess")
MarkwonInlineParserPlugin(@NonNull MarkwonInlineParser.FactoryBuilder factoryBuilder) {
this.factoryBuilder = factoryBuilder;
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.inlineParserFactory(factoryBuilder.build());
}
@NonNull
public MarkwonInlineParser.FactoryBuilder factoryBuilder() {
return factoryBuilder;
}
}

View File

@ -14,6 +14,12 @@ android {
} }
dependencies { dependencies {
deps.with {
// To use LinkifyCompat
// note that this dependency must be added on a client side explicitly
compileOnly it['x-core']
}
api project(':markwon-core') api project(':markwon-core')
} }

View File

@ -1,11 +1,13 @@
package io.noties.markwon.linkify; package io.noties.markwon.linkify;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.text.util.LinkifyCompat;
import org.commonmark.node.Link; import org.commonmark.node.Link;
@ -33,19 +35,43 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
@NonNull @NonNull
public static LinkifyPlugin create() { public static LinkifyPlugin create() {
return create(Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS); return create(false);
}
/**
* @param useCompat If true, use {@link LinkifyCompat} to handle links.
* Note that the {@link LinkifyCompat} depends on androidx.core:core,
* the dependency must be added on a client side explicitly.
* @since 4.3.0 `useCompat` argument
*/
@NonNull
public static LinkifyPlugin create(boolean useCompat) {
return create(Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS | Linkify.WEB_URLS, useCompat);
} }
@NonNull @NonNull
public static LinkifyPlugin create(@LinkifyMask int mask) { public static LinkifyPlugin create(@LinkifyMask int mask) {
return new LinkifyPlugin(mask); return new LinkifyPlugin(mask, false);
}
/**
* @param useCompat If true, use {@link LinkifyCompat} to handle links.
* Note that the {@link LinkifyCompat} depends on androidx.core:core,
* the dependency must be added on a client side explicitly.
* @since 4.3.0 `useCompat` argument
*/
@NonNull
public static LinkifyPlugin create(@LinkifyMask int mask, boolean useCompat) {
return new LinkifyPlugin(mask, useCompat);
} }
private final int mask; private final int mask;
private final boolean useCompat;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
LinkifyPlugin(@LinkifyMask int mask) { LinkifyPlugin(@LinkifyMask int mask, boolean useCompat) {
this.mask = mask; this.mask = mask;
this.useCompat = useCompat;
} }
@Override @Override
@ -53,7 +79,14 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
registry.require(CorePlugin.class, new Action<CorePlugin>() { registry.require(CorePlugin.class, new Action<CorePlugin>() {
@Override @Override
public void apply(@NonNull CorePlugin corePlugin) { public void apply(@NonNull CorePlugin corePlugin) {
corePlugin.addOnTextAddedListener(new LinkifyTextAddedListener(mask)); final LinkifyTextAddedListener listener;
// @since 4.3.0
if (useCompat) {
listener = new LinkifyCompatTextAddedListener(mask);
} else {
listener = new LinkifyTextAddedListener(mask);
}
corePlugin.addOnTextAddedListener(listener);
} }
}); });
} }
@ -80,7 +113,7 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
// render calls from different threads and ... better performance) // render calls from different threads and ... better performance)
final SpannableStringBuilder builder = new SpannableStringBuilder(text); final SpannableStringBuilder builder = new SpannableStringBuilder(text);
if (Linkify.addLinks(builder, mask)) { if (addLinks(builder, mask)) {
// target URL span specifically // target URL span specifically
final URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class); final URLSpan[] spans = builder.getSpans(0, builder.length(), URLSpan.class);
if (spans != null if (spans != null
@ -101,5 +134,22 @@ public class LinkifyPlugin extends AbstractMarkwonPlugin {
} }
} }
} }
protected boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
return Linkify.addLinks(text, mask);
}
}
// @since 4.3.0
private static class LinkifyCompatTextAddedListener extends LinkifyTextAddedListener {
LinkifyCompatTextAddedListener(int mask) {
super(mask);
}
@Override
protected boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
return LinkifyCompat.addLinks(text, mask);
}
} }
} }

View File

@ -25,3 +25,24 @@ A new version must be pushed to MavenCentral and new git-tag with version name m
created in the repository. created in the repository.
Rinse and repeat. Rinse and repeat.
## `@since` annotation
Although it is not required it is a nice thing to do: add `@since $VERSION` comment to the code
whenever it is possible (at least for publicly accessible code - API). This would help
navigating the project without the need to checkout the full VCS history. As keeping track of
current and/or upcoming version can be error-prone it is better to insert a generic `@since code`
that can be properly substituted upon a release.
For example, `@since $nap` seems like a good candidate. For this a live template can be created and used
whenever a new API method/field/functionality-change is introduced (`snc`):
```
@since $nap;
```
This live template would be possible to use in both inline comment and javadoc comment.
## documentation
If there are updates to documentation web site, do not forget to publish it

View File

@ -49,6 +49,7 @@ dependencies {
implementation project(':markwon-syntax-highlight') implementation project(':markwon-syntax-highlight')
implementation project(':markwon-image-picasso') implementation project(':markwon-image-picasso')
implementation project(':markwon-image-glide')
deps.with { deps.with {
implementation it['x-recycler-view'] implementation it['x-recycler-view']

View File

@ -35,6 +35,9 @@
<activity android:name=".inlineparser.InlineParserActivity" /> <activity android:name=".inlineparser.InlineParserActivity" />
<activity android:name=".htmldetails.HtmlDetailsActivity" /> <activity android:name=".htmldetails.HtmlDetailsActivity" />
<activity android:name=".tasklist.TaskListActivity" />
<activity android:name=".images.ImagesActivity" />
<activity android:name=".notification.NotificationActivity" />
</application> </application>

View File

@ -0,0 +1,49 @@
package io.noties.markwon.sample;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public abstract class ActivityWithMenuOptions extends Activity {
@NonNull
public abstract MenuOptions menuOptions();
protected void beforeOptionSelected(@NonNull String option) {
// no op, override to customize
}
protected void afterOptionSelected(@NonNull String option) {
// no op, override to customize
}
private MenuOptions menuOptions;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
menuOptions = menuOptions();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return menuOptions.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final MenuOptions.Option option = menuOptions.onOptionsItemSelected(item);
if (option != null) {
beforeOptionSelected(option.title);
option.action.run();
afterOptionSelected(option.title);
return true;
}
return false;
}
}

View File

@ -25,11 +25,14 @@ import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
import io.noties.markwon.sample.editor.EditorActivity; import io.noties.markwon.sample.editor.EditorActivity;
import io.noties.markwon.sample.html.HtmlActivity; import io.noties.markwon.sample.html.HtmlActivity;
import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity; import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity;
import io.noties.markwon.sample.images.ImagesActivity;
import io.noties.markwon.sample.inlineparser.InlineParserActivity; import io.noties.markwon.sample.inlineparser.InlineParserActivity;
import io.noties.markwon.sample.latex.LatexActivity; import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.notification.NotificationActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity; import io.noties.markwon.sample.precomputed.PrecomputedActivity;
import io.noties.markwon.sample.recycler.RecyclerActivity; import io.noties.markwon.sample.recycler.RecyclerActivity;
import io.noties.markwon.sample.simpleext.SimpleExtActivity; import io.noties.markwon.sample.simpleext.SimpleExtActivity;
import io.noties.markwon.sample.tasklist.TaskListActivity;
public class MainActivity extends Activity { public class MainActivity extends Activity {
@ -132,6 +135,18 @@ public class MainActivity extends Activity {
activity = HtmlDetailsActivity.class; activity = HtmlDetailsActivity.class;
break; break;
case TASK_LIST:
activity = TaskListActivity.class;
break;
case IMAGES:
activity = ImagesActivity.class;
break;
case REMOTE_VIEWS:
activity = NotificationActivity.class;
break;
default: default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item); throw new IllegalStateException("No Activity is associated with sample-item: " + item);
} }

View File

@ -0,0 +1,57 @@
package io.noties.markwon.sample;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
public class MenuOptions {
@NonNull
public static MenuOptions create() {
return new MenuOptions();
}
static class Option {
final String title;
final Runnable action;
Option(@NonNull String title, @NonNull Runnable action) {
this.title = title;
this.action = action;
}
}
// to preserve order use LinkedHashMap
private final Map<String, Runnable> actions = new LinkedHashMap<>();
@NonNull
public MenuOptions add(@NonNull String title, @NonNull Runnable action) {
actions.put(title, action);
return this;
}
boolean onCreateOptionsMenu(Menu menu) {
if (!actions.isEmpty()) {
for (String key : actions.keySet()) {
menu.add(key);
}
return true;
}
return false;
}
@Nullable
Option onOptionsItemSelected(MenuItem item) {
final String title = String.valueOf(item.getTitle());
final Runnable action = actions.get(title);
if (action != null) {
return new Option(title, action);
}
return null;
}
}

View File

@ -27,7 +27,13 @@ public enum Sample {
INLINE_PARSER(R.string.sample_inline_parser), INLINE_PARSER(R.string.sample_inline_parser),
HTML_DETAILS(R.string.sample_html_details); HTML_DETAILS(R.string.sample_html_details),
TASK_LIST(R.string.sample_task_list),
IMAGES(R.string.sample_images),
REMOTE_VIEWS(R.string.sample_remote_views);
private final int textResId; private final int textResId;

View File

@ -1,77 +1,94 @@
package io.noties.markwon.sample.basicplugins; package io.noties.markwon.sample.basicplugins;
import android.app.Activity;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Layout; import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.AlignmentSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.commonmark.node.Heading; import org.commonmark.node.Heading;
import org.commonmark.node.Node;
import org.commonmark.node.Paragraph; import org.commonmark.node.Paragraph;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps; import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.html.HtmlTag; import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.html.tag.SimpleTagHandler;
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;
import io.noties.markwon.image.network.NetworkSchemeHandler; import io.noties.markwon.image.network.NetworkSchemeHandler;
import io.noties.markwon.movement.MovementMethodPlugin; import io.noties.markwon.movement.MovementMethodPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
public class BasicPluginsActivity extends Activity { public class BasicPluginsActivity extends ActivityWithMenuOptions {
private TextView textView; private TextView textView;
private ScrollView scrollView;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("paragraphSpan", this::paragraphSpan)
.add("disableNode", this::disableNode)
.add("customizeTheme", this::customizeTheme)
.add("linkWithMovementMethod", this::linkWithMovementMethod)
.add("imagesPlugin", this::imagesPlugin)
.add("softBreakAddsSpace", this::softBreakAddsSpace)
.add("softBreakAddsNewLine", this::softBreakAddsNewLine)
.add("additionalSpacing", this::additionalSpacing)
.add("headingNoSpace", this::headingNoSpace)
.add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
.add("anchor", this::anchor);
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = new TextView(this); textView = findViewById(R.id.text_view);
setContentView(textView); scrollView = findViewById(R.id.scroll_view);
step_1(); paragraphSpan();
//
step_2(); // disableNode();
//
step_3(); // customizeTheme();
//
step_4(); // linkWithMovementMethod();
//
step_5(); // imagesPlugin();
step_6();
} }
/** /**
* In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care
* of everything else). * of everything else).
* <p>
* Please note that when a plugin is registered and it <em>depends</em> on CorePlugin, there is no
* need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare
* it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}).
* <p>
* Order in which plugins are specified to the builder is of little importance as long as each
* plugin clearly states what dependencies it has
*/ */
private void step_1() { private void paragraphSpan() {
final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!";
@ -91,7 +108,7 @@ public class BasicPluginsActivity extends Activity {
/** /**
* To disable some nodes from rendering another custom plugin can be used * To disable some nodes from rendering another custom plugin can be used
*/ */
private void step_2() { private void disableNode() {
final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
@ -116,7 +133,7 @@ public class BasicPluginsActivity extends Activity {
/** /**
* To customize core theme plugin can be used again * To customize core theme plugin can be used again
*/ */
private void step_3() { private void customizeTheme() {
final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```";
@ -145,7 +162,7 @@ public class BasicPluginsActivity extends Activity {
* <p> * <p>
* In order to customize them a custom plugin should be used * In order to customize them a custom plugin should be used
*/ */
private void step_4() { private void linkWithMovementMethod() {
final String markdown = "[a link without scheme](github.com)"; final String markdown = "[a link without scheme](github.com)";
@ -178,7 +195,7 @@ public class BasicPluginsActivity extends Activity {
* images handling (parsing markdown containing images, obtain an image from network * images handling (parsing markdown containing images, obtain an image from network
* file system or assets). Please note that * file system or assets). Please note that
*/ */
private void step_5() { private void imagesPlugin() {
final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)"; final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)";
@ -220,33 +237,269 @@ public class BasicPluginsActivity extends Activity {
markwon.setMarkdown(textView, markdown); markwon.setMarkdown(textView, markdown);
} }
public void step_6() { private void softBreakAddsSpace() {
// default behavior
final String md = "" +
"Hello there ->(line)\n(break)<- going on and on";
Markwon.create(this).setMarkdown(textView, md);
}
private void softBreakAddsNewLine() {
// insert a new line when markdown has a soft break
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(HtmlPlugin.create()) .usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(new AbstractMarkwonPlugin() { .build();
@Override
public void configure(@NonNull Registry registry) { final String md = "" +
registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() { "Hello there ->(line)\n(break)<- going on and on";
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { markwon.setMarkdown(textView, md);
return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
} }
@NonNull private void additionalSpacing() {
// please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
final int spacing = (int) (128 * getResources().getDisplayMetrics().density + .5F);
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override @Override
public Collection<String> supportedTags() { public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
return Collections.singleton("center"); builder.headingBreakHeight(0);
} }
}));
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.appendFactory(
Heading.class,
(configuration, props) -> new LastLineSpacingSpan(spacing));
} }
}) })
.build(); .build();
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
markwon.setMarkdown(textView, md);
} }
private void headingNoSpace() {
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.headingBreakHeight(0);
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Heading.class, (visitor, heading) -> {
visitor.ensureNewLine();
final int length = visitor.length();
visitor.visitChildren(heading);
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
visitor.setSpansForNodeOptional(heading, length);
if (visitor.hasNext(heading)) {
visitor.ensureNewLine();
// visitor.forceNewLine();
}
});
}
})
.build();
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
markwon.setMarkdown(textView, md);
}
private void headingNoSpaceBlockHandler() {
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerDef() {
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (node instanceof Heading) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// ensure new line but do not force insert one
}
} else {
super.blockEnd(visitor, node);
}
}
});
}
})
.build();
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
markwon.setMarkdown(textView, md);
}
private void allBlocksNoForcedLine() {
final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() {
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
}
}
};
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(blockHandler);
}
})
.build();
final String md = "" +
"# Hello there!\n\n" +
"* a first\n" +
"* second\n" +
"- third\n" +
"* * nested one\n\n" +
"> block quote\n\n" +
"> > and nested one\n\n" +
"```java\n" +
"final int i = 0;\n" +
"```\n\n";
markwon.setMarkdown(textView, md);
}
// public void step_6() {
//
// final Markwon markwon = Markwon.builder(this)
// .usePlugin(HtmlPlugin.create())
// .usePlugin(new AbstractMarkwonPlugin() {
// @Override
// public void configure(@NonNull Registry registry) {
// registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() {
// @Override
// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
// return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
// }
//
// @NonNull
// @Override
// public Collection<String> supportedTags() {
// return Collections.singleton("center");
// }
// }));
// }
// })
// .build();
// }
// text lifecycle (after/before) // text lifecycle (after/before)
// rendering lifecycle (before/after) // rendering lifecycle (before/after)
// renderProps // renderProps
// process // process
// priority
private static class AnchorSpan {
final String anchor;
AnchorSpan(@NonNull String anchor) {
this.anchor = anchor;
}
}
@NonNull
private String createAnchor(@NonNull CharSequence content) {
return String.valueOf(content)
.replaceAll("[^\\w]", "")
.toLowerCase();
}
private static class AnchorLinkResolver extends LinkResolverDef {
interface ScrollTo {
void scrollTo(@NonNull View view, int top);
}
private final ScrollTo scrollTo;
AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
this.scrollTo = scrollTo;
}
@Override
public void resolve(@NonNull View view, @NonNull String link) {
if (link.startsWith("#")) {
final TextView textView = (TextView) view;
final Spanned spanned = (Spannable) textView.getText();
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
if (spans != null) {
final String anchor = link.substring(1);
for (AnchorSpan span: spans) {
if (anchor.equals(span.anchor)) {
final int start = spanned.getSpanStart(span);
final int line = textView.getLayout().getLineForOffset(start);
final int top = textView.getLayout().getLineTop(line);
scrollTo.scrollTo(textView, top);
return;
}
}
}
}
super.resolve(view, link);
}
}
private void anchor() {
final String lorem = getString(R.string.lorem);
final String md = "" +
"Hello [there](#there)!\n\n\n" +
lorem + "\n\n" +
"# There!\n\n" +
lorem;
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top)));
}
@Override
public void afterSetText(@NonNull TextView textView) {
final Spannable spannable = (Spannable) textView.getText();
// obtain heading spans
final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
if (spans != null) {
for (HeadingSpan span : spans) {
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
final int flags = spannable.getSpanFlags(span);
spannable.setSpan(
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
start,
end,
flags
);
}
}
}
})
.build();
markwon.setMarkdown(textView, md);
}
} }

View File

@ -1,36 +1,48 @@
package io.noties.markwon.sample.core; package io.noties.markwon.sample.core;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.text.Spanned; import android.text.Spanned;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.core.CorePlugin; import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
public class CoreActivity extends Activity { public class CoreActivity extends ActivityWithMenuOptions {
private TextView textView; private TextView textView;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("simple", this::simple)
.add("toast", this::toast)
.add("alreadyParsed", this::alreadyParsed);
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = new TextView(this); textView = findViewById(R.id.text_view);
setContentView(textView);
step_1(); // step_1();
step_2(); simple();
step_3(); // toast();
//
step_4(); // alreadyParsed();
} }
/** /**
@ -70,7 +82,7 @@ public class CoreActivity extends Activity {
/** /**
* To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)} * To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)}
*/ */
private void step_2() { private void simple() {
// this is raw markdown // this is raw markdown
final String markdown = "Hello **markdown**!"; final String markdown = "Hello **markdown**!";
@ -91,7 +103,7 @@ public class CoreActivity extends Activity {
* of invalidation. But if a Toast for example is created with a custom view * of invalidation. But if a Toast for example is created with a custom view
* ({@code new Toast(this).setView(...) }) and has access to a TextView everything <em>should</em> work. * ({@code new Toast(this).setView(...) }) and has access to a TextView everything <em>should</em> work.
*/ */
private void step_3() { private void toast() {
final String markdown = "*Toast* __here__!\n\n> And a quote!"; final String markdown = "*Toast* __here__!\n\n> And a quote!";
@ -105,7 +117,7 @@ public class CoreActivity extends Activity {
/** /**
* To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)} * To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)}
*/ */
private void step_4() { private void alreadyParsed() {
final String markdown = "This **is** pre-parsed [markdown](#)"; final String markdown = "This **is** pre-parsed [markdown](#)";

View File

@ -1,6 +1,5 @@
package io.noties.markwon.sample.customextension2; package io.noties.markwon.sample.customextension2;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
@ -25,34 +24,45 @@ import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.core.CoreProps; import io.noties.markwon.core.CoreProps;
import io.noties.markwon.inlineparser.InlineProcessor; import io.noties.markwon.inlineparser.InlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser; import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
public class CustomExtensionActivity2 extends Activity { public class CustomExtensionActivity2 extends ActivityWithMenuOptions {
private static final String MD = "" +
"# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties";
private TextView textView;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("text_added", this::text_added)
.add("inline_parsing", this::inline_parsing);
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view); setContentView(R.layout.activity_text_view);
final TextView textView = findViewById(R.id.text_view); textView = findViewById(R.id.text_view);
// let's look for github special links: // let's look for github special links:
// * `#1` - an issue or a pull request // * `#1` - an issue or a pull request
// * `@user` link to a user // * `@user` link to a user
final String md = "# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties";
// inline_parsing(textView, md); // inline_parsing(textView, md);
text_added(textView, md); text_added();
} }
private void text_added(@NonNull TextView textView, @NonNull String md) { private void text_added() {
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AbstractMarkwonPlugin() {
@ -64,10 +74,10 @@ public class CustomExtensionActivity2 extends Activity {
}) })
.build(); .build();
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, MD);
} }
private void inline_parsing(@NonNull TextView textView, @NonNull String md) { private void inline_parsing() {
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// include all current defaults (otherwise will be empty - contain only our inline-processors) // include all current defaults (otherwise will be empty - contain only our inline-processors)
@ -86,7 +96,7 @@ public class CustomExtensionActivity2 extends Activity {
}) })
.build(); .build();
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, MD);
} }
private static class IssueInlineProcessor extends InlineProcessor { private static class IssueInlineProcessor extends InlineProcessor {

View File

@ -1,11 +1,11 @@
package io.noties.markwon.sample.editor; package io.noties.markwon.sample.editor;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.MetricAffectingSpan; import android.text.style.MetricAffectingSpan;
@ -41,30 +41,61 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
import io.noties.markwon.inlineparser.EntityInlineProcessor; import io.noties.markwon.inlineparser.EntityInlineProcessor;
import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.HtmlInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser; import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
public class EditorActivity extends Activity { public class EditorActivity extends ActivityWithMenuOptions {
private EditText editText; private EditText editText;
private String pendingInput;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("simpleProcess", this::simple_process)
.add("simplePreRender", this::simple_pre_render)
.add("customPunctuationSpan", this::custom_punctuation_span)
.add("additionalEditSpan", this::additional_edit_span)
.add("additionalPlugins", this::additional_plugins)
.add("multipleEditSpans", this::multiple_edit_spans)
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
.add("pluginRequire", this::plugin_require)
.add("pluginNoDefaults", this::plugin_no_defaults);
}
@Override
protected void beforeOptionSelected(@NonNull String option) {
// we cannot _clear_ editText of text-watchers without keeping a reference to them...
pendingInput = editText != null
? editText.getText().toString()
: null;
createView();
}
@Override
protected void afterOptionSelected(@NonNull String option) {
if (!TextUtils.isEmpty(pendingInput)) {
editText.setText(pendingInput);
}
}
private void createView() {
setContentView(R.layout.activity_editor);
this.editText = findViewById(R.id.edit_text);
initBottomBar();
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editor); createView();
this.editText = findViewById(R.id.edit_text);
initBottomBar();
// simple_process();
// simple_pre_render();
// custom_punctuation_span();
// additional_edit_span();
// additional_plugins();
multiple_edit_spans(); multiple_edit_spans();
} }
@ -216,6 +247,76 @@ public class EditorActivity extends Activity {
editor, Executors.newSingleThreadExecutor(), editText)); editor, Executors.newSingleThreadExecutor(), editText));
} }
private void multiple_edit_spans_plugin() {
// inline parsing is configured via MarkwonInlineParserPlugin
// for links to be clickable
editText.setMovementMethod(LinkMovementMethod.getInstance());
final Markwon markwon = Markwon.builder(this)
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(MarkwonInlineParserPlugin.create(builder -> {
builder
.excludeInlineProcessor(BangInlineProcessor.class)
.excludeInlineProcessor(HtmlInlineProcessor.class)
.excludeInlineProcessor(EntityInlineProcessor.class);
}))
.build();
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.useEditHandler(new EmphasisEditHandler())
.useEditHandler(new StrongEmphasisEditHandler())
.useEditHandler(new StrikethroughEditHandler())
.useEditHandler(new CodeEditHandler())
.useEditHandler(new BlockQuoteEditHandler())
.useEditHandler(new LinkEditHandler(onClick))
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
private void plugin_require() {
// usage of plugin from other plugins
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class)
.factoryBuilder()
.excludeInlineProcessor(HtmlInlineProcessor.class);
}
})
.build();
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
private void plugin_no_defaults() {
// a plugin with no defaults registered
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
// // if anything, they can be included here
//// factoryBuilder.includeDefaults()
// }))
.build();
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
private void initBottomBar() { private void initBottomBar() {
// all except block-quote wraps if have selection, or inserts at current cursor position // all except block-quote wraps if have selection, or inserts at current cursor position

View File

@ -40,25 +40,29 @@ class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class); final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
editLinkSpan.link = span.getLink(); editLinkSpan.link = span.getLink();
final int s; // First first __letter__ to find link content (scheme start in URL, receiver in email address)
final int e; // NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
// display. For example, we _could_ also look for a digit, but:
// * if phone number start with special symbol, we won't have it (`+`, `(`)
// * it might interfere with an ordered-list
int start = -1;
// markdown link vs. autolink for (int i = spanStart, length = input.length(); i < length; i++) {
if ('[' == input.charAt(spanStart)) { if (Character.isLetter(input.charAt(i))) {
s = spanStart + 1; start = i;
e = spanStart + 1 + spanTextLength; break;
} else { }
s = spanStart;
e = spanStart + spanTextLength;
} }
if (start > -1) {
editable.setSpan( editable.setSpan(
editLinkSpan, editLinkSpan,
s, start,
e, start + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
); );
} }
}
@NonNull @NonNull
@Override @Override

View File

@ -1,6 +1,5 @@
package io.noties.markwon.sample.html; package io.noties.markwon.sample.html;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.text.Layout; import android.text.Layout;
import android.text.TextUtils; import android.text.TextUtils;
@ -12,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Px; import androidx.annotation.Px;
import org.commonmark.node.Paragraph;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Random; import java.util.Random;
@ -27,9 +28,24 @@ import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer; import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler; import io.noties.markwon.html.TagHandler;
import io.noties.markwon.html.tag.SimpleTagHandler; import io.noties.markwon.html.tag.SimpleTagHandler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
public class HtmlActivity extends Activity { public class HtmlActivity extends ActivityWithMenuOptions {
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("align", this::align)
.add("randomCharSize", this::randomCharSize)
.add("enhance", this::enhance)
.add("image", this::image);
}
private TextView textView;
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@ -39,35 +55,9 @@ public class HtmlActivity extends Activity {
// let's define some custom tag-handlers // let's define some custom tag-handlers
final TextView textView = findViewById(R.id.text_view); textView = findViewById(R.id.text_view);
final Markwon markwon = Markwon.builder(this) align();
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new AlignTagHandler())
.addHandler(new RandomCharSize(new Random(42L), textView.getTextSize()))
.addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
}
})
.build();
final String markdown = "# Hello, HTML\n" +
"\n" +
"<align center>We are centered</align>\n" +
"\n" +
"<align end>We are at the end</align>\n" +
"\n" +
"<align>We should be at the start</align>\n" +
"\n" +
"<random-char-size>\n" +
"This message should have a jumpy feeling because of different sizes of characters\n" +
"</random-char-size>\n\n" +
"<enhance start=\"5\" end=\"12\">This is text that must be enhanced, at least a part of it</enhance>";
markwon.setMarkdown(textView, markdown);
} }
// we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content // we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content
@ -105,6 +95,31 @@ public class HtmlActivity extends Activity {
} }
} }
private void align() {
final String md = "" +
"<align center>We are centered</align>\n" +
"\n" +
"<align end>We are at the end</align>\n" +
"\n" +
"<align>We should be at the start</align>\n" +
"\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new AlignTagHandler()));
}
})
.build();
markwon.setMarkdown(textView, md);
}
// each character will have random size // each character will have random size
private static class RandomCharSize extends TagHandler { private static class RandomCharSize extends TagHandler {
@ -139,6 +154,27 @@ public class HtmlActivity extends Activity {
} }
} }
private void randomCharSize() {
final String md = "" +
"<random-char-size>\n" +
"This message should have a jumpy feeling because of different sizes of characters\n" +
"</random-char-size>\n\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new RandomCharSize(new Random(42L), textView.getTextSize())));
}
})
.build();
markwon.setMarkdown(textView, md);
}
private static class EnhanceTagHandler extends TagHandler { private static class EnhanceTagHandler extends TagHandler {
private final int enhanceTextSize; private final int enhanceTextSize;
@ -187,4 +223,49 @@ public class HtmlActivity extends Activity {
return position; return position;
} }
} }
private void enhance() {
final String md = "" +
"<enhance start=\"5\" end=\"12\">This is text that must be enhanced, at least a part of it</enhance>";
final Markwon markwon = Markwon.builder(this)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
}
})
.build();
markwon.setMarkdown(textView, md);
}
private void image() {
// treat unclosed/void `img` tag as HTML inline
final String md = "" +
"## Try CommonMark\n" +
"\n" +
"Markwon IMG:\n" +
"\n" +
"![](https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG)\n" +
"\n" +
"New lines...\n" +
"\n" +
"HTML IMG:\n" +
"\n" +
"<img src=\"https://upload.wikimedia.org/wikipedia/it/thumb/c/c5/GTA_2.JPG/220px-GTA_2.JPG\"></img>\n" +
"\n" +
"New lines\n\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(ImagesPlugin.create())
.usePlugin(HtmlPlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
} }

View File

@ -0,0 +1,99 @@
package io.noties.markwon.sample.images;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.target.Target;
import io.noties.markwon.Markwon;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.glide.GlideImagesPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
public class ImagesActivity extends ActivityWithMenuOptions {
private TextView textView;
@NonNull
@Override
public MenuOptions menuOptions() {
// todo: same for other plugins
return MenuOptions.create()
.add("glide-singleImage", this::glideSingleImage)
.add("glide-singleImageWithPlaceholder", this::glideSingleImageWithPlaceholder)
.add("click", this::click);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = findViewById(R.id.text_view);
glideSingleImageWithPlaceholder();
}
private void glideSingleImage() {
final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
final Markwon markwon = Markwon.builder(this)
.usePlugin(GlideImagesPlugin.create(this))
.build();
markwon.setMarkdown(textView, md);
}
// can be checked when used first, otherwise works as expected...
private void glideSingleImageWithPlaceholder() {
final String md = "[![undefined](https://img.youtube.com/vi/gs1I8_m4AOM/0.jpg)](https://www.youtube.com/watch?v=gs1I8_m4AOM)";
final Context context = this;
final Markwon markwon = Markwon.builder(context)
.usePlugin(GlideImagesPlugin.create(new GlideImagesPlugin.GlideStore() {
@NonNull
@Override
public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
// final Drawable placeholder = ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp);
// placeholder.setBounds(0, 0, 100, 100);
return Glide.with(context)
.load(drawable.getDestination())
// .placeholder(ContextCompat.getDrawable(context, R.drawable.ic_home_black_36dp));
// .placeholder(placeholder);
.placeholder(R.drawable.ic_home_black_36dp);
}
@Override
public void cancel(@NonNull Target<?> target) {
Glide.with(context)
.clear(target);
}
}))
.build();
markwon.setMarkdown(textView, md);
}
private void click() {
textView.setMovementMethod(LinkMovementMethod.getInstance());
final String md = "[![markdown](https://www.mdeditor.com/images/logos/markdown.png \"markdown\")](https://www.mdeditor.com/images/logos/markdown.png)";
final Markwon markwon = Markwon.builder(this)
.usePlugin(ImagesPlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,6 +1,5 @@
package io.noties.markwon.sample.inlineparser; package io.noties.markwon.sample.inlineparser;
import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.widget.TextView; import android.widget.TextView;
@ -25,19 +24,32 @@ import io.noties.markwon.Markwon;
import io.noties.markwon.inlineparser.BackticksInlineProcessor; import io.noties.markwon.inlineparser.BackticksInlineProcessor;
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor; import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser; import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor; import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
public class InlineParserActivity extends Activity { public class InlineParserActivity extends ActivityWithMenuOptions {
private TextView textView; private TextView textView;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("links_only", this::links_only)
.add("disable_code", this::disable_code)
.add("pluginWithDefaults", this::pluginWithDefaults)
.add("pluginNoDefaults", this::pluginNoDefaults);
}
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view); setContentView(R.layout.activity_text_view);
this.textView = findViewById(R.id.text_view); textView = findViewById(R.id.text_view);
// links_only(); // links_only();
@ -115,4 +127,50 @@ public class InlineParserActivity extends Activity {
"**Good day!**"; "**Good day!**";
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, md);
} }
private void pluginWithDefaults() {
// a plugin with defaults registered
final String md = "no [links](#) for **you** `code`!";
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create())
// the same as:
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilder()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
plugin.factoryBuilder()
.excludeInlineProcessor(OpenBracketInlineProcessor.class);
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
private void pluginNoDefaults() {
// a plugin with NO defaults registered
final String md = "no [links](#) for **you** `code`!";
final Markwon markwon = Markwon.builder(this)
// pass `MarkwonInlineParser.factoryBuilderNoDefaults()` no disable all
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
plugin.factoryBuilder()
.addInlineProcessor(new BackticksInlineProcessor());
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
} }

View File

@ -1,29 +1,31 @@
package io.noties.markwon.sample.latex; package io.noties.markwon.sample.latex;
import android.app.Activity; import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.ext.latex.JLatexMathPlugin; import io.noties.markwon.ext.latex.JLatexMathPlugin;
import io.noties.markwon.ext.latex.JLatexMathTheme;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
import ru.noties.jlatexmath.JLatexMathDrawable;
public class LatexActivity extends Activity { public class LatexActivity extends ActivityWithMenuOptions {
@Override private static final String LATEX_ARRAY;
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
final TextView textView = findViewById(R.id.text_view);
static {
String latex = "\\begin{array}{l}"; String latex = "\\begin{array}{l}";
latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\"; latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\";
latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\"; latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\";
@ -34,61 +36,222 @@ public class LatexActivity extends Activity {
latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\"; latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\";
latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\"; latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\";
latex += "\\end{array}"; latex += "\\end{array}";
LATEX_ARRAY = latex;
}
// String latex = "\\text{A long division \\longdiv{12345}{13}"; private static final String LATEX_LONG_DIVISION = "\\text{A long division \\longdiv{12345}{13}";
// String latex = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}"; private static final String LATEX_BANGLE = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}";
private static final String LATEX_BOXES;
// String latex = "\\begin{array}{cc}"; static {
// latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr"; String latex = "\\begin{array}{cc}";
// latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr"; latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
// latex += "\\end{array}"; latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
latex += "\\end{array}";
LATEX_BOXES = latex;
}
final String markdown = "# Example of LaTeX\n\n$$" private TextView textView;
+ latex + "$$\n\n something like **this**"; private View parent;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("array", this::array)
.add("longDivision", this::longDivision)
.add("bangle", this::bangle)
.add("boxes", this::boxes)
.add("insideBlockQuote", this::insideBlockQuote)
.add("error", this::error)
.add("legacy", this::legacy)
.add("textColor", this::textColor)
.add("defaultTextColor", this::defaultTextColor)
.add("inlineAndBlock", this::inlineAndBlock)
.add("dark", this::dark);
}
@Override
protected void beforeOptionSelected(@NonNull String option) {
super.beforeOptionSelected(option);
// reset text color
textView.setTextColor(0xFF000000);
// reset background
parent.setBackgroundColor(0xFFffffff);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = findViewById(R.id.text_view);
parent = findViewById(R.id.scroll_view);
// array();
longDivision();
}
private void array() {
renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_ARRAY));
}
private void longDivision() {
renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION));
}
private void bangle() {
renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BANGLE));
}
private void boxes() {
renderWithBlocksAndInlines(wrapLatexInSampleMarkdown(LATEX_BOXES));
}
private void insideBlockQuote() {
String latex = "W=W_1+W_2=F_1X_1-F_2X_2";
final String md = "" +
"# LaTeX inside a blockquote\n" +
"> $$" + latex + "$$\n";
renderWithBlocksAndInlines(md);
}
private void error() {
final String md = wrapLatexInSampleMarkdown("\\sum_{i=0}^\\infty x \\cdot 0 \\rightarrow \\iMightNotExist{0}");
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
// .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() { .usePlugin(MarkwonInlineParserPlugin.create())
// @Override .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
// public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) { builder.inlinesEnabled(true);
// builder //noinspection Convert2Lambda
// .backgroundProvider(new JLatexMathPlugin.BackgroundProvider() { builder.errorHandler(new JLatexMathPlugin.ErrorHandler() {
// @NonNull @Nullable
// @Override @Override
// public Drawable provide() { public Drawable handleError(@Nullable String latex, @NonNull Throwable error) {
// return new ColorDrawable(0x40ff0000); Debug.e(error, latex);
// } return ContextCompat.getDrawable(LatexActivity.this, R.drawable.ic_android_black_24dp);
// }) }
// .fitCanvas(true) });
// .align(JLatexMathDrawable.ALIGN_LEFT) }))
// .padding(48) .build();
// ;
// } markwon.setMarkdown(textView, md);
// })) }
.usePlugin(JLatexMathPlugin.create(textView.getTextSize()))
private void legacy() {
final String md = wrapLatexInSampleMarkdown(LATEX_BANGLE);
final Markwon markwon = Markwon.builder(this)
// LEGACY does not require inline parser
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
builder.blocksLegacy(true);
builder.theme()
.backgroundProvider(() -> new ColorDrawable(0x100000ff))
.padding(JLatexMathTheme.Padding.all(48));
}))
.build();
markwon.setMarkdown(textView, md);
}
private void textColor() {
final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION);
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
builder.inlinesEnabled(true);
builder.theme()
.inlineTextColor(Color.RED)
.blockTextColor(Color.GREEN)
.inlineBackgroundProvider(() -> new ColorDrawable(Color.YELLOW))
.blockBackgroundProvider(() -> new ColorDrawable(Color.GRAY));
}))
.build();
markwon.setMarkdown(textView, md);
}
private void defaultTextColor() {
// @since 4.3.0 text color is automatically taken from textView
// (if it's not specified explicitly via configuration)
textView.setTextColor(0xFFff0000);
final String md = wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION);
final Markwon markwon = Markwon.builder(this)
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
@Override
public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
builder.inlinesEnabled(true);
// override default text color
builder.theme()
.inlineTextColor(0xFF00ffff);
}
}))
.build();
markwon.setMarkdown(textView, md);
}
private void inlineAndBlock() {
final String md = "" +
"# Inline and block\n\n" +
"$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$\n\n" +
"this was **inline** _LaTeX_ $$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$ and once again it was\n\n" +
"Now a block:\n\n" +
"$$\n" +
"\\int_{a}^{b} f(x)dx = F(b) - F(a)\n" +
"$$\n\n" +
"Not a block (content on delimited line), but inline instead:\n\n" +
"$$\\int_{a}^{b} f(x)dx = F(b) - F(a)$$" +
"\n\n" +
"that's it";
renderWithBlocksAndInlines(md);
}
private void dark() {
parent.setBackgroundColor(0xFF000000);
textView.setTextColor(0xFFffffff);
String latex = "W=W_1+W_2=F_1X_1-F_2X_2";
final String md = "" +
"# LaTeX inside a blockquote\n" +
"> $$" + latex + "$$\n";
renderWithBlocksAndInlines(md);
}
@NonNull
private static String wrapLatexInSampleMarkdown(@NonNull String latex) {
return "" +
"# Example of LaTeX\n\n" +
"(inline): $$" + latex + "$$ so nice, really-really really-really really-really? Now, (block):\n\n" +
"$$\n" +
"" + latex + "\n" +
"$$\n\n" +
"the end";
}
private void renderWithBlocksAndInlines(@NonNull String markdown) {
final float textSize = textView.getTextSize();
final Resources r = getResources();
final Markwon markwon = Markwon.builder(this)
// NB! `MarkwonInlineParserPlugin` is required in order to parse inlines
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(JLatexMathPlugin.create(textSize, textSize * 1.25F, builder -> {
// Important thing to do is to enable inlines (by default disabled)
builder.inlinesEnabled(true);
builder.theme()
.inlineBackgroundProvider(() -> new ColorDrawable(0x1000ff00))
.blockBackgroundProvider(() -> new ColorDrawable(0x10ff0000))
.blockPadding(JLatexMathTheme.Padding.symmetric(
r.getDimensionPixelSize(R.dimen.latex_block_padding_vertical),
r.getDimensionPixelSize(R.dimen.latex_block_padding_horizontal)
));
}))
.build(); .build();
//
// if (true) {
//// final String l = "$$\n" +
//// " P(X=r)=\\frac{\\lambda^r e^{-\\lambda}}{r!}\n" +
//// "$$\n" +
//// "\n" +
//// "$$\n" +
//// " P(X<r)=P(X<r-1)\n" +
//// "$$\n" +
//// "\n" +
//// "$$\n" +
//// " P(X>r)=1-P(X<r=1)\n" +
//// "$$\n" +
//// "\n" +
//// "$$\n" +
//// " \\text{Variance} = \\lambda\n" +
//// "$$";
// final String l = "$$ \n" +
// " \\sigma_T^2 = \\frac{1-p}{p^2}\n" +
// "$$";
// markwon.setMarkdown(textView, l);
// return;
// }
markwon.setMarkdown(textView, markdown); markwon.setMarkdown(textView, markdown);
} }

View File

@ -0,0 +1,254 @@
package io.noties.markwon.sample.notification;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BulletSpan;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;
import android.text.style.QuoteSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.Emphasis;
import org.commonmark.node.Heading;
import org.commonmark.node.ListItem;
import org.commonmark.node.StrongEmphasis;
import io.noties.debug.Debug;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
public class NotificationActivity extends ActivityWithMenuOptions {
private static final String CHANNEL_ID = "whatever";
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("bold-italic", this::bold_italic)
.add("heading", this::heading)
.add("lists", this::lists)
.add("image", this::image)
.add("link", this::link)
.add("blockquote", this::blockquote)
.add("strikethrough", this::strikethrough);
}
private void bold_italic() {
// Unfortunately we cannot just use Markwon created CharSequence in a RemoteViews context
// because it requires for spans to be platform ones
final String md = "Just a **bold** here and _italic_, but what if **it is bold _and italic_**?";
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder
.setFactory(StrongEmphasis.class, (configuration, props) -> new StyleSpan(Typeface.BOLD))
.setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC));
}
})
.build();
display(markwon.toMarkdown(md));
}
private void heading() {
// please note that heading doesn't seem to be working in remote views,
// tried both `RelativeSizeSpan` and `AbsoluteSizeSpan` with no effect
final float base = 12;
final float[] sizes = {
2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
};
final String md = "" +
"# H1\n" +
"## H2\n" +
"### H3\n" +
"#### H4\n" +
"##### H5\n" +
"###### H6\n\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Heading.class, (configuration, props) -> {
final Integer level = CoreProps.HEADING_LEVEL.get(props);
Debug.i(level);
if (level != null && level > 0 && level <= sizes.length) {
// return new RelativeSizeSpan(sizes[level - 1]);
final Object span = new AbsoluteSizeSpan((int) (base * sizes[level - 1] + .5F), true);
return new Object[]{
span,
new StyleSpan(Typeface.BOLD)
};
}
return null;
});
}
})
.build();
display(markwon.toMarkdown(md));
}
private void lists() {
final String md = "" +
"* bullet 1\n" +
"* bullet 2\n" +
"* * bullet 2 1\n" +
" * bullet 2 0 1\n" +
"1) order 1\n" +
"1) order 2\n" +
"1) order 3\n";
// ordered lists _could_ be translated to raw text representation (`1.`, `1)` etc) in resulting markdown
// or they could be _disabled_ all together... (can ordered lists be disabled in parser?)
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(ListItem.class, (configuration, props) -> {
final CoreProps.ListItemType type = CoreProps.LIST_ITEM_TYPE.get(props);
if (type != null) {
// bullet and ordered list share the same markdown node
return new BulletSpan();
}
return null;
});
}
})
.build();
display(markwon.toMarkdown(md));
}
private void image() {
// please note that image _could_ be supported only if it would be available immediately
// debugging possibility
//
// doesn't seem to be working
final Bitmap bitmap = Bitmap.createBitmap(128, 256, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.drawColor(0xFFAD1457);
final SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append("An image: ");
final int length = builder.length();
builder.append("[bitmap]");
builder.setSpan(
new ImageSpan(this, bitmap, DynamicDrawableSpan.ALIGN_BOTTOM),
length,
builder.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
builder.append(" okay, and ");
final int start = builder.length();
builder.append("[resource]");
builder.setSpan(
new ImageSpan(this, R.drawable.ic_memory_black_48dp),
start,
builder.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
display(builder);
}
private void link() {
final String md = "" +
"[a link](https://isa.link/) is here, styling yes, clicking - no";
display(Markwon.create(this).toMarkdown(md));
}
private void blockquote() {
final String md = "" +
"> This was once said by me\n" +
"> > And this one also\n\n" +
"Me";
final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan());
}
})
.build();
display(markwon.toMarkdown(md));
}
private void strikethrough() {
final String md = "~~strike that!~~";
final Markwon markwon = Markwon.builder(this)
.usePlugin(new StrikethroughPlugin())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan());
}
})
.build();
display(markwon.toMarkdown(md));
}
private void display(@NonNull CharSequence cs) {
final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (manager == null) {
throw new IllegalStateException("No NotificationManager is available");
}
ensureChannel(manager);
final Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_stat_name)
.setContentTitle("Markwon")
.setContentText(cs)
.setStyle(new Notification.BigTextStyle().bigText(cs));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CHANNEL_ID);
}
manager.notify(1, builder.build());
}
private void ensureChannel(@NonNull NotificationManager manager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
final NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID);
if (channel == null) {
manager.createNotificationChannel(new NotificationChannel(
CHANNEL_ID,
CHANNEL_ID,
NotificationManager.IMPORTANCE_DEFAULT));
}
}
}

View File

@ -0,0 +1,169 @@
package io.noties.markwon.sample.tasklist;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import java.util.Objects;
import io.noties.debug.Debug;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.ext.tasklist.TaskListItem;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.ext.tasklist.TaskListSpan;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
public class TaskListActivity extends ActivityWithMenuOptions {
private static final String MD = "" +
"- [ ] Not done here!\n" +
"- [x] and done\n" +
"- [X] and again!\n" +
"* [ ] **and** syntax _included_ `code`\n" +
"- [ ] [link](#)\n" +
"- [ ] [a check box](https://goog.le)\n" +
"- [x] [test]()\n" +
"- [List](https://goog.le) 3";
private TextView textView;
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("regular", this::regular)
.add("customColors", this::customColors)
.add("customDrawableResources", this::customDrawableResources)
.add("mutate", this::mutate);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = findViewById(R.id.text_view);
// mutate();
regular();
}
private void regular() {
// default theme
final Markwon markwon = Markwon.builder(this)
.usePlugin(TaskListPlugin.create(this))
.build();
markwon.setMarkdown(textView, MD);
}
private void customColors() {
final int checkedFillColor = Color.RED;
final int normalOutlineColor = Color.GREEN;
final int checkMarkColor = Color.BLUE;
final Markwon markwon = Markwon.builder(this)
.usePlugin(TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor))
.build();
markwon.setMarkdown(textView, MD);
}
private void customDrawableResources() {
// drawable **must** be stateful
final Drawable drawable = Objects.requireNonNull(
ContextCompat.getDrawable(this, R.drawable.custom_task_list));
final Markwon markwon = Markwon.builder(this)
.usePlugin(TaskListPlugin.create(drawable))
.build();
markwon.setMarkdown(textView, MD);
}
private void mutate() {
final Markwon markwon = Markwon.builder(this)
.usePlugin(TaskListPlugin.create(this))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// obtain origin task-list-factory
final SpanFactory origin = builder.getFactory(TaskListItem.class);
if (origin == null) {
return;
}
builder.setFactory(TaskListItem.class, (configuration, props) -> {
// maybe it's better to validate the actual type here also
// and not force cast to task-list-span
final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
if (span == null) {
return null;
}
// NB, toggle click will intercept possible links inside task-list-item
return new Object[]{
span,
new TaskListToggleSpan(span)
};
});
}
})
.build();
markwon.setMarkdown(textView, MD);
}
private static class TaskListToggleSpan extends ClickableSpan {
private final TaskListSpan span;
TaskListToggleSpan(@NonNull TaskListSpan span) {
this.span = span;
}
@Override
public void onClick(@NonNull View widget) {
// toggle span (this is a mere visual change)
span.setDone(!span.isDone());
// request visual update
widget.invalidate();
// it must be a TextView
final TextView textView = (TextView) widget;
// it must be spanned
final Spanned spanned = (Spanned) textView.getText();
// actual text of the span (this can be used along with the `span`)
final CharSequence task = spanned.subSequence(
spanned.getSpanStart(this),
spanned.getSpanEnd(this)
);
Debug.i("task done: %s, '%s'", span.isDone(), task);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
// no op, so text is not rendered as a link
}
}
}

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="26.086956"
android:viewportHeight="26.086956"
android:tint="#FFFFFF">
<group android:translateX="1.0434783"
android:translateY="1.0434783">
<path
android:fillColor="#FF000000"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/ic_android_black_24dp" />
<item android:drawable="@drawable/ic_home_black_36dp" />
</selector>

View File

@ -1,13 +1,16 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="8dip">
<TextView <TextView
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"

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="latex_block_padding_vertical">8dip</dimen>
<dimen name="latex_block_padding_horizontal">16dip</dimen>
</resources>

View File

@ -29,6 +29,12 @@
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string> <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> <string name="sample_html_details"># \# HTML\n\n`details` tag parsed and rendered</string>
<string name="sample_task_list"># \# TaskList\n\nUsage of TaskListPlugin</string>
<string name="sample_images"># \# Images\n\nUsage of different images plugins</string>
<string name="sample_remote_views"># \# Notification\n\nExample usage in notifications and other remote views</string>
</resources> </resources>

View File

@ -12,4 +12,16 @@ Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64
]]> ]]>
</string> </string>
<string name="lorem"><![CDATA[
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.
Sed eu enim neque. Maecenas dictum faucibus ullamcorper. In ullamcorper orci in neque varius, nec rutrum nisl eleifend. Vestibulum tincidunt, ipsum at porta suscipit, est nibh commodo ex, et ultrices eros lacus vel neque. Praesent nulla velit, hendrerit sed sodales at, feugiat non lectus. Vivamus vel ultricies mi. Ut finibus commodo feugiat. Sed tempor lorem tortor, tempor sodales leo varius id. Curabitur rutrum sem at euismod rhoncus. Ut iaculis sem pharetra neque accumsan vestibulum. Nunc ultrices pharetra massa, at luctus nulla maximus et. Donec rhoncus in nisi eu pellentesque.
Sed consequat convallis massa quis bibendum. Phasellus vel suscipit velit. Pellentesque vel nisi at nisi facilisis condimentum. Cras feugiat magna ex, ut ultricies eros porttitor id. Quisque iaculis rutrum arcu eget placerat. Vestibulum pellentesque, urna eget consectetur commodo, est metus gravida nisl, id lacinia ligula ipsum porta nulla. Etiam aliquam convallis sollicitudin. Etiam sit amet mi aliquet purus faucibus hendrerit pharetra eu quam. Cras ut ornare sapien. Nam sapien diam, porttitor eu sagittis nec, vehicula nec mi. In fringilla turpis nec nisi fringilla, a facilisis eros ultrices. Proin eget arcu velit.
Sed gravida auctor malesuada. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus non justo sem. Donec dictum a elit quis pretium. Fusce accumsan sodales ornare. Nunc facilisis ligula eu ultrices faucibus. Proin vel molestie augue, ut convallis enim. Curabitur efficitur eget urna quis tempor. In non arcu non ex vulputate pulvinar. In laoreet aliquam mauris. Suspendisse vulputate magna at lorem bibendum, quis dapibus sapien malesuada. Curabitur at leo sit amet est egestas vestibulum. Sed hendrerit mi vel massa vestibulum, non semper nisl iaculis. Pellentesque feugiat at dolor a viverra. Sed ut consectetur tellus. Maecenas venenatis nunc a arcu convallis, at semper nulla cursus.
Curabitur placerat neque a congue pulvinar. Nulla non commodo est. Aenean nec gravida odio. Cras tincidunt accumsan pulvinar. Vestibulum non imperdiet velit. Sed ut mollis velit, vel ornare metus. Morbi consequat mi quis dui consectetur, sed condimentum lacus pulvinar.
]]></string>
</resources> </resources>