commit
710bcbcc1d
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,5 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 4.4.0
|
||||||
|
* `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`)
|
||||||
|
* `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed)
|
||||||
|
* `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235])
|
||||||
|
* `AsyncDrawable` now uses `TextView` width without padding instead of width of canvas
|
||||||
|
* Support for images inside table cells (`ext-tables` module)
|
||||||
|
* Expose `enabledBlockTypes` in `CorePlugin`
|
||||||
|
* Update `jlatexmath-android` dependency ([#225])
|
||||||
|
* Update `image-coil` module (Coil version `0.10.1`) ([#244])<br>Thanks to [@tylerbwong]
|
||||||
|
* Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -> `io.noties.markwon.image.destination`) and limit its usage to process **only** destination URL of images (was used to also process links before)
|
||||||
|
* `fallbackToRawInputWhenEmpty` `Markwon.Builder` configuration to fallback to raw input if rendered markdown is empty ([#242])
|
||||||
|
|
||||||
|
[#235]: https://github.com/noties/Markwon/issues/235
|
||||||
|
[#225]: https://github.com/noties/Markwon/issues/225
|
||||||
|
[#244]: https://github.com/noties/Markwon/pull/244
|
||||||
|
[#242]: https://github.com/noties/Markwon/issues/242
|
||||||
|
[@tylerbwong]: https://github.com/tylerbwong
|
||||||
|
|
||||||
|
|
||||||
# 4.3.1
|
# 4.3.1
|
||||||
* Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone]
|
* Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone]
|
||||||
* module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name
|
* module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name
|
||||||
|
@ -5,15 +5,15 @@ import android.text.TextUtils;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessor;
|
import io.noties.markwon.image.destination.ImageDestinationProcessor;
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute;
|
||||||
|
|
||||||
class UrlProcessorInitialReadme implements UrlProcessor {
|
class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor {
|
||||||
|
|
||||||
private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/";
|
private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/";
|
||||||
|
|
||||||
private final UrlProcessorRelativeToAbsolute processor
|
private final ImageDestinationProcessorRelativeToAbsolute processor
|
||||||
= new UrlProcessorRelativeToAbsolute(GITHUB_BASE);
|
= new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
@ -24,6 +24,8 @@ import io.noties.markwon.ext.tables.TablePlugin;
|
|||||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||||
import io.noties.markwon.html.HtmlPlugin;
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
import io.noties.markwon.image.ImagesPlugin;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.image.destination.ImageDestinationProcessor;
|
||||||
|
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute;
|
||||||
import io.noties.markwon.image.file.FileSchemeHandler;
|
import io.noties.markwon.image.file.FileSchemeHandler;
|
||||||
import io.noties.markwon.image.gif.GifMediaDecoder;
|
import io.noties.markwon.image.gif.GifMediaDecoder;
|
||||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
||||||
@ -31,8 +33,6 @@ import io.noties.markwon.syntax.Prism4jTheme;
|
|||||||
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||||
import io.noties.markwon.syntax.Prism4jThemeDefault;
|
import io.noties.markwon.syntax.Prism4jThemeDefault;
|
||||||
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
|
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessor;
|
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
|
||||||
import io.noties.prism4j.Prism4j;
|
import io.noties.prism4j.Prism4j;
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
@ -86,11 +86,11 @@ public class MarkdownRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void execute() {
|
private void execute() {
|
||||||
final UrlProcessor urlProcessor;
|
final ImageDestinationProcessor imageDestinationProcessor;
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
urlProcessor = new UrlProcessorInitialReadme();
|
imageDestinationProcessor = new ImageDestinationProcessorInitialReadme();
|
||||||
} else {
|
} else {
|
||||||
urlProcessor = new UrlProcessorRelativeToAbsolute(uri.toString());
|
imageDestinationProcessor = new ImageDestinationProcessorRelativeToAbsolute(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
final Prism4jTheme prism4jTheme = isLightTheme
|
final Prism4jTheme prism4jTheme = isLightTheme
|
||||||
@ -119,7 +119,7 @@ public class MarkdownRenderer {
|
|||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
@Override
|
@Override
|
||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||||
builder.urlProcessor(urlProcessor);
|
builder.imageDestinationProcessor(imageDestinationProcessor);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
@ -72,7 +72,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.1',
|
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.2.0',
|
||||||
'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',
|
||||||
@ -80,7 +80,7 @@ ext {
|
|||||||
'dagger' : "com.google.dagger:dagger:$daggerVersion",
|
'dagger' : "com.google.dagger:dagger:$daggerVersion",
|
||||||
'picasso' : 'com.squareup.picasso:picasso:2.71828',
|
'picasso' : 'com.squareup.picasso:picasso:2.71828',
|
||||||
'glide' : 'com.github.bumptech.glide:glide:4.9.0',
|
'glide' : 'com.github.bumptech.glide:glide:4.9.0',
|
||||||
'coil' : 'io.coil-kt:coil:0.8.0'
|
'coil' : 'io.coil-kt:coil:0.10.1'
|
||||||
]
|
]
|
||||||
|
|
||||||
deps['annotationProcessor'] = [
|
deps['annotationProcessor'] = [
|
||||||
|
@ -99,12 +99,7 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht
|
|||||||
## # Awesome Markwon
|
## # Awesome Markwon
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<br>
|
||||||
<u>Applications using Markwon</u>:
|
|
||||||
|
|
||||||
* [Partico](https://partiko.app/) - Partiko is a censorship free social network.
|
|
||||||
* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas.
|
|
||||||
* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds.
|
|
||||||
|
|
||||||
<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.'},
|
||||||
@ -113,6 +108,11 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht
|
|||||||
{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'}
|
{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'}
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
|
* [Partico](https://partiko.app/) - Partiko is a censorship free social network.
|
||||||
|
* [FairNote](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote) - simple and intuitive notepad app. It gives you speed and efficiency when you write notes, to-do lists, e-mails, or jot down quick ideas.
|
||||||
|
* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds.
|
||||||
|
* [Senstone Portable Voice Assistant](https://play.google.com/store/apps/details?id=com.senstone) - Senstone is a tiny wearable personal assistant powered by this App. It lets you capture your ideas, notes and reminders handsfree without pulling out your phone.
|
||||||
|
|
||||||
|
|
||||||
<u>Extension/plugins</u>:
|
<u>Extension/plugins</u>:
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ These are _configurable_ properties:
|
|||||||
* `AsyncDrawableLoader` (back here since <Badge text="4.0.0" />)
|
* `AsyncDrawableLoader` (back here since <Badge text="4.0.0" />)
|
||||||
* `SyntaxHighlight`
|
* `SyntaxHighlight`
|
||||||
* `LinkResolver` (since <Badge text="4.0.0" />, before — `LinkSpan.Resolver`)
|
* `LinkResolver` (since <Badge text="4.0.0" />, before — `LinkSpan.Resolver`)
|
||||||
* `UrlProcessor`
|
* `ImageDestinationProcessor` (since <Badge text="4.4.0" />, before — `UrlProcessor`)
|
||||||
* `ImageSizeResolver`
|
* `ImageSizeResolver`
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
@ -36,10 +36,11 @@ final Markwon markwon = Markwon.builder(context)
|
|||||||
.build();
|
.build();
|
||||||
```
|
```
|
||||||
|
|
||||||
Currently `Markwon` provides 3 implementations for loading images:
|
Currently `Markwon` provides 4 implementations for loading images:
|
||||||
* [markwon implementation](/docs/v4/image/) with SVG, GIF, data uri and android_assets support
|
* [markwon implementation](/docs/v4/image/) with SVG, GIF, data uri and android_assets support
|
||||||
* [based on Picasso](/docs/v4/image-picasso/)
|
* [based on Picasso](/docs/v4/image-picasso/)
|
||||||
* [based on Glide](/docs/v4/image-glide/)
|
* [based on Glide](/docs/v4/image-glide/)
|
||||||
|
* [base on Coil](/docs/v4/image-coil/)
|
||||||
|
|
||||||
## SyntaxHighlight
|
## SyntaxHighlight
|
||||||
|
|
||||||
@ -87,32 +88,32 @@ if there is none registered. if you wish to register own instance of a `Movement
|
|||||||
apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md)
|
apply it directly to a TextView or use [MovementMethodPlugin](/docs/v4/core/movement-method-plugin.md)
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## UrlProcessor
|
## ImageDestinationProcessor
|
||||||
|
|
||||||
Process URLs in your markdown (for links and images). If not provided explicitly,
|
Process destinations (URLs) of images in your markdown. If not provided explicitly,
|
||||||
default **no-op** implementation will be used, which does not modify URLs (keeping them as-is).
|
default **no-op** implementation will be used, which does not modify URLs (keeping them as-is).
|
||||||
|
|
||||||
`Markwon` provides 2 implementations of `UrlProcessor`:
|
`Markwon` provides 2 implementations of `UrlProcessor`:
|
||||||
* `UrlProcessorRelativeToAbsolute`
|
* `ImageDestinationProcessorRelativeToAbsolute`
|
||||||
* `UrlProcessorAndroidAssets`
|
* `ImageDestinationProcessorAssets`
|
||||||
|
|
||||||
### UrlProcessorRelativeToAbsolute
|
### ImageDestinationProcessorRelativeToAbsolute
|
||||||
|
|
||||||
`UrlProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is
|
`ImageDestinationProcessorRelativeToAbsolute` can be used to make relative URL absolute. For example if an image is
|
||||||
defined like this: `` and `UrlProcessorRelativeToAbsolute`
|
defined like this: `` and `ImageDestinationProcessorRelativeToAbsolute`
|
||||||
is created with `https://github.com/noties/Markwon/raw/master/` as the base:
|
is created with `https://github.com/noties/Markwon/raw/master/` as the base:
|
||||||
`new UrlProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`,
|
`new ImageDestinationProcessorRelativeToAbsolute("https://github.com/noties/Markwon/raw/master/")`,
|
||||||
then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG`
|
then final image will have `https://github.com/noties/Markwon/raw/master/art/image.JPG`
|
||||||
as the destination.
|
as the destination.
|
||||||
|
|
||||||
### UrlProcessorAndroidAssets
|
### ImageDestinationProcessorAssets
|
||||||
|
|
||||||
`UrlProcessorAndroidAssets` can be used to make processed links to point to Android assets folder.
|
`ImageDestinationProcessorAssets` can be used to make processed destinations to point to Android assets folder.
|
||||||
So an image: `` will have `file:///android_asset/art/image.JPG` as the
|
So an image: `` will have `file:///android_asset/art/image.JPG` as the
|
||||||
destination.
|
destination.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
Please note that `UrlProcessorAndroidAssets` will process only URLs that have no `scheme` information,
|
Please note that `ImageDestinationProcessorAssets` will process only URLs that have no `scheme` information,
|
||||||
so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
|
so a `./art/image.png` will become `file:///android_asset/art/image.JPG` whilst `https://so.me/where.png`
|
||||||
will be kept as-is.
|
will be kept as-is.
|
||||||
:::
|
:::
|
||||||
|
@ -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.3.1
|
VERSION_NAME=4.4.0
|
||||||
|
|
||||||
GROUP=io.noties.markwon
|
GROUP=io.noties.markwon
|
||||||
POM_DESCRIPTION=Markwon markdown for Android
|
POM_DESCRIPTION=Markwon markdown for Android
|
||||||
|
@ -192,6 +192,17 @@ public abstract class Markwon {
|
|||||||
@NonNull
|
@NonNull
|
||||||
Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins);
|
Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control if small chunks of non-finished markdown sentences (for example, a single `*` character)
|
||||||
|
* should be displayed/rendered as raw input instead of an empty string.
|
||||||
|
* <p>
|
||||||
|
* Since 4.4.0 {@code true} by default, versions prior - {@code false}
|
||||||
|
*
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Markwon build();
|
Markwon build();
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,9 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
|
|
||||||
private Markwon.TextSetter textSetter;
|
private Markwon.TextSetter textSetter;
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
private boolean fallbackToRawInputWhenEmpty = true;
|
||||||
|
|
||||||
MarkwonBuilderImpl(@NonNull Context context) {
|
MarkwonBuilderImpl(@NonNull Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
@ -71,6 +74,13 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Markwon.Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty) {
|
||||||
|
this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Markwon build() {
|
public Markwon build() {
|
||||||
@ -114,7 +124,8 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
|||||||
parserBuilder.build(),
|
parserBuilder.build(),
|
||||||
visitorFactory,
|
visitorFactory,
|
||||||
configuration,
|
configuration,
|
||||||
Collections.unmodifiableList(plugins)
|
Collections.unmodifiableList(plugins),
|
||||||
|
fallbackToRawInputWhenEmpty
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,15 +6,13 @@ import io.noties.markwon.core.MarkwonTheme;
|
|||||||
import io.noties.markwon.image.AsyncDrawableLoader;
|
import io.noties.markwon.image.AsyncDrawableLoader;
|
||||||
import io.noties.markwon.image.ImageSizeResolver;
|
import io.noties.markwon.image.ImageSizeResolver;
|
||||||
import io.noties.markwon.image.ImageSizeResolverDef;
|
import io.noties.markwon.image.ImageSizeResolverDef;
|
||||||
|
import io.noties.markwon.image.destination.ImageDestinationProcessor;
|
||||||
import io.noties.markwon.syntax.SyntaxHighlight;
|
import io.noties.markwon.syntax.SyntaxHighlight;
|
||||||
import io.noties.markwon.syntax.SyntaxHighlightNoOp;
|
import io.noties.markwon.syntax.SyntaxHighlightNoOp;
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessor;
|
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessorNoOp;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration`
|
* since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration`
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
public class MarkwonConfiguration {
|
public class MarkwonConfiguration {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -26,7 +24,8 @@ public class MarkwonConfiguration {
|
|||||||
private final AsyncDrawableLoader asyncDrawableLoader;
|
private final AsyncDrawableLoader asyncDrawableLoader;
|
||||||
private final SyntaxHighlight syntaxHighlight;
|
private final SyntaxHighlight syntaxHighlight;
|
||||||
private final LinkResolver linkResolver;
|
private final LinkResolver linkResolver;
|
||||||
private final UrlProcessor urlProcessor;
|
// @since 4.4.0
|
||||||
|
private final ImageDestinationProcessor imageDestinationProcessor;
|
||||||
private final ImageSizeResolver imageSizeResolver;
|
private final ImageSizeResolver imageSizeResolver;
|
||||||
|
|
||||||
// @since 3.0.0
|
// @since 3.0.0
|
||||||
@ -37,7 +36,7 @@ public class MarkwonConfiguration {
|
|||||||
this.asyncDrawableLoader = builder.asyncDrawableLoader;
|
this.asyncDrawableLoader = builder.asyncDrawableLoader;
|
||||||
this.syntaxHighlight = builder.syntaxHighlight;
|
this.syntaxHighlight = builder.syntaxHighlight;
|
||||||
this.linkResolver = builder.linkResolver;
|
this.linkResolver = builder.linkResolver;
|
||||||
this.urlProcessor = builder.urlProcessor;
|
this.imageDestinationProcessor = builder.imageDestinationProcessor;
|
||||||
this.imageSizeResolver = builder.imageSizeResolver;
|
this.imageSizeResolver = builder.imageSizeResolver;
|
||||||
this.spansFactory = builder.spansFactory;
|
this.spansFactory = builder.spansFactory;
|
||||||
}
|
}
|
||||||
@ -62,9 +61,12 @@ public class MarkwonConfiguration {
|
|||||||
return linkResolver;
|
return linkResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public UrlProcessor urlProcessor() {
|
public ImageDestinationProcessor imageDestinationProcessor() {
|
||||||
return urlProcessor;
|
return imageDestinationProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -87,7 +89,8 @@ public class MarkwonConfiguration {
|
|||||||
private AsyncDrawableLoader asyncDrawableLoader;
|
private AsyncDrawableLoader asyncDrawableLoader;
|
||||||
private SyntaxHighlight syntaxHighlight;
|
private SyntaxHighlight syntaxHighlight;
|
||||||
private LinkResolver linkResolver;
|
private LinkResolver linkResolver;
|
||||||
private UrlProcessor urlProcessor;
|
// @since 4.4.0
|
||||||
|
private ImageDestinationProcessor imageDestinationProcessor;
|
||||||
private ImageSizeResolver imageSizeResolver;
|
private ImageSizeResolver imageSizeResolver;
|
||||||
private MarkwonSpansFactory spansFactory;
|
private MarkwonSpansFactory spansFactory;
|
||||||
|
|
||||||
@ -115,9 +118,12 @@ public class MarkwonConfiguration {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public Builder urlProcessor(@NonNull UrlProcessor urlProcessor) {
|
public Builder imageDestinationProcessor(@NonNull ImageDestinationProcessor imageDestinationProcessor) {
|
||||||
this.urlProcessor = urlProcessor;
|
this.imageDestinationProcessor = imageDestinationProcessor;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,8 +157,9 @@ public class MarkwonConfiguration {
|
|||||||
linkResolver = new LinkResolverDef();
|
linkResolver = new LinkResolverDef();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlProcessor == null) {
|
// @since 4.4.0
|
||||||
urlProcessor = new UrlProcessorNoOp();
|
if (imageDestinationProcessor == null) {
|
||||||
|
imageDestinationProcessor = ImageDestinationProcessor.noOp();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageSizeResolver == null) {
|
if (imageSizeResolver == null) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package io.noties.markwon;
|
package io.noties.markwon;
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -28,19 +30,25 @@ class MarkwonImpl extends Markwon {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final TextSetter textSetter;
|
private final TextSetter textSetter;
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
private final boolean fallbackToRawInputWhenEmpty;
|
||||||
|
|
||||||
MarkwonImpl(
|
MarkwonImpl(
|
||||||
@NonNull TextView.BufferType bufferType,
|
@NonNull TextView.BufferType bufferType,
|
||||||
@Nullable TextSetter textSetter,
|
@Nullable TextSetter textSetter,
|
||||||
@NonNull Parser parser,
|
@NonNull Parser parser,
|
||||||
@NonNull MarkwonVisitorFactory visitorFactory,
|
@NonNull MarkwonVisitorFactory visitorFactory,
|
||||||
@NonNull MarkwonConfiguration configuration,
|
@NonNull MarkwonConfiguration configuration,
|
||||||
@NonNull List<MarkwonPlugin> plugins) {
|
@NonNull List<MarkwonPlugin> plugins,
|
||||||
|
boolean fallbackToRawInputWhenEmpty
|
||||||
|
) {
|
||||||
this.bufferType = bufferType;
|
this.bufferType = bufferType;
|
||||||
this.textSetter = textSetter;
|
this.textSetter = textSetter;
|
||||||
this.parser = parser;
|
this.parser = parser;
|
||||||
this.visitorFactory = visitorFactory;
|
this.visitorFactory = visitorFactory;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.plugins = plugins;
|
this.plugins = plugins;
|
||||||
|
this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -86,7 +94,18 @@ class MarkwonImpl extends Markwon {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Spanned toMarkdown(@NonNull String input) {
|
public Spanned toMarkdown(@NonNull String input) {
|
||||||
return render(parse(input));
|
final Spanned spanned = render(parse(input));
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
// if spanned is empty, we are configured to use raw input and input is not empty
|
||||||
|
if (TextUtils.isEmpty(spanned)
|
||||||
|
&& fallbackToRawInputWhenEmpty
|
||||||
|
&& !TextUtils.isEmpty(input)) {
|
||||||
|
// let's use SpannableStringBuilder in order to keep backward-compatibility
|
||||||
|
return new SpannableStringBuilder(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spanned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package io.noties.markwon.core;
|
package io.noties.markwon.core;
|
||||||
|
|
||||||
|
import android.text.Spannable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@ -8,6 +9,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import org.commonmark.node.Block;
|
||||||
import org.commonmark.node.BlockQuote;
|
import org.commonmark.node.BlockQuote;
|
||||||
import org.commonmark.node.BulletList;
|
import org.commonmark.node.BulletList;
|
||||||
import org.commonmark.node.Code;
|
import org.commonmark.node.Code;
|
||||||
@ -15,6 +17,7 @@ import org.commonmark.node.Emphasis;
|
|||||||
import org.commonmark.node.FencedCodeBlock;
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
import org.commonmark.node.HardLineBreak;
|
import org.commonmark.node.HardLineBreak;
|
||||||
import org.commonmark.node.Heading;
|
import org.commonmark.node.Heading;
|
||||||
|
import org.commonmark.node.HtmlBlock;
|
||||||
import org.commonmark.node.Image;
|
import org.commonmark.node.Image;
|
||||||
import org.commonmark.node.IndentedCodeBlock;
|
import org.commonmark.node.IndentedCodeBlock;
|
||||||
import org.commonmark.node.Link;
|
import org.commonmark.node.Link;
|
||||||
@ -29,7 +32,10 @@ import org.commonmark.node.Text;
|
|||||||
import org.commonmark.node.ThematicBreak;
|
import org.commonmark.node.ThematicBreak;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.MarkwonConfiguration;
|
import io.noties.markwon.MarkwonConfiguration;
|
||||||
@ -48,6 +54,7 @@ import io.noties.markwon.core.factory.ListItemSpanFactory;
|
|||||||
import io.noties.markwon.core.factory.StrongEmphasisSpanFactory;
|
import io.noties.markwon.core.factory.StrongEmphasisSpanFactory;
|
||||||
import io.noties.markwon.core.factory.ThematicBreakSpanFactory;
|
import io.noties.markwon.core.factory.ThematicBreakSpanFactory;
|
||||||
import io.noties.markwon.core.spans.OrderedListItemSpan;
|
import io.noties.markwon.core.spans.OrderedListItemSpan;
|
||||||
|
import io.noties.markwon.core.spans.TextViewSpan;
|
||||||
import io.noties.markwon.image.ImageProps;
|
import io.noties.markwon.image.ImageProps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +95,23 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
|||||||
return new CorePlugin();
|
return new CorePlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a set with enabled by default block types
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static Set<Class<? extends Block>> enabledBlockTypes() {
|
||||||
|
return new HashSet<>(Arrays.asList(
|
||||||
|
BlockQuote.class,
|
||||||
|
Heading.class,
|
||||||
|
FencedCodeBlock.class,
|
||||||
|
HtmlBlock.class,
|
||||||
|
ThematicBreak.class,
|
||||||
|
ListBlock.class,
|
||||||
|
IndentedCodeBlock.class
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// @since 4.0.0
|
// @since 4.0.0
|
||||||
private final List<OnTextAddedListener> onTextAddedListeners = new ArrayList<>(0);
|
private final List<OnTextAddedListener> onTextAddedListeners = new ArrayList<>(0);
|
||||||
|
|
||||||
@ -150,6 +174,13 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
|||||||
@Override
|
@Override
|
||||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||||
OrderedListItemSpan.measure(textView, markdown);
|
OrderedListItemSpan.measure(textView, markdown);
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
// we do not break API compatibility, instead we introduce the `instance of` check
|
||||||
|
if (markdown instanceof Spannable) {
|
||||||
|
final Spannable spannable = (Spannable) markdown;
|
||||||
|
TextViewSpan.applyTo(spannable, textView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -289,7 +320,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
|||||||
final boolean link = parent instanceof Link;
|
final boolean link = parent instanceof Link;
|
||||||
|
|
||||||
final String destination = configuration
|
final String destination = configuration
|
||||||
.urlProcessor()
|
.imageDestinationProcessor()
|
||||||
.process(image.getDestination());
|
.process(image.getDestination());
|
||||||
|
|
||||||
final RenderProps props = visitor.renderProps();
|
final RenderProps props = visitor.renderProps();
|
||||||
@ -493,8 +524,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
|||||||
final int length = visitor.length();
|
final int length = visitor.length();
|
||||||
visitor.visitChildren(link);
|
visitor.visitChildren(link);
|
||||||
|
|
||||||
final MarkwonConfiguration configuration = visitor.configuration();
|
final String destination = link.getDestination();
|
||||||
final String destination = configuration.urlProcessor().process(link.getDestination());
|
|
||||||
|
|
||||||
CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination);
|
CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination);
|
||||||
|
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package io.noties.markwon.core.spans;
|
||||||
|
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public class TextLayoutSpan {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #applyTo(Spannable, Layout)
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Layout layoutOf(@NonNull CharSequence cs) {
|
||||||
|
if (cs instanceof Spanned) {
|
||||||
|
return layoutOf((Spanned) cs);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Layout layoutOf(@NonNull Spanned spanned) {
|
||||||
|
final TextLayoutSpan[] spans = spanned.getSpans(
|
||||||
|
0,
|
||||||
|
spanned.length(),
|
||||||
|
TextLayoutSpan.class
|
||||||
|
);
|
||||||
|
return spans != null && spans.length > 0
|
||||||
|
? spans[0].layout()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyTo(@NonNull Spannable spannable, @NonNull Layout layout) {
|
||||||
|
|
||||||
|
// remove all current ones (only one should be present)
|
||||||
|
final TextLayoutSpan[] spans = spannable.getSpans(0, spannable.length(), TextLayoutSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
for (TextLayoutSpan span : spans) {
|
||||||
|
spannable.removeSpan(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final TextLayoutSpan span = new TextLayoutSpan(layout);
|
||||||
|
spannable.setSpan(
|
||||||
|
span,
|
||||||
|
0,
|
||||||
|
spannable.length(),
|
||||||
|
Spanned.SPAN_INCLUSIVE_INCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final WeakReference<Layout> reference;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
TextLayoutSpan(@NonNull Layout layout) {
|
||||||
|
this.reference = new WeakReference<>(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Layout layout() {
|
||||||
|
return reference.get();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package io.noties.markwon.core.spans;
|
||||||
|
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special span that allows to obtain {@code TextView} in which spans are displayed
|
||||||
|
*
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public class TextViewSpan {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static TextView textViewOf(@NonNull CharSequence cs) {
|
||||||
|
if (cs instanceof Spanned) {
|
||||||
|
return textViewOf((Spanned) cs);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static TextView textViewOf(@NonNull Spanned spanned) {
|
||||||
|
final TextViewSpan[] spans = spanned.getSpans(0, spanned.length(), TextViewSpan.class);
|
||||||
|
return spans != null && spans.length > 0
|
||||||
|
? spans[0].textView()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyTo(@NonNull Spannable spannable, @NonNull TextView textView) {
|
||||||
|
|
||||||
|
final TextViewSpan[] spans = spannable.getSpans(0, spannable.length(), TextViewSpan.class);
|
||||||
|
if (spans != null) {
|
||||||
|
for (TextViewSpan span : spans) {
|
||||||
|
spannable.removeSpan(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final TextViewSpan span = new TextViewSpan(textView);
|
||||||
|
// `SPAN_INCLUSIVE_INCLUSIVE` to persist in case of possible text change (deletion, etc)
|
||||||
|
spannable.setSpan(
|
||||||
|
span,
|
||||||
|
0,
|
||||||
|
spannable.length(),
|
||||||
|
Spanned.SPAN_INCLUSIVE_INCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final WeakReference<TextView> reference;
|
||||||
|
|
||||||
|
public TextViewSpan(@NonNull TextView textView) {
|
||||||
|
this.reference = new WeakReference<>(textView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public TextView textView() {
|
||||||
|
return reference.get();
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
import io.noties.markwon.core.MarkwonTheme;
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.utils.SpanUtils;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
public class AsyncDrawableSpan extends ReplacementSpan {
|
public class AsyncDrawableSpan extends ReplacementSpan {
|
||||||
@ -99,7 +100,11 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
|||||||
int bottom,
|
int bottom,
|
||||||
@NonNull Paint paint) {
|
@NonNull Paint paint) {
|
||||||
|
|
||||||
drawable.initWithKnownDimensions(canvas.getWidth(), paint.getTextSize());
|
// @since 4.4.0 use SpanUtils instead of `canvas.getWidth`
|
||||||
|
drawable.initWithKnownDimensions(
|
||||||
|
SpanUtils.width(canvas, text),
|
||||||
|
paint.getTextSize()
|
||||||
|
);
|
||||||
|
|
||||||
final AsyncDrawable drawable = this.drawable;
|
final AsyncDrawable drawable = this.drawable;
|
||||||
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package io.noties.markwon.image.destination;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process destination of image nodes
|
||||||
|
*
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public abstract class ImageDestinationProcessor {
|
||||||
|
@NonNull
|
||||||
|
public abstract String process(@NonNull String destination);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ImageDestinationProcessor noOp() {
|
||||||
|
return new NoOp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoOp extends ImageDestinationProcessor {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String process(@NonNull String destination) {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package io.noties.markwon.image.destination;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ImageDestinationProcessor} that treats all destinations <strong>without scheme</strong>
|
||||||
|
* information as pointing to the {@code assets} folder of an application. Please note that this
|
||||||
|
* processor only adds required {@code file:///android_asset/} prefix to destinations and
|
||||||
|
* actual image loading must take that into account (implement this functionality).
|
||||||
|
* <p>
|
||||||
|
* {@code FileSchemeHandler} from the {@code image} module supports asset images when created with
|
||||||
|
* {@code createWithAssets} factory method
|
||||||
|
*
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public class ImageDestinationProcessorAssets extends ImageDestinationProcessor {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ImageDestinationProcessorAssets create(@Nullable ImageDestinationProcessor parent) {
|
||||||
|
return new ImageDestinationProcessorAssets(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final String MOCK = "https://android.asset/";
|
||||||
|
static final String BASE = "file:///android_asset/";
|
||||||
|
|
||||||
|
private final ImageDestinationProcessorRelativeToAbsolute assetsProcessor
|
||||||
|
= new ImageDestinationProcessorRelativeToAbsolute(MOCK);
|
||||||
|
|
||||||
|
private final ImageDestinationProcessor processor;
|
||||||
|
|
||||||
|
public ImageDestinationProcessorAssets() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageDestinationProcessorAssets(@Nullable ImageDestinationProcessor parent) {
|
||||||
|
this.processor = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String process(@NonNull String destination) {
|
||||||
|
final String out;
|
||||||
|
final Uri uri = Uri.parse(destination);
|
||||||
|
if (TextUtils.isEmpty(uri.getScheme())) {
|
||||||
|
out = assetsProcessor.process(destination).replace(MOCK, BASE);
|
||||||
|
} else {
|
||||||
|
if (processor != null) {
|
||||||
|
out = processor.process(destination);
|
||||||
|
} else {
|
||||||
|
out = destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
package io.noties.markwon.image.destination;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -6,15 +6,30 @@ import androidx.annotation.Nullable;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
/**
|
||||||
public class UrlProcessorRelativeToAbsolute implements UrlProcessor {
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public class ImageDestinationProcessorRelativeToAbsolute extends ImageDestinationProcessor {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull String base) {
|
||||||
|
return new ImageDestinationProcessorRelativeToAbsolute(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull URL base) {
|
||||||
|
return new ImageDestinationProcessorRelativeToAbsolute(base);
|
||||||
|
}
|
||||||
|
|
||||||
private final URL base;
|
private final URL base;
|
||||||
|
|
||||||
public UrlProcessorRelativeToAbsolute(@NonNull String base) {
|
public ImageDestinationProcessorRelativeToAbsolute(@NonNull String base) {
|
||||||
this.base = obtain(base);
|
this.base = obtain(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImageDestinationProcessorRelativeToAbsolute(@NonNull URL base) {
|
||||||
|
this.base = base;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String process(@NonNull String destination) {
|
public String process(@NonNull String destination) {
|
@ -1,8 +0,0 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public interface UrlProcessor {
|
|
||||||
@NonNull
|
|
||||||
String process(@NonNull String destination);
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processor that will <em>assume</em> that an URL without scheme points to android assets folder.
|
|
||||||
* URL with a scheme will be processed by {@link #processor} (if it is specified) or returned `as-is`.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
|
||||||
public class UrlProcessorAndroidAssets implements UrlProcessor {
|
|
||||||
|
|
||||||
|
|
||||||
static final String MOCK = "https://android.asset/";
|
|
||||||
static final String BASE = "file:///android_asset/";
|
|
||||||
|
|
||||||
private final UrlProcessorRelativeToAbsolute assetsProcessor
|
|
||||||
= new UrlProcessorRelativeToAbsolute(MOCK);
|
|
||||||
|
|
||||||
private final UrlProcessor processor;
|
|
||||||
|
|
||||||
public UrlProcessorAndroidAssets() {
|
|
||||||
this(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UrlProcessorAndroidAssets(@Nullable UrlProcessor parent) {
|
|
||||||
this.processor = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String process(@NonNull String destination) {
|
|
||||||
final String out;
|
|
||||||
final Uri uri = Uri.parse(destination);
|
|
||||||
if (TextUtils.isEmpty(uri.getScheme())) {
|
|
||||||
out = assetsProcessor.process(destination).replace(MOCK, BASE);
|
|
||||||
} else {
|
|
||||||
if (processor != null) {
|
|
||||||
out = processor.process(destination);
|
|
||||||
} else {
|
|
||||||
out = destination;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class UrlProcessorNoOp implements UrlProcessor {
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String process(@NonNull String destination) {
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,6 +18,7 @@ public class Dip {
|
|||||||
|
|
||||||
private final float density;
|
private final float density;
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public Dip(float density) {
|
public Dip(float density) {
|
||||||
this.density = density;
|
this.density = density;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import java.lang.reflect.Method;
|
|||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
|
|
||||||
// utility class to print parsed Nodes hierarchy
|
// utility class to print parsed Nodes hierarchy
|
||||||
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||||
public abstract class DumpNodes {
|
public abstract class DumpNodes {
|
||||||
|
|
||||||
public interface NodeProcessor {
|
public interface NodeProcessor {
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
package io.noties.markwon.utils;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Layout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public abstract class LayoutUtils {
|
||||||
|
|
||||||
|
private static final float DEFAULT_EXTRA = 0F;
|
||||||
|
private static final float DEFAULT_MULTIPLIER = 1F;
|
||||||
|
|
||||||
|
public static int getLineBottomWithoutPaddingAndSpacing(
|
||||||
|
@NonNull Layout layout,
|
||||||
|
int line
|
||||||
|
) {
|
||||||
|
|
||||||
|
final int bottom = layout.getLineBottom(line);
|
||||||
|
final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
final boolean isSpanLastLine = line == (layout.getLineCount() - 1);
|
||||||
|
|
||||||
|
final int lineBottom;
|
||||||
|
final float lineSpacingExtra = layout.getSpacingAdd();
|
||||||
|
final float lineSpacingMultiplier = layout.getSpacingMultiplier();
|
||||||
|
|
||||||
|
// simplified check
|
||||||
|
final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA
|
||||||
|
|| lineSpacingMultiplier != DEFAULT_MULTIPLIER;
|
||||||
|
|
||||||
|
if (!hasLineSpacing
|
||||||
|
|| (isSpanLastLine && lastLineSpacingNotAdded)) {
|
||||||
|
lineBottom = bottom;
|
||||||
|
} else {
|
||||||
|
final float extra;
|
||||||
|
if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) {
|
||||||
|
final int lineHeight = getLineHeight(layout, line);
|
||||||
|
extra = lineHeight -
|
||||||
|
((lineHeight - lineSpacingExtra) / lineSpacingMultiplier);
|
||||||
|
} else {
|
||||||
|
extra = lineSpacingExtra;
|
||||||
|
}
|
||||||
|
lineBottom = (int) (bottom - extra + .5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is the last line that span is occupying **and** that this line is the last
|
||||||
|
// one in TextView
|
||||||
|
if (isSpanLastLine
|
||||||
|
&& (line == layout.getLineCount() - 1)) {
|
||||||
|
return lineBottom - layout.getBottomPadding();
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) {
|
||||||
|
final int top = layout.getLineTop(line);
|
||||||
|
if (line == 0) {
|
||||||
|
return top - layout.getTopPadding();
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLineHeight(@NonNull Layout layout, int line) {
|
||||||
|
return layout.getLineTop(line + 1) - layout.getLineTop(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LayoutUtils() {
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import android.text.Spanned;
|
|||||||
|
|
||||||
public abstract class LeadingMarginUtils {
|
public abstract class LeadingMarginUtils {
|
||||||
|
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
||||||
public static boolean selfStart(int start, CharSequence text, Object span) {
|
public static boolean selfStart(int start, CharSequence text, Object span) {
|
||||||
return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start;
|
return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package io.noties.markwon.utils;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import io.noties.markwon.core.spans.TextLayoutSpan;
|
||||||
|
import io.noties.markwon.core.spans.TextViewSpan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
public abstract class SpanUtils {
|
||||||
|
|
||||||
|
public static int width(@NonNull Canvas canvas, @NonNull CharSequence cs) {
|
||||||
|
// Layout
|
||||||
|
// TextView
|
||||||
|
// canvas
|
||||||
|
|
||||||
|
if (cs instanceof Spanned) {
|
||||||
|
final Spanned spanned = (Spanned) cs;
|
||||||
|
|
||||||
|
// if we are displayed with layout information -> use it
|
||||||
|
final Layout layout = TextLayoutSpan.layoutOf(spanned);
|
||||||
|
if (layout != null) {
|
||||||
|
return layout.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have TextView -> obtain width from it (exclude padding)
|
||||||
|
final TextView textView = TextViewSpan.textViewOf(spanned);
|
||||||
|
if (textView != null) {
|
||||||
|
return textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else just use canvas width
|
||||||
|
return canvas.getWidth();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package io.noties.markwon;
|
package io.noties.markwon;
|
||||||
|
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
@ -50,7 +51,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.singletonList(plugin));
|
Collections.singletonList(plugin),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
impl.parse("whatever");
|
impl.parse("whatever");
|
||||||
|
|
||||||
@ -74,7 +77,9 @@ public class MarkwonImplTest {
|
|||||||
parser,
|
parser,
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Arrays.asList(first, second));
|
Arrays.asList(first, second),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
impl.parse("zero");
|
impl.parse("zero");
|
||||||
|
|
||||||
@ -102,7 +107,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
visitorFactory,
|
visitorFactory,
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.singletonList(plugin));
|
Collections.singletonList(plugin),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
when(visitorFactory.create()).thenReturn(visitor);
|
when(visitorFactory.create()).thenReturn(visitor);
|
||||||
when(visitor.builder()).thenReturn(builder);
|
when(visitor.builder()).thenReturn(builder);
|
||||||
@ -149,7 +156,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
visitorFactory,
|
visitorFactory,
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.<MarkwonPlugin>emptyList());
|
Collections.<MarkwonPlugin>emptyList(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
impl.render(mock(Node.class));
|
impl.render(mock(Node.class));
|
||||||
|
|
||||||
@ -185,7 +194,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
visitorFactory,
|
visitorFactory,
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.singletonList(plugin));
|
Collections.singletonList(plugin),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
final AtomicBoolean flag = new AtomicBoolean(false);
|
final AtomicBoolean flag = new AtomicBoolean(false);
|
||||||
final Node node = mock(Node.class);
|
final Node node = mock(Node.class);
|
||||||
@ -224,7 +235,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class, RETURNS_MOCKS),
|
mock(MarkwonVisitorFactory.class, RETURNS_MOCKS),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.singletonList(plugin));
|
Collections.singletonList(plugin),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
final TextView textView = mock(TextView.class);
|
final TextView textView = mock(TextView.class);
|
||||||
final AtomicBoolean flag = new AtomicBoolean(false);
|
final AtomicBoolean flag = new AtomicBoolean(false);
|
||||||
@ -272,7 +285,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
plugins);
|
plugins,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
assertTrue("First", impl.hasPlugin(First.class));
|
assertTrue("First", impl.hasPlugin(First.class));
|
||||||
assertFalse("Second", impl.hasPlugin(Second.class));
|
assertFalse("Second", impl.hasPlugin(Second.class));
|
||||||
@ -295,7 +310,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
Collections.singletonList(plugin));
|
Collections.singletonList(plugin),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
final TextView textView = mock(TextView.class);
|
final TextView textView = mock(TextView.class);
|
||||||
final Spanned spanned = mock(Spanned.class);
|
final Spanned spanned = mock(Spanned.class);
|
||||||
@ -339,7 +356,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
plugins);
|
plugins,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// should be returned
|
// should be returned
|
||||||
assertNotNull(impl.requirePlugin(MarkwonPlugin.class));
|
assertNotNull(impl.requirePlugin(MarkwonPlugin.class));
|
||||||
@ -370,7 +389,9 @@ public class MarkwonImplTest {
|
|||||||
mock(Parser.class),
|
mock(Parser.class),
|
||||||
mock(MarkwonVisitorFactory.class),
|
mock(MarkwonVisitorFactory.class),
|
||||||
mock(MarkwonConfiguration.class),
|
mock(MarkwonConfiguration.class),
|
||||||
plugins);
|
plugins,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
final List<? extends MarkwonPlugin> list = impl.getPlugins();
|
final List<? extends MarkwonPlugin> list = impl.getPlugins();
|
||||||
|
|
||||||
@ -385,4 +406,42 @@ public class MarkwonImplTest {
|
|||||||
assertTrue(e.getMessage(), true);
|
assertTrue(e.getMessage(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fallback_to_raw() {
|
||||||
|
final String md = "*";
|
||||||
|
|
||||||
|
final MarkwonImpl impl = new MarkwonImpl(
|
||||||
|
TextView.BufferType.SPANNABLE,
|
||||||
|
null,
|
||||||
|
mock(Parser.class, RETURNS_MOCKS),
|
||||||
|
// it must be sufficient to just return mocks and thus empty rendering result
|
||||||
|
mock(MarkwonVisitorFactory.class, RETURNS_MOCKS),
|
||||||
|
mock(MarkwonConfiguration.class),
|
||||||
|
Collections.<MarkwonPlugin>emptyList(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
final Spanned spanned = impl.toMarkdown(md);
|
||||||
|
assertEquals(md, spanned.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fallback_to_raw_false() {
|
||||||
|
final String md = "*";
|
||||||
|
|
||||||
|
final MarkwonImpl impl = new MarkwonImpl(
|
||||||
|
TextView.BufferType.SPANNABLE,
|
||||||
|
null,
|
||||||
|
mock(Parser.class, RETURNS_MOCKS),
|
||||||
|
// it must be sufficient to just return mocks and thus empty rendering result
|
||||||
|
mock(MarkwonVisitorFactory.class, RETURNS_MOCKS),
|
||||||
|
mock(MarkwonConfiguration.class),
|
||||||
|
Collections.<MarkwonPlugin>emptyList(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
final Spanned spanned = impl.toMarkdown(md);
|
||||||
|
assertTrue(spanned.toString(), TextUtils.isEmpty(spanned));
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
package io.noties.markwon.image.destination;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -6,18 +6,18 @@ import org.junit.runner.RunWith;
|
|||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import static io.noties.markwon.image.destination.ImageDestinationProcessorAssets.BASE;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets.BASE;
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
public class UrlProcessorAndroidAssetsTest {
|
public class ImageDestinationProcessorAssetsTest {
|
||||||
|
|
||||||
private UrlProcessorAndroidAssets processor;
|
private ImageDestinationProcessorAssets processor;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
processor = new UrlProcessorAndroidAssets();
|
processor = new ImageDestinationProcessorAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
@ -1,4 +1,4 @@
|
|||||||
package io.noties.markwon.urlprocessor;
|
package io.noties.markwon.image.destination;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -9,39 +9,39 @@ import static org.junit.Assert.*;
|
|||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@Config(manifest = Config.NONE)
|
@Config(manifest = Config.NONE)
|
||||||
public class UrlProcessorRelativeToAbsoluteTest {
|
public class ImageDestinationProcessorRelativeToAbsoluteTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void malformed_base_do_not_process() {
|
public void malformed_base_do_not_process() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("!@#$%^&*(");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("!@#$%^&*(");
|
||||||
final String destination = "../hey.there.html";
|
final String destination = "../hey.there.html";
|
||||||
assertEquals(destination, processor.process(destination));
|
assertEquals(destination, processor.process(destination));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void access_root() {
|
public void access_root() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/");
|
||||||
final String url = "/index.html";
|
final String url = "/index.html";
|
||||||
assertEquals("https://ro.ot/index.html", processor.process(url));
|
assertEquals("https://ro.ot/index.html", processor.process(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void access_same_directory() {
|
public void access_same_directory() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("https://ro.ot/hello/");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("https://ro.ot/hello/");
|
||||||
final String url = "./.htaccess";
|
final String url = "./.htaccess";
|
||||||
assertEquals("https://ro.ot/hello/.htaccess", processor.process(url));
|
assertEquals("https://ro.ot/hello/.htaccess", processor.process(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void asset_directory_up() {
|
public void asset_directory_up() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/second/");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/second/");
|
||||||
final String url = "../cat.JPG";
|
final String url = "../cat.JPG";
|
||||||
assertEquals("http://ro.ot/first/cat.JPG", processor.process(url));
|
assertEquals("http://ro.ot/first/cat.JPG", processor.process(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void change_directory_inside_destination() {
|
public void change_directory_inside_destination() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/");
|
||||||
final String url = "../first/../second/./thi.rd";
|
final String url = "../first/../second/./thi.rd";
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"http://ro.ot/second/thi.rd",
|
"http://ro.ot/second/thi.rd",
|
||||||
@ -51,7 +51,7 @@ public class UrlProcessorRelativeToAbsoluteTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void with_query_arguments() {
|
public void with_query_arguments() {
|
||||||
final UrlProcessorRelativeToAbsolute processor = new UrlProcessorRelativeToAbsolute("http://ro.ot/first/");
|
final ImageDestinationProcessorRelativeToAbsolute processor = new ImageDestinationProcessorRelativeToAbsolute("http://ro.ot/first/");
|
||||||
final String url = "../index.php?ROOT=1";
|
final String url = "../index.php?ROOT=1";
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"http://ro.ot/index.php?ROOT=1",
|
"http://ro.ot/index.php?ROOT=1",
|
@ -458,8 +458,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
|||||||
|
|
||||||
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
||||||
.textSize(theme.blockTextSize())
|
.textSize(theme.blockTextSize())
|
||||||
.align(theme.blockHorizontalAlignment())
|
.align(theme.blockHorizontalAlignment());
|
||||||
.fitCanvas(theme.blockFitCanvas());
|
|
||||||
|
|
||||||
if (backgroundProvider != null) {
|
if (backgroundProvider != null) {
|
||||||
builder.background(backgroundProvider.provide());
|
builder.background(backgroundProvider.provide());
|
||||||
@ -489,8 +488,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
|||||||
final int color = theme.inlineTextColor();
|
final int color = theme.inlineTextColor();
|
||||||
|
|
||||||
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
||||||
.textSize(theme.inlineTextSize())
|
.textSize(theme.inlineTextSize());
|
||||||
.fitCanvas(false);
|
|
||||||
|
|
||||||
if (backgroundProvider != null) {
|
if (backgroundProvider != null) {
|
||||||
builder.background(backgroundProvider.provide());
|
builder.background(backgroundProvider.provide());
|
||||||
@ -530,7 +528,20 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
|
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
|
||||||
return drawable.getResult().getBounds();
|
|
||||||
|
// @since 4.4.0 resolve inline size (scale down if exceed available width)
|
||||||
|
final Rect imageBounds = drawable.getResult().getBounds();
|
||||||
|
final int canvasWidth = drawable.getLastKnownCanvasWidth();
|
||||||
|
final int w = imageBounds.width();
|
||||||
|
|
||||||
|
if (w > canvasWidth) {
|
||||||
|
// here we must scale it down (keeping the ratio)
|
||||||
|
final float ratio = (float) w / imageBounds.height();
|
||||||
|
final int h = (int) (canvasWidth / ratio + .5F);
|
||||||
|
return new Rect(0, 0, canvasWidth, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageBounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ import android.annotation.SuppressLint;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.StaticLayout;
|
import android.text.StaticLayout;
|
||||||
import android.text.TextPaint;
|
import android.text.TextPaint;
|
||||||
@ -20,7 +23,11 @@ import java.lang.annotation.RetentionPolicy;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.noties.markwon.core.spans.TextLayoutSpan;
|
||||||
|
import io.noties.markwon.image.AsyncDrawable;
|
||||||
|
import io.noties.markwon.image.AsyncDrawableSpan;
|
||||||
import io.noties.markwon.utils.LeadingMarginUtils;
|
import io.noties.markwon.utils.LeadingMarginUtils;
|
||||||
|
import io.noties.markwon.utils.SpanUtils;
|
||||||
|
|
||||||
public class TableRowSpan extends ReplacementSpan {
|
public class TableRowSpan extends ReplacementSpan {
|
||||||
|
|
||||||
@ -67,7 +74,7 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
|
|
||||||
private final TableTheme theme;
|
private final TableTheme theme;
|
||||||
private final List<Cell> cells;
|
private final List<Cell> cells;
|
||||||
private final List<StaticLayout> layouts;
|
private final List<Layout> layouts;
|
||||||
private final TextPaint textPaint;
|
private final TextPaint textPaint;
|
||||||
private final boolean header;
|
private final boolean header;
|
||||||
private final boolean odd;
|
private final boolean odd;
|
||||||
@ -108,7 +115,7 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
if (fm != null) {
|
if (fm != null) {
|
||||||
|
|
||||||
int max = 0;
|
int max = 0;
|
||||||
for (StaticLayout layout : layouts) {
|
for (Layout layout : layouts) {
|
||||||
final int height = layout.getHeight();
|
final int height = layout.getHeight();
|
||||||
if (height > max) {
|
if (height > max) {
|
||||||
max = height;
|
max = height;
|
||||||
@ -144,8 +151,9 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
int bottom,
|
int bottom,
|
||||||
@NonNull Paint p) {
|
@NonNull Paint p) {
|
||||||
|
|
||||||
if (recreateLayouts(canvas.getWidth())) {
|
final int spanWidth = SpanUtils.width(canvas, text);
|
||||||
width = canvas.getWidth();
|
if (recreateLayouts(spanWidth)) {
|
||||||
|
width = spanWidth;
|
||||||
// @since 4.3.1 it's important to cast to TextPaint in order to display links, etc
|
// @since 4.3.1 it's important to cast to TextPaint in order to display links, etc
|
||||||
if (p instanceof TextPaint) {
|
if (p instanceof TextPaint) {
|
||||||
// there must be a reason why this method receives Paint instead of TextPaint...
|
// there must be a reason why this method receives Paint instead of TextPaint...
|
||||||
@ -236,7 +244,7 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
final int borderTop = isFirstTableRow ? borderWidth : 0;
|
final int borderTop = isFirstTableRow ? borderWidth : 0;
|
||||||
final int borderBottom = bottom - top - borderWidth;
|
final int borderBottom = bottom - top - borderWidth;
|
||||||
|
|
||||||
StaticLayout layout;
|
Layout layout;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
layout = layouts.get(i);
|
layout = layouts.get(i);
|
||||||
final int save = canvas.save();
|
final int save = canvas.save();
|
||||||
@ -293,20 +301,76 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
final int w = (width / columns) - padding;
|
final int w = (width / columns) - padding;
|
||||||
|
|
||||||
this.layouts.clear();
|
this.layouts.clear();
|
||||||
Cell cell;
|
|
||||||
StaticLayout layout;
|
|
||||||
for (int i = 0, size = cells.size(); i < size; i++) {
|
for (int i = 0, size = cells.size(); i < size; i++) {
|
||||||
cell = cells.get(i);
|
makeLayout(i, w, cells.get(i));
|
||||||
layout = new StaticLayout(
|
}
|
||||||
cell.text,
|
}
|
||||||
|
|
||||||
|
private void makeLayout(final int index, final int width, @NonNull final Cell cell) {
|
||||||
|
|
||||||
|
final Runnable recreate = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final Invalidator invalidator = TableRowSpan.this.invalidator;
|
||||||
|
if (invalidator != null) {
|
||||||
|
layouts.remove(index);
|
||||||
|
makeLayout(index, width, cell);
|
||||||
|
invalidator.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final Spannable spannable;
|
||||||
|
|
||||||
|
if (cell.text instanceof Spannable) {
|
||||||
|
spannable = (Spannable) cell.text;
|
||||||
|
} else {
|
||||||
|
spannable = new SpannableString(cell.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Layout layout = new StaticLayout(
|
||||||
|
spannable,
|
||||||
textPaint,
|
textPaint,
|
||||||
w,
|
width,
|
||||||
alignment(cell.alignment),
|
alignment(cell.alignment),
|
||||||
1.F,
|
1.0F,
|
||||||
.0F,
|
0.0F,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
layouts.add(layout);
|
|
||||||
|
// @since 4.4.0
|
||||||
|
TextLayoutSpan.applyTo(spannable, layout);
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
scheduleAsyncDrawables(spannable, recreate);
|
||||||
|
|
||||||
|
layouts.add(index, layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleAsyncDrawables(@NonNull Spannable spannable, @NonNull final Runnable recreate) {
|
||||||
|
|
||||||
|
final AsyncDrawableSpan[] spans = spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class);
|
||||||
|
if (spans != null
|
||||||
|
&& spans.length > 0) {
|
||||||
|
|
||||||
|
for (AsyncDrawableSpan span : spans) {
|
||||||
|
|
||||||
|
final AsyncDrawable drawable = span.getDrawable();
|
||||||
|
|
||||||
|
// it is absolutely crucial to check if drawable is already attached,
|
||||||
|
// otherwise we would end up with a loop
|
||||||
|
if (drawable.isAttached()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable.setCallback2(new CallbackAdapter() {
|
||||||
|
@Override
|
||||||
|
public void invalidateDrawable(@NonNull Drawable who) {
|
||||||
|
recreate.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,4 +394,21 @@ public class TableRowSpan extends ReplacementSpan {
|
|||||||
public void invalidator(@Nullable Invalidator invalidator) {
|
public void invalidator(@Nullable Invalidator invalidator) {
|
||||||
this.invalidator = invalidator;
|
this.invalidator = invalidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static abstract class CallbackAdapter implements Drawable.Callback {
|
||||||
|
@Override
|
||||||
|
public void invalidateDrawable(@NonNull Drawable who) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ public class HtmlEmptyTagReplacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final String IMG_REPLACEMENT = "\uFFFC";
|
private static final String IMG_REPLACEMENT = "\uFFFC";
|
||||||
|
private static final String IFRAME_REPLACEMENT = "\u00a0"; // non-breakable space
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return replacement for supplied startTag or null if no replacement should occur (which will
|
* @return replacement for supplied startTag or null if no replacement should occur (which will
|
||||||
@ -44,6 +45,9 @@ public class HtmlEmptyTagReplacement {
|
|||||||
} else {
|
} else {
|
||||||
replacement = alt;
|
replacement = alt;
|
||||||
}
|
}
|
||||||
|
} else if ("iframe".equals(name)) {
|
||||||
|
// @since 4.4.0 make iframe non-empty
|
||||||
|
replacement = IFRAME_REPLACEMENT;
|
||||||
} else {
|
} else {
|
||||||
replacement = null;
|
replacement = null;
|
||||||
}
|
}
|
||||||
|
@ -53,13 +53,16 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
|
|||||||
public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F;
|
public static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F;
|
||||||
|
|
||||||
private final MarkwonHtmlRendererImpl.Builder builder;
|
private final MarkwonHtmlRendererImpl.Builder builder;
|
||||||
private final MarkwonHtmlParser htmlParser;
|
|
||||||
|
private MarkwonHtmlParser htmlParser;
|
||||||
private MarkwonHtmlRenderer htmlRenderer;
|
private MarkwonHtmlRenderer htmlRenderer;
|
||||||
|
|
||||||
|
// @since 4.4.0
|
||||||
|
private HtmlEmptyTagReplacement emptyTagReplacement = new HtmlEmptyTagReplacement();
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
HtmlPlugin() {
|
HtmlPlugin() {
|
||||||
this.builder = new MarkwonHtmlRendererImpl.Builder();
|
this.builder = new MarkwonHtmlRendererImpl.Builder();
|
||||||
this.htmlParser = MarkwonHtmlParserImpl.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,6 +107,16 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param emptyTagReplacement {@link HtmlEmptyTagReplacement}
|
||||||
|
* @since 4.4.0
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public HtmlPlugin emptyTagReplacement(@NonNull HtmlEmptyTagReplacement emptyTagReplacement) {
|
||||||
|
this.emptyTagReplacement = emptyTagReplacement;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) {
|
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder configurationBuilder) {
|
||||||
|
|
||||||
@ -128,6 +141,7 @@ public class HtmlPlugin extends AbstractMarkwonPlugin {
|
|||||||
builder.addDefaultTagHandler(new HeadingHandler());
|
builder.addDefaultTagHandler(new HeadingHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htmlParser = MarkwonHtmlParserImpl.create(emptyTagReplacement);
|
||||||
htmlRenderer = builder.build();
|
htmlRenderer = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ public class ImageHandler extends SimpleTagHandler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String destination = configuration.urlProcessor().process(src);
|
final String destination = configuration.imageDestinationProcessor().process(src);
|
||||||
final ImageSize imageSize = imageSizeParser.parse(tag.attributes());
|
final ImageSize imageSize = imageSizeParser.parse(tag.attributes());
|
||||||
|
|
||||||
// todo: replacement text is link... as we are not at block level
|
// todo: replacement text is link... as we are not at block level
|
||||||
|
@ -27,7 +27,8 @@ public class LinkHandler extends SimpleTagHandler {
|
|||||||
|
|
||||||
CoreProps.LINK_DESTINATION.set(
|
CoreProps.LINK_DESTINATION.set(
|
||||||
renderProps,
|
renderProps,
|
||||||
configuration.urlProcessor().process(destination));
|
destination
|
||||||
|
);
|
||||||
|
|
||||||
return spanFactory.getSpans(configuration, renderProps);
|
return spanFactory.getSpans(configuration, renderProps);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import java.util.Map;
|
|||||||
|
|
||||||
import coil.Coil;
|
import coil.Coil;
|
||||||
import coil.ImageLoader;
|
import coil.ImageLoader;
|
||||||
import coil.api.ImageLoaders;
|
|
||||||
import coil.request.LoadRequest;
|
import coil.request.LoadRequest;
|
||||||
import coil.request.RequestDisposable;
|
import coil.request.RequestDisposable;
|
||||||
import coil.target.Target;
|
import coil.target.Target;
|
||||||
@ -48,7 +47,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public LoadRequest load(@NonNull AsyncDrawable drawable) {
|
public LoadRequest load(@NonNull AsyncDrawable drawable) {
|
||||||
return ImageLoaders.newLoadBuilder(Coil.loader(), context)
|
return LoadRequest.builder(context)
|
||||||
.data(drawable.getDestination())
|
.data(drawable.getDestination())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -57,7 +56,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin {
|
|||||||
public void cancel(@NonNull RequestDisposable disposable) {
|
public void cancel(@NonNull RequestDisposable disposable) {
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
}
|
}
|
||||||
}, Coil.loader());
|
}, Coil.imageLoader(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -67,7 +66,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public LoadRequest load(@NonNull AsyncDrawable drawable) {
|
public LoadRequest load(@NonNull AsyncDrawable drawable) {
|
||||||
return ImageLoaders.newLoadBuilder(imageLoader, context)
|
return LoadRequest.builder(context)
|
||||||
.data(drawable.getDestination())
|
.data(drawable.getDestination())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -129,7 +128,7 @@ public class CoilImagesPlugin extends AbstractMarkwonPlugin {
|
|||||||
LoadRequest request = coilStore.load(drawable).newBuilder()
|
LoadRequest request = coilStore.load(drawable).newBuilder()
|
||||||
.target(target)
|
.target(target)
|
||||||
.build();
|
.build();
|
||||||
RequestDisposable disposable = imageLoader.load(request);
|
RequestDisposable disposable = imageLoader.execute(request);
|
||||||
cache.put(drawable, disposable);
|
cache.put(drawable, disposable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@ package io.noties.markwon.image.file;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@ -18,7 +19,6 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessorAndroidAssets;
|
|
||||||
import io.noties.markwon.image.ImageItem;
|
import io.noties.markwon.image.ImageItem;
|
||||||
import io.noties.markwon.image.SchemeHandler;
|
import io.noties.markwon.image.SchemeHandler;
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ public class FileSchemeHandler extends SchemeHandler {
|
|||||||
public static final String SCHEME = "file";
|
public static final String SCHEME = "file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see UrlProcessorAndroidAssets
|
* @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) {
|
public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) {
|
||||||
@ -39,7 +39,7 @@ public class FileSchemeHandler extends SchemeHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #createWithAssets(AssetManager)
|
* @see #createWithAssets(AssetManager)
|
||||||
* @see UrlProcessorAndroidAssets
|
* @see io.noties.markwon.image.destination.ImageDestinationProcessorAssets
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -38,6 +38,7 @@ For example, `@since $nap` seems like a good candidate. For this a live template
|
|||||||
whenever a new API method/field/functionality-change is introduced (`snc`):
|
whenever a new API method/field/functionality-change is introduced (`snc`):
|
||||||
|
|
||||||
```
|
```
|
||||||
|
// semicolon with a space so this one is not accedentally replaced with release version
|
||||||
@since $nap ;
|
@since $nap ;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -24,7 +24,11 @@
|
|||||||
<activity android:name="io.noties.markwon.sample.basicplugins.BasicPluginsActivity" />
|
<activity android:name="io.noties.markwon.sample.basicplugins.BasicPluginsActivity" />
|
||||||
<activity android:name="io.noties.markwon.sample.recycler.RecyclerActivity" />
|
<activity android:name="io.noties.markwon.sample.recycler.RecyclerActivity" />
|
||||||
<activity android:name="io.noties.markwon.sample.theme.ThemeActivity" />
|
<activity android:name="io.noties.markwon.sample.theme.ThemeActivity" />
|
||||||
<activity android:name=".html.HtmlActivity" />
|
|
||||||
|
<activity
|
||||||
|
android:name=".html.HtmlActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
<activity android:name=".simpleext.SimpleExtActivity" />
|
<activity android:name=".simpleext.SimpleExtActivity" />
|
||||||
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
<activity android:name=".customextension2.CustomExtensionActivity2" />
|
||||||
<activity android:name=".precomputed.PrecomputedActivity" />
|
<activity android:name=".precomputed.PrecomputedActivity" />
|
||||||
@ -32,6 +36,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".editor.EditorActivity"
|
android:name=".editor.EditorActivity"
|
||||||
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity android:name=".inlineparser.InlineParserActivity" />
|
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||||
|
@ -5,6 +5,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ import java.util.Collections;
|
|||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.BlockHandlerDef;
|
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.MarkwonSpansFactory;
|
import io.noties.markwon.MarkwonSpansFactory;
|
||||||
@ -153,7 +155,7 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
* <ul>
|
* <ul>
|
||||||
* <li>SyntaxHighlight</li>
|
* <li>SyntaxHighlight</li>
|
||||||
* <li>LinkSpan.Resolver</li>
|
* <li>LinkSpan.Resolver</li>
|
||||||
* <li>UrlProcessor</li>
|
* <li>ImageDestinationProcessor</li>
|
||||||
* <li>ImageSizeResolver</li>
|
* <li>ImageSizeResolver</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
@ -173,12 +175,18 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||||
// for example if specified destination has no scheme info, we will
|
// for example if specified destination has no scheme info, we will
|
||||||
// _assume_ that it's network request and append HTTPS scheme
|
// _assume_ that it's network request and append HTTPS scheme
|
||||||
builder.urlProcessor(destination -> {
|
builder.linkResolver(new LinkResolverDef() {
|
||||||
final Uri uri = Uri.parse(destination);
|
@Override
|
||||||
|
public void resolve(@NonNull View view, @NonNull String link) {
|
||||||
|
final String destination;
|
||||||
|
final Uri uri = Uri.parse(link);
|
||||||
if (TextUtils.isEmpty(uri.getScheme())) {
|
if (TextUtils.isEmpty(uri.getScheme())) {
|
||||||
return "https://" + destination;
|
destination = "https://" + link;
|
||||||
|
} else {
|
||||||
|
destination = link;
|
||||||
|
}
|
||||||
|
super.resolve(view, destination);
|
||||||
}
|
}
|
||||||
return destination;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -434,4 +442,25 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private void code() {
|
||||||
|
// final String md = "" +
|
||||||
|
// "hello `there`!\n\n" +
|
||||||
|
// "so this, `is super duper long very very very long line that should be going further and further and further down` yep.\n\n" +
|
||||||
|
// "`okay`";
|
||||||
|
// final Markwon markwon = Markwon.builder(this)
|
||||||
|
// .usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
// @Override
|
||||||
|
// public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||||
|
// builder.setFactory(Code.class, new SpanFactory() {
|
||||||
|
// @Override
|
||||||
|
// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||||
|
// return new CodeTextView.CodeSpan();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .build();
|
||||||
|
// markwon.setMarkdown(textView, md);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,192 @@
|
|||||||
|
package io.noties.markwon.sample.basicplugins;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import io.noties.debug.Debug;
|
||||||
|
|
||||||
|
@SuppressLint("AppCompatCustomView")
|
||||||
|
public class CodeTextView extends TextView {
|
||||||
|
|
||||||
|
static class CodeSpan {
|
||||||
|
}
|
||||||
|
|
||||||
|
private int paddingHorizontal;
|
||||||
|
private int paddingVertical;
|
||||||
|
|
||||||
|
private float cornerRadius;
|
||||||
|
private float strokeWidth;
|
||||||
|
private int strokeColor;
|
||||||
|
private int backgroundColor;
|
||||||
|
|
||||||
|
public CodeTextView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CodeTextView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
paint.setColor(0xFFff0000);
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
final Layout layout = getLayout();
|
||||||
|
if (layout != null) {
|
||||||
|
draw(this, canvas, layout);
|
||||||
|
}
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void draw(
|
||||||
|
@NonNull View view,
|
||||||
|
@NonNull Canvas canvas,
|
||||||
|
@NonNull Layout layout
|
||||||
|
) {
|
||||||
|
|
||||||
|
final CharSequence cs = layout.getText();
|
||||||
|
if (!(cs instanceof Spanned)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Spanned spanned = (Spanned) cs;
|
||||||
|
|
||||||
|
final int save = canvas.save();
|
||||||
|
try {
|
||||||
|
canvas.translate(view.getPaddingLeft(), view.getPaddingTop());
|
||||||
|
|
||||||
|
// TODO: block?
|
||||||
|
// TODO: we must remove _original_ spans
|
||||||
|
// TODO: cache (attach a listener?)
|
||||||
|
// TODO: editor?
|
||||||
|
|
||||||
|
final CodeSpan[] spans = spanned.getSpans(0, spanned.length(), CodeSpan.class);
|
||||||
|
if (spans != null && spans.length > 0) {
|
||||||
|
for (CodeSpan span : spans) {
|
||||||
|
|
||||||
|
final int startOffset = spanned.getSpanStart(span);
|
||||||
|
final int endOffset = spanned.getSpanEnd(span);
|
||||||
|
|
||||||
|
final int startLine = layout.getLineForOffset(startOffset);
|
||||||
|
final int endLine = layout.getLineForOffset(endOffset);
|
||||||
|
|
||||||
|
// do we need to round them?
|
||||||
|
final float left = layout.getPrimaryHorizontal(startOffset)
|
||||||
|
+ (-1 * layout.getParagraphDirection(startLine) * paddingHorizontal);
|
||||||
|
|
||||||
|
final float right = layout.getPrimaryHorizontal(endOffset)
|
||||||
|
+ (layout.getParagraphDirection(endLine) * paddingHorizontal);
|
||||||
|
|
||||||
|
final float top = getLineTop(layout, startLine, paddingVertical);
|
||||||
|
final float bottom = getLineBottom(layout, endLine, paddingVertical);
|
||||||
|
|
||||||
|
Debug.i(new RectF(left, top, right, bottom).toShortString());
|
||||||
|
|
||||||
|
if (startLine == endLine) {
|
||||||
|
canvas.drawRect(left, top, right, bottom, paint);
|
||||||
|
} else {
|
||||||
|
// draw first line (start until the lineEnd)
|
||||||
|
// draw everything in-between (startLine - endLine)
|
||||||
|
// draw last line (lineStart until the end
|
||||||
|
|
||||||
|
canvas.drawRect(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
layout.getLineRight(startLine),
|
||||||
|
getLineBottom(layout, startLine, paddingVertical),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int line = startLine + 1; line < endLine; line++) {
|
||||||
|
canvas.drawRect(
|
||||||
|
layout.getLineLeft(line),
|
||||||
|
getLineTop(layout, line, paddingVertical),
|
||||||
|
layout.getLineRight(line),
|
||||||
|
getLineBottom(layout, line, paddingVertical),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawRect(
|
||||||
|
layout.getLineLeft(endLine),
|
||||||
|
getLineTop(layout, endLine, paddingVertical),
|
||||||
|
right,
|
||||||
|
getLineBottom(layout, endLine, paddingVertical),
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
canvas.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getLineTop(@NonNull Layout layout, int line, float padding) {
|
||||||
|
float value = layout.getLineTop(line) - padding;
|
||||||
|
if (line == 0) {
|
||||||
|
value -= layout.getTopPadding();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getLineBottom(@NonNull Layout layout, int line, float padding) {
|
||||||
|
float value = getLineBottomWithoutSpacing(layout, line) - padding;
|
||||||
|
if (line == (layout.getLineCount() - 1)) {
|
||||||
|
value -= layout.getBottomPadding();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getLineBottomWithoutSpacing(@NonNull Layout layout, int line) {
|
||||||
|
final float value = layout.getLineBottom(line);
|
||||||
|
|
||||||
|
final boolean isLastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||||
|
final boolean isLastLine = line == (layout.getLineCount() - 1);
|
||||||
|
|
||||||
|
final float lineBottomWithoutSpacing;
|
||||||
|
|
||||||
|
final float lineSpacingExtra = layout.getSpacingAdd();
|
||||||
|
final float lineSpacingMultiplier = layout.getSpacingMultiplier();
|
||||||
|
|
||||||
|
final boolean hasLineSpacing = Float.compare(lineSpacingExtra, .0F) != 0
|
||||||
|
|| Float.compare(lineSpacingMultiplier, 1F) != 0;
|
||||||
|
|
||||||
|
if (!hasLineSpacing || isLastLine && isLastLineSpacingNotAdded) {
|
||||||
|
lineBottomWithoutSpacing = value;
|
||||||
|
} else {
|
||||||
|
final float extra;
|
||||||
|
if (Float.compare(lineSpacingMultiplier, 1F) != 0) {
|
||||||
|
final float lineHeight = getLineHeight(layout, line);
|
||||||
|
extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier;
|
||||||
|
} else {
|
||||||
|
extra = lineSpacingExtra;
|
||||||
|
}
|
||||||
|
lineBottomWithoutSpacing = value - extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineBottomWithoutSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getLineHeight(@NonNull Layout layout, int line) {
|
||||||
|
return layout.getLineTop(line + 1) - layout.getLineTop(line);
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,14 @@ import android.widget.Toast;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.node.Block;
|
||||||
|
import org.commonmark.node.BlockQuote;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
import io.noties.markwon.core.CorePlugin;
|
import io.noties.markwon.core.CorePlugin;
|
||||||
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
||||||
@ -26,7 +32,8 @@ public class CoreActivity extends ActivityWithMenuOptions {
|
|||||||
return MenuOptions.create()
|
return MenuOptions.create()
|
||||||
.add("simple", this::simple)
|
.add("simple", this::simple)
|
||||||
.add("toast", this::toast)
|
.add("toast", this::toast)
|
||||||
.add("alreadyParsed", this::alreadyParsed);
|
.add("alreadyParsed", this::alreadyParsed)
|
||||||
|
.add("enabledBlockTypes", this::enabledBlockTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -132,4 +139,28 @@ public class CoreActivity extends ActivityWithMenuOptions {
|
|||||||
// apply parsed markdown
|
// apply parsed markdown
|
||||||
markwon.setParsedMarkdown(textView, spanned);
|
markwon.setParsedMarkdown(textView, spanned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void enabledBlockTypes() {
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
"# Head\n\n" +
|
||||||
|
"> and disabled quote\n\n" +
|
||||||
|
"```\n" +
|
||||||
|
"yep\n" +
|
||||||
|
"```";
|
||||||
|
|
||||||
|
final Set<Class<? extends Block>> blocks = CorePlugin.enabledBlockTypes();
|
||||||
|
blocks.remove(BlockQuote.class);
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.enabledBlockTypes(blocks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ 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.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
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;
|
||||||
@ -25,8 +26,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import io.noties.debug.AndroidLogDebugOutput;
|
||||||
|
import io.noties.debug.Debug;
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||||
import io.noties.markwon.core.spans.EmphasisSpan;
|
import io.noties.markwon.core.spans.EmphasisSpan;
|
||||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||||
import io.noties.markwon.editor.AbstractEditHandler;
|
import io.noties.markwon.editor.AbstractEditHandler;
|
||||||
@ -65,7 +69,8 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
|
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
|
||||||
.add("pluginRequire", this::plugin_require)
|
.add("pluginRequire", this::plugin_require)
|
||||||
.add("pluginNoDefaults", this::plugin_no_defaults)
|
.add("pluginNoDefaults", this::plugin_no_defaults)
|
||||||
.add("heading", this::heading);
|
.add("heading", this::heading)
|
||||||
|
.add("newLine", this::newLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -98,7 +103,10 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
createView();
|
createView();
|
||||||
|
|
||||||
|
Debug.init(new AndroidLogDebugOutput(true));
|
||||||
|
|
||||||
multiple_edit_spans();
|
multiple_edit_spans();
|
||||||
|
// newLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void simple_process() {
|
private void simple_process() {
|
||||||
@ -230,6 +238,7 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
builder.inlineParserFactory(inlineParserFactory);
|
builder.inlineParserFactory(inlineParserFactory);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
||||||
@ -280,6 +289,13 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
editor, Executors.newSingleThreadExecutor(), editText));
|
editor, Executors.newSingleThreadExecutor(), editText));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void newLine() {
|
||||||
|
final Markwon markwon = Markwon.create(this);
|
||||||
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
final TextWatcher textWatcher = MarkdownNewLine.wrap(MarkwonEditorTextWatcher.withProcess(editor));
|
||||||
|
editText.addTextChangedListener(textWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
private void plugin_require() {
|
private void plugin_require() {
|
||||||
// usage of plugin from other plugins
|
// usage of plugin from other plugins
|
||||||
|
|
||||||
@ -295,6 +311,8 @@ public class EditorActivity extends ActivityWithMenuOptions {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
|
||||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||||
|
|
||||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
package io.noties.markwon.sample.editor;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import io.noties.debug.Debug;
|
||||||
|
|
||||||
|
abstract class MarkdownNewLine {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static TextWatcher wrap(@NonNull TextWatcher textWatcher) {
|
||||||
|
return new NewLineTextWatcher(textWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkdownNewLine() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NewLineTextWatcher implements TextWatcher {
|
||||||
|
|
||||||
|
// NB! matches only bullet lists
|
||||||
|
private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$");
|
||||||
|
|
||||||
|
private final TextWatcher wrapped;
|
||||||
|
|
||||||
|
private boolean selfChange;
|
||||||
|
|
||||||
|
// this content is pending to be inserted at the beginning
|
||||||
|
private String pendingNewLineContent;
|
||||||
|
private int pendingNewLineIndex;
|
||||||
|
|
||||||
|
// mark current edited line for removal (range start/end)
|
||||||
|
private int clearLineStart;
|
||||||
|
private int clearLineEnd;
|
||||||
|
|
||||||
|
NewLineTextWatcher(@NonNull TextWatcher wrapped) {
|
||||||
|
this.wrapped = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
if (selfChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just one new character added
|
||||||
|
if (before == 0
|
||||||
|
&& count == 1
|
||||||
|
&& '\n' == s.charAt(start)) {
|
||||||
|
int end = -1;
|
||||||
|
for (int i = start - 1; i >= 0; i--) {
|
||||||
|
if ('\n' == s.charAt(i)) {
|
||||||
|
end = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start at the very beginning
|
||||||
|
if (end < 0) {
|
||||||
|
end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String pendingNewLineContent;
|
||||||
|
|
||||||
|
final int clearLineStart;
|
||||||
|
final int clearLineEnd;
|
||||||
|
|
||||||
|
final Matcher matcher = RE.matcher(s.subSequence(end, start));
|
||||||
|
if (matcher.matches()) {
|
||||||
|
// if second group is empty -> remove new line
|
||||||
|
final String content = matcher.group(2);
|
||||||
|
Debug.e("new line, content: '%s'", content);
|
||||||
|
if (TextUtils.isEmpty(content)) {
|
||||||
|
// another empty new line, remove this start
|
||||||
|
clearLineStart = end;
|
||||||
|
clearLineEnd = start;
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
} else {
|
||||||
|
pendingNewLineContent = matcher.group(1);
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
this.pendingNewLineContent = pendingNewLineContent;
|
||||||
|
this.pendingNewLineIndex = start + 1;
|
||||||
|
this.clearLineStart = clearLineStart;
|
||||||
|
this.clearLineEnd = clearLineEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
if (selfChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingNewLineContent != null || clearLineStart < clearLineEnd) {
|
||||||
|
selfChange = true;
|
||||||
|
try {
|
||||||
|
if (pendingNewLineContent != null) {
|
||||||
|
s.insert(pendingNewLineIndex, pendingNewLineContent);
|
||||||
|
pendingNewLineContent = null;
|
||||||
|
} else {
|
||||||
|
s.replace(clearLineStart, clearLineEnd, "");
|
||||||
|
clearLineStart = clearLineEnd = 0;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
selfChange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB, we assume MarkdownEditor text watcher that only listens for this event,
|
||||||
|
// other text-watchers must be interested in other events also
|
||||||
|
wrapped.afterTextChanged(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,242 @@
|
|||||||
|
package io.noties.markwon.sample.html;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.LineBackgroundSpan;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Px;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import io.noties.markwon.core.spans.TextLayoutSpan;
|
||||||
|
import io.noties.markwon.core.spans.TextViewSpan;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit goes to [Romain Guy](https://github.com/romainguy/elegant-underline)
|
||||||
|
* <p>
|
||||||
|
* Failed attempt to create elegant underline as a span
|
||||||
|
* <ul>
|
||||||
|
* <li>in a `TextView` span is rendered, but `draw` method is invoked constantly which put pressure on CPU and memory
|
||||||
|
* <li>in an `EditText` only the first line draws this underline span (seems to be a weird
|
||||||
|
* issue between LineBackgroundSpan and EditText). Also, in `EditText` `draw` method is invoked
|
||||||
|
* constantly (for each drawing of the blinking cursor)
|
||||||
|
* <li>cannot reliably receive proper text, for example if underline is applied to a text range which has
|
||||||
|
* different typefaces applied to different words (underline cannot know that, which applied to which)
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
// will apply other spans that 100% contain this one, so for example if
|
||||||
|
// an underline that inside some other spans (different typeface), they won't be applied and thus
|
||||||
|
// underline would be incorrect
|
||||||
|
// do not use in editor, due to some obscure thing, LineBackgroundSpan would be applied to the first line only
|
||||||
|
// also, in editor this span would be redrawn with each blink of the cursor
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
class ElegantUnderlineSpan implements LineBackgroundSpan {
|
||||||
|
|
||||||
|
private static final float DEFAULT_UNDERLINE_HEIGHT_DIP = 0.8F;
|
||||||
|
private static final float DEFAULT_UNDERLINE_CLEAR_GAP_DIP = 5.5F;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ElegantUnderlineSpan create() {
|
||||||
|
return new ElegantUnderlineSpan(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ElegantUnderlineSpan create(@Px int underlineHeight) {
|
||||||
|
return new ElegantUnderlineSpan(underlineHeight, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ElegantUnderlineSpan create(@Px int underlineHeight, @Px int underlineClearGap) {
|
||||||
|
return new ElegantUnderlineSpan(underlineHeight, underlineClearGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: underline color?
|
||||||
|
private final int underlineHeight;
|
||||||
|
private final int underlineClearGap;
|
||||||
|
|
||||||
|
private final Path underline = new Path();
|
||||||
|
private final Path outline = new Path();
|
||||||
|
private final Paint stroke = new Paint();
|
||||||
|
private final Path strokedOutline = new Path();
|
||||||
|
|
||||||
|
private final CharCache charCache = new CharCache();
|
||||||
|
|
||||||
|
private final TextPaint tempTextPaint = new TextPaint();
|
||||||
|
|
||||||
|
protected ElegantUnderlineSpan(@Px int underlineHeight, @Px int underlineClearGap) {
|
||||||
|
this.underlineHeight = underlineHeight;
|
||||||
|
this.underlineClearGap = underlineClearGap;
|
||||||
|
stroke.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
stroke.setStrokeCap(Paint.Cap.BUTT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it possible that LineBackgroundSpan is not receiving proper spans? like typeface?
|
||||||
|
// it complicates things (like the need to have own copy of paint)
|
||||||
|
|
||||||
|
// is it possible that LineBackgroundSpan is called constantly even in a TextView?
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawBackground(
|
||||||
|
Canvas c,
|
||||||
|
Paint p,
|
||||||
|
int left,
|
||||||
|
int right,
|
||||||
|
int top,
|
||||||
|
int baseline,
|
||||||
|
int bottom,
|
||||||
|
CharSequence text,
|
||||||
|
int start,
|
||||||
|
int end,
|
||||||
|
int lnum
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Debug.trace();
|
||||||
|
|
||||||
|
final Spanned spanned = (Spanned) text;
|
||||||
|
final TextView textView = TextViewSpan.textViewOf(spanned);
|
||||||
|
|
||||||
|
if (textView == null) {
|
||||||
|
// TextView is required
|
||||||
|
Log.e("EU", "no text view");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Layout layout;
|
||||||
|
{
|
||||||
|
// check if there is dedicated layout, if not, use from textView
|
||||||
|
// (think tableRowSpan that uses own Layout)
|
||||||
|
final Layout layoutFromSpan = TextLayoutSpan.layoutOf(spanned);
|
||||||
|
if (layoutFromSpan != null) {
|
||||||
|
layout = layoutFromSpan;
|
||||||
|
} else {
|
||||||
|
layout = textView.getLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout == null) {
|
||||||
|
// we could call `p.setUnderlineText(true)` here a fallback,
|
||||||
|
// but this would make __all__ text in a TextView underlined, which is not
|
||||||
|
// what we want
|
||||||
|
Log.e("EU", "no layout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempTextPaint.set((TextPaint) p);
|
||||||
|
|
||||||
|
// we must use _selfStart_ because underline can start **not** at the beginning of a line.
|
||||||
|
// as we are using LineBackground `start` would indicate the start position of the line
|
||||||
|
// and not start of the span (self). The same goes for selfEnd (ended before line)
|
||||||
|
final int selfStart = spanned.getSpanStart(this);
|
||||||
|
final int selfEnd = spanned.getSpanEnd(this);
|
||||||
|
|
||||||
|
final int s = max(selfStart, start);
|
||||||
|
|
||||||
|
// all lines should use (end - 1) to receive proper line end coordinate X,
|
||||||
|
// unless it is last line in _layout_
|
||||||
|
final boolean isLastLine = lnum == (layout.getLineCount() - 1);
|
||||||
|
final int e = min(selfEnd, end - (isLastLine ? 0 : 1));
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
Log.e("EU", String.format("lnum: %s, hash: %s, text: '%s'",
|
||||||
|
lnum, text.subSequence(s, e).hashCode(), text.subSequence(s, e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int leading;
|
||||||
|
final int trailing;
|
||||||
|
{
|
||||||
|
final int l = (int) (layout.getPrimaryHorizontal(s) + .5F);
|
||||||
|
final int r = (int) (layout.getPrimaryHorizontal(e) + .5F);
|
||||||
|
leading = min(l, r);
|
||||||
|
trailing = max(l, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
underline.rewind();
|
||||||
|
|
||||||
|
// middle between baseline and descent
|
||||||
|
final int diff = (int) (p.descent() / 2F + .5F);
|
||||||
|
|
||||||
|
underline.addRect(
|
||||||
|
leading, baseline + diff,
|
||||||
|
trailing, baseline + diff + underlineHeight(textView),
|
||||||
|
Path.Direction.CW
|
||||||
|
);
|
||||||
|
|
||||||
|
outline.rewind();
|
||||||
|
|
||||||
|
final int charsLength = e - s;
|
||||||
|
final char[] chars = charCache.chars(charsLength);
|
||||||
|
TextUtils.getChars(spanned, s, e, chars, 0);
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
final MetricAffectingSpan[] metricAffectingSpans = spanned.getSpans(s, e, MetricAffectingSpan.class);
|
||||||
|
// Log.e("EU", Arrays.toString(metricAffectingSpans));
|
||||||
|
for (MetricAffectingSpan span : metricAffectingSpans) {
|
||||||
|
span.updateMeasureState(tempTextPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: styleSpan
|
||||||
|
// todo all other spans (maybe UpdateMeasureSpans?)
|
||||||
|
tempTextPaint.getTextPath(
|
||||||
|
chars,
|
||||||
|
0, charsLength,
|
||||||
|
leading, baseline,
|
||||||
|
outline
|
||||||
|
);
|
||||||
|
|
||||||
|
outline.op(underline, Path.Op.INTERSECT);
|
||||||
|
|
||||||
|
strokedOutline.rewind();
|
||||||
|
stroke.setStrokeWidth(underlineClearGap(textView));
|
||||||
|
stroke.getFillPath(outline, strokedOutline);
|
||||||
|
|
||||||
|
underline.op(strokedOutline, Path.Op.DIFFERENCE);
|
||||||
|
|
||||||
|
c.drawPath(underline, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int underlineHeight(@NonNull TextView textView) {
|
||||||
|
if (underlineHeight > 0) {
|
||||||
|
return underlineHeight;
|
||||||
|
}
|
||||||
|
return (int) (DEFAULT_UNDERLINE_HEIGHT_DIP * textView.getResources().getDisplayMetrics().density + 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int underlineClearGap(@NonNull TextView textView) {
|
||||||
|
if (underlineClearGap > 0) {
|
||||||
|
return underlineClearGap;
|
||||||
|
}
|
||||||
|
return (int) (DEFAULT_UNDERLINE_CLEAR_GAP_DIP * textView.getResources().getDisplayMetrics().density + 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// primitive cache that grows internal array (never shrinks, nor clear buffer)
|
||||||
|
// TODO: but... each span has own instance, so not much of the memory saving
|
||||||
|
private static class CharCache {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
char[] chars(int ofLength) {
|
||||||
|
final char[] out;
|
||||||
|
if (chars == null || chars.length < ofLength) {
|
||||||
|
out = chars = new char[ofLength];
|
||||||
|
} else {
|
||||||
|
out = chars;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private char[] chars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,18 @@
|
|||||||
package io.noties.markwon.sample.html;
|
package io.noties.markwon.sample.html;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.AbsoluteSizeSpan;
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
import android.text.style.AlignmentSpan;
|
import android.text.style.AlignmentSpan;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
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;
|
||||||
@ -23,6 +23,7 @@ import io.noties.markwon.MarkwonConfiguration;
|
|||||||
import io.noties.markwon.MarkwonVisitor;
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
import io.noties.markwon.RenderProps;
|
import io.noties.markwon.RenderProps;
|
||||||
import io.noties.markwon.SpannableBuilder;
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
import io.noties.markwon.html.HtmlEmptyTagReplacement;
|
||||||
import io.noties.markwon.html.HtmlPlugin;
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
import io.noties.markwon.html.HtmlTag;
|
import io.noties.markwon.html.HtmlTag;
|
||||||
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
@ -42,7 +43,10 @@ public class HtmlActivity extends ActivityWithMenuOptions {
|
|||||||
.add("align", this::align)
|
.add("align", this::align)
|
||||||
.add("randomCharSize", this::randomCharSize)
|
.add("randomCharSize", this::randomCharSize)
|
||||||
.add("enhance", this::enhance)
|
.add("enhance", this::enhance)
|
||||||
.add("image", this::image);
|
.add("image", this::image)
|
||||||
|
// .add("elegantUnderline", this::elegantUnderline)
|
||||||
|
.add("iframe", this::iframe)
|
||||||
|
.add("emptyTagReplacement", this::emptyTagReplacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextView textView;
|
private TextView textView;
|
||||||
@ -57,7 +61,7 @@ public class HtmlActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
textView = findViewById(R.id.text_view);
|
textView = findViewById(R.id.text_view);
|
||||||
|
|
||||||
align();
|
emptyTagReplacement();
|
||||||
}
|
}
|
||||||
|
|
||||||
// we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content
|
// we can use `SimpleTagHandler` for _simple_ cases (when the whole tag content
|
||||||
@ -268,4 +272,86 @@ public class HtmlActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void elegantUnderline() {
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Elegant underline is supported on KitKat and up",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String underline = "Well well wel, and now <u>Gogogo, quite **perfect** yeah</u> and nice and elegant";
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
underline + "\n\n" +
|
||||||
|
"<b>" + underline + "</b>\n\n" +
|
||||||
|
"<font name=serif>" + underline + "</font>\n\n" +
|
||||||
|
"<font name=sans-serif>" + underline + underline + underline + "</font>\n\n" +
|
||||||
|
"<font name=monospace>" + underline + "</font>\n\n" +
|
||||||
|
"";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(HtmlPlugin.create(plugin -> plugin
|
||||||
|
.addHandler(new HtmlFontTagHandler())
|
||||||
|
.addHandler(new HtmlElegantUnderlineTagHandler())))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void iframe() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Hello iframe\n\n" +
|
||||||
|
"<p class=\"p1\"><img title=\"JUMP FORCE\" src=\"https://img1.ak.crunchyroll.com/i/spire1/f0c009039dd9f8dff5907fff148adfca1587067000_full.jpg\" alt=\"JUMP FORCE\" width=\"640\" height=\"362\" /></p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\">Switch owners will soon get to take part in the ultimate <em>Shonen Jump </em>rumble. Bandai Namco announced plans to bring <strong><em>Jump Force </em></strong>to <strong>Switch</strong> as <strong><em>Jump Force Deluxe Edition</em></strong>, with a release set for sometime this year. This version will include all of the original playable characters and those from Character Pass 1, and <strong>Character Pass 2 is also in the works </strong>for all versions, starting with <strong>Shoto Todoroki from </strong><span style=\"color: #ff9900;\"><a href=\"/my-hero-academia?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><strong><em>My Hero Academia</em></strong></span></a></span>.</p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\">Other than Todoroki, Bandai Namco hinted that the four other Character Pass 2 characters will hail from <span style=\"color: #ff9900;\"><a href=\"/hunter-x-hunter?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Hunter x Hunter</em></span></a></span>, <em>Yu Yu Hakusho</em>, <span style=\"color: #ff9900;\"><a href=\"/bleach?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>Bleach</em></span></a></span>, and <span style=\"color: #ff9900;\"><a href=\"/jojos-bizarre-adventure?utm_source=editorial_cr&utm_medium=news&utm_campaign=article_driven&referrer=editorial_cr_news_article_driven\"><span style=\"color: #ff9900;\"><em>JoJo's Bizarre Adventure</em></span></a></span>. Character Pass 2 will be priced at $17.99, and Todoroki launches this spring.<span class=\"Apple-converted-space\"> </span></p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/At1qTj-LWCc\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\">Character Pass 2 promo:</p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\"><iframe style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://www.youtube.com/embed/CukwN6kV4R4\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe></p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><img style=\"display: block; margin-left: auto; margin-right: auto;\" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1587067041_full.png\" alt=\"\" width=\"640\" height=\"43\" /></a></p>\n" +
|
||||||
|
"<p class=\"p2\"> </p>\n" +
|
||||||
|
"<p class=\"p1\">-------</p>\n" +
|
||||||
|
"<p class=\"p1\"><em>Joseph Luster is the Games and Web editor at </em><a href=\"http://www.otakuusamagazine.com/ME2/Default.asp\"><em>Otaku USA Magazine</em></a><em>. You can read his webcomic, </em><a href=\"http://subhumanzoids.com/comics/big-dumb-fighting-idiots/\">BIG DUMB FIGHTING IDIOTS</a><em> at </em><a href=\"http://subhumanzoids.com/\"><em>subhumanzoids</em></a><em>. Follow him on Twitter </em><a href=\"https://twitter.com/Moldilox\"><em>@Moldilox</em></a><em>.</em><span class=\"Apple-converted-space\"> </span></p>";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(HtmlPlugin.create())
|
||||||
|
.usePlugin(new IFrameHtmlPlugin())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emptyTagReplacement() {
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
"<empty></empty> the `<empty></empty>` is replaced?";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(HtmlPlugin.create(plugin -> {
|
||||||
|
plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String replace(@NonNull HtmlTag tag) {
|
||||||
|
if ("empty".equals(tag.name())) {
|
||||||
|
return "REPLACED_EMPTY_WITH_IT";
|
||||||
|
}
|
||||||
|
return super.replace(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package io.noties.markwon.sample.html;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
import io.noties.markwon.html.HtmlTag;
|
||||||
|
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
|
import io.noties.markwon.html.TagHandler;
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
public class HtmlElegantUnderlineTagHandler extends TagHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) {
|
||||||
|
if (tag.isBlock()) {
|
||||||
|
visitChildren(visitor, renderer, tag.getAsBlock());
|
||||||
|
}
|
||||||
|
SpannableBuilder.setSpans(
|
||||||
|
visitor.builder(),
|
||||||
|
ElegantUnderlineSpan.create(),
|
||||||
|
tag.start(),
|
||||||
|
tag.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTags() {
|
||||||
|
return Collections.singleton("u");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package io.noties.markwon.sample.html;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.TypefaceSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
import io.noties.markwon.html.HtmlTag;
|
||||||
|
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
|
import io.noties.markwon.html.TagHandler;
|
||||||
|
|
||||||
|
public class HtmlFontTagHandler extends TagHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) {
|
||||||
|
|
||||||
|
if (tag.isBlock()) {
|
||||||
|
visitChildren(visitor, renderer, tag.getAsBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String font = tag.attributes().get("name");
|
||||||
|
if (!TextUtils.isEmpty(font)) {
|
||||||
|
SpannableBuilder.setSpans(
|
||||||
|
visitor.builder(),
|
||||||
|
new TypefaceSpan(font),
|
||||||
|
tag.start(),
|
||||||
|
tag.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTags() {
|
||||||
|
return Collections.singleton("font");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package io.noties.markwon.sample.html;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.node.Image;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.noties.debug.Debug;
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.MarkwonConfiguration;
|
||||||
|
import io.noties.markwon.RenderProps;
|
||||||
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
|
import io.noties.markwon.html.HtmlTag;
|
||||||
|
import io.noties.markwon.html.tag.SimpleTagHandler;
|
||||||
|
import io.noties.markwon.image.ImageProps;
|
||||||
|
import io.noties.markwon.image.ImageSize;
|
||||||
|
|
||||||
|
public class IFrameHtmlPlugin extends AbstractMarkwonPlugin {
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
registry.require(HtmlPlugin.class, htmlPlugin -> {
|
||||||
|
// TODO: empty tag replacement
|
||||||
|
htmlPlugin.addHandler(new EmbedTagHandler());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EmbedTagHandler extends SimpleTagHandler {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
|
||||||
|
final ImageSize imageSize = new ImageSize(new ImageSize.Dimension(640, "px"), new ImageSize.Dimension(480, "px"));
|
||||||
|
ImageProps.IMAGE_SIZE.set(renderProps, imageSize);
|
||||||
|
ImageProps.DESTINATION.set(renderProps, "https://hey.com/1.png");
|
||||||
|
return configuration.spansFactory().require(Image.class)
|
||||||
|
.getSpans(configuration, renderProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTags() {
|
||||||
|
return Collections.singleton("iframe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,14 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
||||||
|
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
||||||
import org.commonmark.node.Block;
|
import org.commonmark.node.Block;
|
||||||
import org.commonmark.node.BlockQuote;
|
import org.commonmark.node.BlockQuote;
|
||||||
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
import org.commonmark.node.Heading;
|
import org.commonmark.node.Heading;
|
||||||
import org.commonmark.node.HtmlBlock;
|
import org.commonmark.node.HtmlBlock;
|
||||||
|
import org.commonmark.node.IndentedCodeBlock;
|
||||||
import org.commonmark.node.ListBlock;
|
import org.commonmark.node.ListBlock;
|
||||||
import org.commonmark.node.ThematicBreak;
|
import org.commonmark.node.ThematicBreak;
|
||||||
import org.commonmark.parser.InlineParserFactory;
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
@ -22,7 +26,9 @@ import java.util.Set;
|
|||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
|
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
|
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||||
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
|
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
|
||||||
@ -41,7 +47,9 @@ public class InlineParserActivity extends ActivityWithMenuOptions {
|
|||||||
.add("links_only", this::links_only)
|
.add("links_only", this::links_only)
|
||||||
.add("disable_code", this::disable_code)
|
.add("disable_code", this::disable_code)
|
||||||
.add("pluginWithDefaults", this::pluginWithDefaults)
|
.add("pluginWithDefaults", this::pluginWithDefaults)
|
||||||
.add("pluginNoDefaults", this::pluginNoDefaults);
|
.add("pluginNoDefaults", this::pluginNoDefaults)
|
||||||
|
.add("disableHtmlInlineParser", this::disableHtmlInlineParser)
|
||||||
|
.add("disableHtmlSanitize", this::disableHtmlSanitize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -173,4 +181,67 @@ public class InlineParserActivity extends ActivityWithMenuOptions {
|
|||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void disableHtmlInlineParser() {
|
||||||
|
final String md = "# Html <b>disabled</b>\n\n" +
|
||||||
|
"<em>emphasis <strong>strong</strong>\n\n" +
|
||||||
|
"<p>paragraph <img src='hey.jpg' /></p>\n\n" +
|
||||||
|
"<test></test>\n\n" +
|
||||||
|
"<test>";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configure(@NonNull Registry registry) {
|
||||||
|
// NB! `AsteriskDelimiterProcessor` and `UnderscoreDelimiterProcessor`
|
||||||
|
// handles both emphasis and strong-emphasis nodes
|
||||||
|
registry.require(MarkwonInlineParserPlugin.class, plugin -> {
|
||||||
|
plugin.factoryBuilder()
|
||||||
|
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||||
|
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||||
|
.excludeInlineProcessor(OpenBracketInlineProcessor.class)
|
||||||
|
.excludeDelimiterProcessor(AsteriskDelimiterProcessor.class)
|
||||||
|
.excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.enabledBlockTypes(new HashSet<>(Arrays.asList(
|
||||||
|
Heading.class,
|
||||||
|
// HtmlBlock.class,
|
||||||
|
ThematicBreak.class,
|
||||||
|
FencedCodeBlock.class,
|
||||||
|
IndentedCodeBlock.class,
|
||||||
|
BlockQuote.class,
|
||||||
|
ListBlock.class
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableHtmlSanitize() {
|
||||||
|
final String md = "# Html <b>disabled</b>\n\n" +
|
||||||
|
"<em>emphasis <strong>strong</strong>\n\n" +
|
||||||
|
"<p>paragraph <img src='hey.jpg' /></p>\n\n" +
|
||||||
|
"<test></test>\n\n" +
|
||||||
|
"<test>";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String processMarkdown(@NonNull String markdown) {
|
||||||
|
return markdown
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,8 @@ public class LatexActivity extends ActivityWithMenuOptions {
|
|||||||
.add("textColor", this::textColor)
|
.add("textColor", this::textColor)
|
||||||
.add("defaultTextColor", this::defaultTextColor)
|
.add("defaultTextColor", this::defaultTextColor)
|
||||||
.add("inlineAndBlock", this::inlineAndBlock)
|
.add("inlineAndBlock", this::inlineAndBlock)
|
||||||
.add("dark", this::dark);
|
.add("dark", this::dark)
|
||||||
|
.add("omega", this::omega);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -221,6 +222,18 @@ public class LatexActivity extends ActivityWithMenuOptions {
|
|||||||
renderWithBlocksAndInlines(md);
|
renderWithBlocksAndInlines(md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void omega() {
|
||||||
|
final String md = "" +
|
||||||
|
"# Block\n\n" +
|
||||||
|
"$$\n" +
|
||||||
|
"\\Omega\n" +
|
||||||
|
"$$\n\n" +
|
||||||
|
"# Inline\n\n" +
|
||||||
|
"$$\\Omega$$";
|
||||||
|
|
||||||
|
renderWithBlocksAndInlines(md);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String wrapLatexInSampleMarkdown(@NonNull String latex) {
|
private static String wrapLatexInSampleMarkdown(@NonNull String latex) {
|
||||||
return "" +
|
return "" +
|
||||||
|
@ -34,6 +34,8 @@ import io.noties.markwon.MarkwonVisitor;
|
|||||||
import io.noties.markwon.core.CorePlugin;
|
import io.noties.markwon.core.CorePlugin;
|
||||||
import io.noties.markwon.html.HtmlPlugin;
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
import io.noties.markwon.image.ImagesPlugin;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.image.destination.ImageDestinationProcessor;
|
||||||
|
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute;
|
||||||
import io.noties.markwon.image.file.FileSchemeHandler;
|
import io.noties.markwon.image.file.FileSchemeHandler;
|
||||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
||||||
import io.noties.markwon.image.svg.SvgMediaDecoder;
|
import io.noties.markwon.image.svg.SvgMediaDecoder;
|
||||||
@ -42,8 +44,6 @@ import io.noties.markwon.recycler.SimpleEntry;
|
|||||||
import io.noties.markwon.recycler.table.TableEntry;
|
import io.noties.markwon.recycler.table.TableEntry;
|
||||||
import io.noties.markwon.recycler.table.TableEntryPlugin;
|
import io.noties.markwon.recycler.table.TableEntryPlugin;
|
||||||
import io.noties.markwon.sample.R;
|
import io.noties.markwon.sample.R;
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessor;
|
|
||||||
import io.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
|
||||||
|
|
||||||
public class RecyclerActivity extends Activity {
|
public class RecyclerActivity extends Activity {
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ public class RecyclerActivity extends Activity {
|
|||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
@Override
|
@Override
|
||||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||||
builder.urlProcessor(new UrlProcessorInitialReadme());
|
builder.imageDestinationProcessor(new ImageDestinationProcessorInitialReadme());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -182,12 +182,12 @@ public class RecyclerActivity extends Activity {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UrlProcessorInitialReadme implements UrlProcessor {
|
private static class ImageDestinationProcessorInitialReadme extends ImageDestinationProcessor {
|
||||||
|
|
||||||
private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/";
|
private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/";
|
||||||
|
|
||||||
private final UrlProcessorRelativeToAbsolute processor
|
private final ImageDestinationProcessorRelativeToAbsolute processor
|
||||||
= new UrlProcessorRelativeToAbsolute(GITHUB_BASE);
|
= new ImageDestinationProcessorRelativeToAbsolute(GITHUB_BASE);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
@ -7,10 +7,11 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
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.tables.TablePlugin;
|
import io.noties.markwon.ext.tables.TablePlugin;
|
||||||
import io.noties.markwon.ext.tables.TableTheme;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
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.ActivityWithMenuOptions;
|
||||||
import io.noties.markwon.sample.MenuOptions;
|
import io.noties.markwon.sample.MenuOptions;
|
||||||
@ -25,7 +26,9 @@ public class TableActivity extends ActivityWithMenuOptions {
|
|||||||
public MenuOptions menuOptions() {
|
public MenuOptions menuOptions() {
|
||||||
return MenuOptions.create()
|
return MenuOptions.create()
|
||||||
.add("customize", this::customize)
|
.add("customize", this::customize)
|
||||||
.add("tableAndLinkify", this::tableAndLinkify);
|
.add("tableAndLinkify", this::tableAndLinkify)
|
||||||
|
.add("withImages", this::withImages)
|
||||||
|
.add("withLatex", this::withLatex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextView textView;
|
private TextView textView;
|
||||||
@ -86,4 +89,47 @@ public class TableActivity extends ActivityWithMenuOptions {
|
|||||||
|
|
||||||
markwon.setMarkdown(textView, md);
|
markwon.setMarkdown(textView, md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void withImages() {
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
"| HEADER | HEADER |\n" +
|
||||||
|
"|:----:|:----:|\n" +
|
||||||
|
"|  | Build |\n" +
|
||||||
|
"| Stable |  |\n" +
|
||||||
|
"| BIG |  |\n" +
|
||||||
|
"\n";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(TablePlugin.create(this))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withLatex() {
|
||||||
|
|
||||||
|
String latex = "\\begin{array}{cc}";
|
||||||
|
latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
|
||||||
|
latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
|
||||||
|
latex += "\\end{array}";
|
||||||
|
|
||||||
|
final String md = "" +
|
||||||
|
"| HEADER | HEADER |\n" +
|
||||||
|
"|:----:|:----:|\n" +
|
||||||
|
"|  | Build |\n" +
|
||||||
|
"| Stable |  |\n" +
|
||||||
|
"| BIG | $$" + latex + "$$ |\n" +
|
||||||
|
"\n";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.inlinesEnabled(true)))
|
||||||
|
.usePlugin(TablePlugin.create(this))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
android:textColor="#000"
|
android:textColor="#000"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
android:padding="8dip"
|
||||||
tools:text="whatever" />
|
tools:text="whatever" />
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
Loading…
x
Reference in New Issue
Block a user