commit
7baa70b15e
54
CHANGELOG.md
54
CHANGELOG.md
@ -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` -> `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`)
|
||||||
|
@ -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',
|
||||||
|
BIN
docs/.vuepress/public/assets/apps/purewriter.png
Normal file
BIN
docs/.vuepress/public/assets/apps/purewriter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -109,7 +109,8 @@ and 2 themes included: Light & 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'}
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
|
```
|
@ -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)
|
||||||
:::
|
:::
|
@ -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;
|
||||||
|
@ -5,8 +5,18 @@ next: /docs/v4/core/getting-started.md
|
|||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||

|
<table>
|
||||||

|
<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 />
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 + '\'';
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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>() {
|
||||||
|
@ -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']
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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']
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 = "";
|
final String markdown = "";
|
||||||
|
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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](#)";
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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" +
|
||||||
|
"\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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = "[](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 = "[](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 = "[](https://www.mdeditor.com/images/logos/markdown.png)";
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.build();
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml
Normal file
13
sample/src/main/res/drawable-anydpi-v24/ic_stat_name.xml
Normal 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>
|
BIN
sample/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
BIN
sample/src/main/res/drawable-hdpi/ic_stat_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 513 B |
BIN
sample/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
BIN
sample/src/main/res/drawable-mdpi/ic_stat_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 346 B |
BIN
sample/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
BIN
sample/src/main/res/drawable-xhdpi/ic_stat_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 687 B |
BIN
sample/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
BIN
sample/src/main/res/drawable-xxhdpi/ic_stat_name.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
5
sample/src/main/res/drawable/custom_task_list.xml
Normal file
5
sample/src/main/res/drawable/custom_task_list.xml
Normal 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>
|
@ -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"
|
||||||
|
5
sample/src/main/res/values/dimens.xml
Normal file
5
sample/src/main/res/values/dimens.xml
Normal 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>
|
@ -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 <details> tag\n\n<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>
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user