Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,3 +0,0 @@
|
||||
#repo: https://github.com/noties/Markwon
|
||||
|
||||
custom: ["https://paypal.me/dimitryivanov"]
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -1,4 +0,0 @@
|
||||
* **Markwon version**: _{REQUIRED}_
|
||||
|
||||
1. Please specify expected/actual behavior
|
||||
2. Please specify conditions/steps to reproduce (layout, code, markdown used, etc)
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@ -1,18 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build -Prelease -PCI --stacktrace
|
17
.github/workflows/pull-request.yml
vendored
17
.github/workflows/pull-request.yml
vendored
@ -1,17 +0,0 @@
|
||||
name: Pull request checks
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build -PCI --stacktrace
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,10 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea
|
||||
.DS_Store
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
**/build
|
||||
**/dist
|
||||
**/node_modules
|
475
CHANGELOG.md
475
CHANGELOG.md
@ -1,475 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
# 4.6.2
|
||||
|
||||
#### Added
|
||||
* `image` - `DefaultDownScalingMediaDecoder` which scales displayed images down ([#329])
|
||||
|
||||
[#329]: https://github.com/noties/Markwon/issues/329
|
||||
|
||||
|
||||
# 4.6.1
|
||||
|
||||
#### Changed
|
||||
* `core` - `CustomTypefaceSpan` new `mergeStyles` functionality and new factory method([#298])<br>Thanks [@c-b-h]
|
||||
* `image-coil` - update `Coil` to `0.13.0` ([#303])<br>Thanks [@ubuntudroid]
|
||||
|
||||
#### Deprecated
|
||||
* `core` - `CustomTypefaceSpan(Typeface)` constructor, use `CustomTypefaceSpan.create(Typeface)`
|
||||
or `CustomTypefaceSpan.create(Typeface, boolean)` factory methods instead
|
||||
|
||||
|
||||
[#298]: https://github.com/noties/Markwon/pull/298
|
||||
[#303]: https://github.com/noties/Markwon/pull/303
|
||||
|
||||
[@c-b-h]: https://github.com/c-b-h
|
||||
[@ubuntudroid]: https://github.com/ubuntudroid
|
||||
|
||||
|
||||
# 4.6.0
|
||||
|
||||
#### Added
|
||||
* `ext-tables` - `TableAwareMovementMethod` a special movement method to handle clicks inside tables ([#289])
|
||||
|
||||
#### Changed
|
||||
* `ext-tasklist` - changed implementation to be in line with GFM (Github flavored markdown),
|
||||
task list item is a regular list item (BulletList and OrderedList can contain it).
|
||||
Internal implementation changed from block parsing to node post processing ([#291])
|
||||
* `image-glide` - update to `4.11.0` version
|
||||
* `inline-parser` - revert parsing index when `InlineProcessor` returns `null` as result
|
||||
* `image-coil` - update `Coil` to `0.12.0` ([Coil changelog](https://coil-kt.github.io/coil/changelog/)) ([#284])<br>Thanks [@magnusvs]
|
||||
|
||||
[#284]: https://github.com/noties/Markwon/pull/284
|
||||
[#289]: https://github.com/noties/Markwon/issues/289
|
||||
[#291]: https://github.com/noties/Markwon/issues/291
|
||||
|
||||
[@magnusvs]: https://github.com/magnusvs
|
||||
|
||||
|
||||
# 4.5.1
|
||||
|
||||
#### Changed
|
||||
* `image-coil` - use `coil-base` as `api` dependency (would require explicit `coil` dependency) ([#274])
|
||||
|
||||
#### Fixed
|
||||
* `image-coil` - deliver image result if it loaded before request disposable is created ([#272])
|
||||
* `ext-tables` - fix column width rounding issue
|
||||
|
||||
[#272]: https://github.com/noties/Markwon/issues/272
|
||||
[#274]: https://github.com/noties/Markwon/issues/274
|
||||
|
||||
|
||||
# 4.5.0
|
||||
|
||||
#### Added
|
||||
* `core` - `MovementMethodPlugin.none()`, `MovementMethodPlugin.link()` factory methods
|
||||
* `core` - `CorePlugin` `hasExplicitMovementMethod` configuration method to **not** add implicit `LinkMovementMethod` in `afterSetText`
|
||||
* `core` - `MarkwonTheme` `isLinkedUnderlined` attribute for links([#270])<br>Thanks to [@dallasgutauckis]
|
||||
* `ext-latex` - `JLatexMathTheme.Padding.of(int,int,int,int)` factory method
|
||||
* `app-sample` - example application
|
||||
|
||||
#### Changed
|
||||
* `html` - `SimpleTagHandler` visits children tags if supplied tag is block one ([#235])
|
||||
* `inline-parser` - `BangInlineProcessor` properly returns `null` if no image node is found (possible to define other inline parsers that use `!` as special character)
|
||||
* `image` - `AsyncDrawable` won't trigger loading if it has result (aim: `RecyclerView` due to multiple attach/detach events of a View)
|
||||
* `image` - `AsyncDrawable` will resume result if it is `Animatable` and was playing before detach event (aim: `RecyclerView`) ([#241])
|
||||
* `core` - `MarkwonReducer` filter out `LinkReferenceDefinition` nodes
|
||||
|
||||
#### Fixed
|
||||
* `image-glide` cache `RequestManager` in `GlideImagesPlugin#create(Context)` factory method ([#259])
|
||||
|
||||
#### Deprecated
|
||||
* `core` - `MovementMethodPlugin.create()` use explicit `MovementMethodPlugin.link()` instead
|
||||
|
||||
#### Removed
|
||||
* `image` - `AsyncDrawable#hasKnownDimentions` (deprecated in `4.2.1`)
|
||||
* `app` and `sample` applications (merged together in a `app-sample` single app)
|
||||
|
||||
[#235]: https://github.com/noties/Markwon/issues/235
|
||||
[#241]: https://github.com/noties/Markwon/issues/241
|
||||
[#259]: https://github.com/noties/Markwon/issues/259
|
||||
[#270]: https://github.com/noties/Markwon/pull/270
|
||||
|
||||
[@dallasgutauckis]: https://github.com/dallasgutauckis
|
||||
|
||||
|
||||
# 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
|
||||
* Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone]
|
||||
* module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name
|
||||
* `ext-table`: fix links in tables ([#224])
|
||||
* `ext-table`: proper borders (equal for all sides)
|
||||
* module `core`: Add `PrecomputedFutureTextSetterCompat`<br>Thanks [@KirkBushman]
|
||||
|
||||
[#216]: https://github.com/noties/Markwon/pull/216
|
||||
[#224]: https://github.com/noties/Markwon/issues/224
|
||||
[@francescocervone]: https://github.com/francescocervone
|
||||
[@KirkBushman]: https://github.com/KirkBushman
|
||||
|
||||
|
||||
# 4.3.0
|
||||
* add `MarkwonInlineParserPlugin` in `inline-parser` module
|
||||
* `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin`
|
||||
dependency (must be explicitly added to `Markwon` whilst configuring)
|
||||
* `JLatexMathPlugin`: add `theme` (to customize both inlines and blocks)
|
||||
* add `JLatexMathPlugin.ErrorHandler` to catch latex rendering errors and (optionally) display error drawable ([#204])
|
||||
* `JLatexMathPlugin` add text color customization ([#207])
|
||||
* `JLatexMathPlugin` will use text color of widget in which it is displayed **if color is not set explicitly**
|
||||
* add `SoftBreakAddsNewLinePlugin` plugin (`core` module)
|
||||
* `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75])
|
||||
* add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu
|
||||
* non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189])
|
||||
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])<br>Thanks to [@drakeet]
|
||||
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them
|
||||
|
||||
|
||||
```java
|
||||
// default usage: new blocks parser, no inlines
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(JLatexMathPlugin.create(textSize))
|
||||
.build();
|
||||
```
|
||||
|
||||
```java
|
||||
// legacy blocks (pre `4.3.0`) parsing, no inlines
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> builder.blocksLegacy(true)))
|
||||
.build();
|
||||
```
|
||||
|
||||
```java
|
||||
// new blocks parsing and inline parsing
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
|
||||
// blocksEnabled and blocksLegacy can be omitted
|
||||
builder
|
||||
.blocksEnabled(true)
|
||||
.blocksLegacy(false)
|
||||
.inlinesEnabled(true);
|
||||
}))
|
||||
.build();
|
||||
```
|
||||
|
||||
[#189]: https://github.com/noties/Markwon/issues/189
|
||||
[#75]: https://github.com/noties/Markwon/issues/75
|
||||
[#204]: https://github.com/noties/Markwon/issues/204
|
||||
[#207]: https://github.com/noties/Markwon/issues/207
|
||||
[#201]: https://github.com/noties/Markwon/issues/201
|
||||
[@drakeet]: https://github.com/drakeet
|
||||
|
||||
|
||||
# 4.2.2
|
||||
* Fixed `AsyncDrawable` display when it has placeholder with empty bounds ([#189])
|
||||
* Fixed `syntax-highlight` where code input is empty string ([#192])
|
||||
* Add `appendFactory`/`prependFactory` in `MarkwonSpansFactory.Builder` for more explicit `SpanFactory` ordering ([#193])
|
||||
|
||||
[#189]: https://github.com/noties/Markwon/issues/189
|
||||
[#192]: https://github.com/noties/Markwon/issues/192
|
||||
[#193]: https://github.com/noties/Markwon/issues/193
|
||||
|
||||
# 4.2.1
|
||||
* Fix SpannableBuilder `subSequence` method
|
||||
* Introduce Nougat check in `BulletListItemSpan` to position bullet (for bullets to be
|
||||
positioned correctly when nested inside other `LeadingMarginSpan`s)
|
||||
* Reduced number of invalidations in AsyncDrawable when result is ready
|
||||
* AsyncDrawable#hasKnownDimentions -> AsyncDrawable#hasKnownDimensions typo fix
|
||||
|
||||
# 4.2.0
|
||||
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
||||
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
||||
<br>Thanks to [@tylerbwong]
|
||||
* `MarkwonInlineParser` to customize inline parsing (new module: `markwon-inline-parser`)
|
||||
* Update commonmark-java to `0.13.0` (and commonmark spec `0.29`)
|
||||
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
|
||||
* `HeadingSpan#getLevel` getter
|
||||
* Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165])
|
||||
* `LinkSpan#getLink` method
|
||||
* `LinkifyPlugin` applies link span that is configured by `Markwon` (obtain via span factory)
|
||||
* `LinkifyPlugin` is thread-safe
|
||||
|
||||
[@tylerbwong]: https://github.com/tylerbwong
|
||||
[Coil]: https://github.com/coil-kt/coil
|
||||
[#165]: https://github.com/noties/Markwon/issues/165
|
||||
[#166]: https://github.com/noties/Markwon/issues/166
|
||||
[#174]: https://github.com/noties/Markwon/pull/174
|
||||
|
||||
# 4.1.2
|
||||
* Do not re-use RenderProps when creating a new visitor (fixes [#171])
|
||||
|
||||
[#171]: https://github.com/noties/Markwon/issues/171
|
||||
|
||||
# 4.1.1
|
||||
* `markwon-ext-tables`: fix padding between subsequent table blocks ([#159])
|
||||
* `markwon-images`: print a single warning instead full stacktrace in case when SVG or GIF
|
||||
are not present in the classpath ([#160])
|
||||
* Make `Markwon` instance thread-safe by using a single `MarkwonVisitor` for each `render` call ([#157])
|
||||
* Add `CoreProps.CODE_BLOCK_INFO` with code-block info (language)
|
||||
|
||||
[#159]: https://github.com/noties/Markwon/issues/159
|
||||
[#160]: https://github.com/noties/Markwon/issues/160
|
||||
[#157]: https://github.com/noties/Markwon/issues/157
|
||||
|
||||
# 4.1.0
|
||||
* Add `Markwon.TextSetter` interface to be able to use PrecomputedText/PrecomputedTextCompat
|
||||
* Add `PrecomputedTextSetterCompat` and `compileOnly` dependency on `androidx.core:core`
|
||||
(clients must have this dependency in the classpath)
|
||||
* Add `requirePlugin(Class)` and `getPlugins` for `Markwon` instance
|
||||
* TablePlugin -> defer table invalidation (via `View.post`), so only one invalidation
|
||||
happens with each draw-call
|
||||
* AsyncDrawableSpan -> defer invalidation
|
||||
|
||||
# 4.0.2
|
||||
* Fix `JLatexMathPlugin` formula placeholder (cannot have line breaks) ([#149])
|
||||
* Fix `JLatexMathPlugin` to update resulting formula bounds when `fitCanvas=true` and
|
||||
formula exceed canvas width (scale down keeping formula width/height ratio)
|
||||
|
||||
[#149]: https://github.com/noties/Markwon/issues/149
|
||||
|
||||
# 4.0.1
|
||||
* Fix `JLatexMathPlugin` (background-provider null) ([#147])
|
||||
|
||||
[#147]: https://github.com/noties/Markwon/issues/147
|
||||
|
||||
# 4.0.0
|
||||
* maven group-id change to `io.noties.markwon` (was `ru.noties.markwon`)
|
||||
* package name change to `io.notier.markwon.*` (was `ru.noties.markwon.*`)
|
||||
* androidx artifacts ([#76])
|
||||
* `Markwon#builder` does not require explicit `CorePlugin` (added automatically),
|
||||
use `Markwon#builderNoCore()` to obtain a builder without `CorePlugin`
|
||||
* Removed `Priority` abstraction and `MarkwonPlugin#priority` (use `MarkwonPlugin.Registry`)
|
||||
* Removed `MarkwonPlugin#configureHtmlRenderer` (for configuration use `HtmlPlugin` directly)
|
||||
* Removed `MarkwonPlugin#configureImages` (for configuration use `ImagesPlugin` directly)
|
||||
* Added `MarkwonPlugin.Registry` and `MarkwonPlugin#configure(Registry)` method
|
||||
* `CorePlugin#addOnTextAddedListener` (process raw text added)
|
||||
* `ImageSizeResolver` signature change (accept `AsyncDrawable`)
|
||||
* `LinkResolver` is now an independent entity (previously part of the `LinkSpan`), `LinkSpan.Resolver` -> `LinkResolver`
|
||||
* `AsyncDrawableScheduler` can now be called multiple times without performance penalty
|
||||
* `AsyncDrawable` now exposes its destination, image-size, last known dimensions (canvas, text-size)
|
||||
* `AsyncDrawableLoader` signature change (accept `AsyncDrawable`)
|
||||
* Add `LastLineSpacingSpan`
|
||||
* Add `MarkwonConfiguration.Builder#asyncDrawableLoader` method
|
||||
* `ImagesPlugin` removed from `core` artifact
|
||||
(also removed `images-gif`, `images-okhttp` and `images-svg` artifacts and their plugins)
|
||||
* `ImagesPlugin` exposes configuration (adding scheme-handler, media-decoder, etc)
|
||||
* `ImagesPlugin` allows multiple images with the same source (URL)
|
||||
* Add `PlaceholderProvider` and `ErrorHandler` to `ImagesPlugin`
|
||||
* `GIF` and `SVG` media-decoders are automatically added to `ImagesPlugin` if required libraries are found in the classpath
|
||||
* `ImageItem` is now abstract, has 2 implementations: `withResult`, `withDecodingNeeded`
|
||||
* Add `images-glide`, `images-picasso`, `linkify`, `simple-ext` modules
|
||||
* `JLatexMathPlugin` is now independent of `ImagesPlugin`
|
||||
* Fix wrong `JLatexMathPlugin` formulas sizes ([#138])
|
||||
* `JLatexMathPlugin` has `backgroundProvider`, `executorService` configuration
|
||||
* `HtmlPlugin` is self-contained (all configuration is moved in the plugin itself)
|
||||
|
||||
[#76]: https://github.com/noties/Markwon/issues/76
|
||||
[#138]: https://github.com/noties/Markwon/issues/138
|
||||
|
||||
# 3.1.0
|
||||
* `AsyncDrawable` exposes `ImageSize`, `ImageSizeResolver` and last known dimensions (canvas width and text size)
|
||||
* `AsyncDrawableLoader` `load` and `cancel` signatures change - both accept an `AsyncDrawable`
|
||||
* Fix for multiple images with the same source in `AsyncDrawableLoader`
|
||||
|
||||
With this release `Markwon` `3.x.x` version goes into maintenance mode.
|
||||
No new features will be added in `3.x.x` version, development is focused on `4.x.x` version.
|
||||
|
||||
|
||||
# 3.0.2
|
||||
* Fix `latex` plugin ([#136])
|
||||
* Add `#create(Call.Factory)` factory method to `OkHttpImagesPlugin` ([#129])
|
||||
<br>Thanks to [@ZacSweers]
|
||||
|
||||
[#136]: https://github.com/noties/Markwon/issues/136
|
||||
[#129]: https://github.com/noties/Markwon/issues/129
|
||||
[@ZacSweers]: https://github.com/ZacSweers
|
||||
|
||||
|
||||
# 3.0.1
|
||||
* Add `AsyncDrawableLoader.Builder#implementation` method ([#109])
|
||||
* AsyncDrawable allow placeholder to have independent size ([#115])
|
||||
* `addFactory` method for MarkwonSpansFactory
|
||||
* Add optional spans for list blocks (bullet and ordered)
|
||||
* AsyncDrawable placeholder bounds fix
|
||||
* SpannableBuilder setSpans allow array of arrays
|
||||
* Add `requireFactory` method to MarkwonSpansFactory
|
||||
* Add DrawableUtils
|
||||
|
||||
[#109]: https://github.com/noties/Markwon/issues/109
|
||||
[#115]: https://github.com/noties/Markwon/issues/115
|
||||
|
||||
|
||||
# 3.0.0
|
||||
* Plugins, plugins, plugins
|
||||
* Split basic functionality blocks into standalone modules
|
||||
* Maven artifacts group changed to `ru.noties.markwon` (previously had been `ru.noties`)
|
||||
* removed `markwon`, `markwon-image-loader`, `markwon-html-pareser-api`, `markwon-html-parser-impl`, `markwon-view` modules
|
||||
* new module system: `core`, `ext-latex`, `ext-strikethrough`, `ext-tables`, `ext-tasklist`, `html`, `image-gif`, `image-okhttp`, `image-svg`, `recycler`, `recycler-table`, `syntax-highlight`
|
||||
* Add BufferType option for Markwon configuration
|
||||
* Fix typo in AsyncDrawable waitingForDimensions
|
||||
* New tests format
|
||||
* `Markwon.render` returns `Spanned` instance of generic `CharSequence`
|
||||
* LinkMovementMethod is applied implicitly if not set on a TextView explicitly
|
||||
* Split code and codeBlock spans and factories
|
||||
* Add CustomTypefaceSpan
|
||||
* Add NoCopySpansFactory
|
||||
* Add placeholder to image loading
|
||||
|
||||
Generally speaking there are a lot of changes. Most of them are not backwards-compatible.
|
||||
The main point of this release is the `Plugin` system that allows more fluent configuration
|
||||
and opens the possibility of extending `Markwon` with 3rd party functionality in a simple
|
||||
and intuitive fashion. Please refer to the [documentation web-site](https://noties.github.io/Markwon)
|
||||
that has information on how to start migration.
|
||||
|
||||
The shortest excerpt of this release can be expressed like this:
|
||||
|
||||
```java
|
||||
// previous v2.x.x way
|
||||
Markwon.setMarkdown(textView, "**Hello there!**");
|
||||
```
|
||||
|
||||
```java
|
||||
// 3.x.x
|
||||
Markwon.create(context)
|
||||
.setMarkdown(textView, "**Hello there!**");
|
||||
```
|
||||
|
||||
But there is much more to it, please visit documentation web-site
|
||||
to get the full picture of latest changes.
|
||||
|
||||
# 2.0.1
|
||||
* `SpannableMarkdownVisitor` Rename blockQuoteIndent to blockIndent
|
||||
* Fixed block new lines logic for block quote and paragraph ([#82])
|
||||
* AsyncDrawable fix no dimensions bug ([#81])
|
||||
* Update SpannableTheme to use Px instead of Dimension annotation
|
||||
* Allow TaskListSpan isDone mutation
|
||||
* Updated commonmark-java to 0.12.1
|
||||
* Add OrderedListItemSpan measure utility method ([#78])
|
||||
* Add SpannableBuilder#getSpans method
|
||||
* Fix DataUri scheme handler in image-loader ([#74])
|
||||
* Introduced a "copy" builder for SpannableThem
|
||||
<br>Thanks [@c-b-h]
|
||||
|
||||
[#82]: https://github.com/noties/Markwon/issues/82
|
||||
[#81]: https://github.com/noties/Markwon/issues/81
|
||||
[#78]: https://github.com/noties/Markwon/issues/78
|
||||
[#74]: https://github.com/noties/Markwon/issues/74
|
||||
[@c-b-h]: https://github.com/c-b-h
|
||||
|
||||
|
||||
# 2.0.0
|
||||
* Add `html-parser-api` and `html-parser-impl` modules
|
||||
* Add `HtmlEmptyTagReplacement`
|
||||
* Implement Appendable and CharSequence in SpannableBuilder
|
||||
* Renamed library modules to reflect maven artifact names
|
||||
* Rename `markwon-syntax` to `markwon-syntax-highlight`
|
||||
* Add HtmlRenderer asbtraction
|
||||
* Add CssInlineStyleParser
|
||||
* Fix Theme#listItemColor and OL
|
||||
* Fix task list block parser to revert parsing state when line is not matching
|
||||
* Defined test format files
|
||||
* image-loader add datauri parser
|
||||
* image-loader add support for inline data uri image references
|
||||
* Add travis configuration
|
||||
* Fix image with width greater than canvas scaled
|
||||
* Fix blockquote span
|
||||
* Dealing with white spaces at the end of a document
|
||||
* image-loader add SchemeHandler abstraction
|
||||
* Add sample-latex-math module
|
||||
|
||||
# 1.1.1
|
||||
* Fix OrderedListItemSpan text position (baseline) ([#55])
|
||||
* Add softBreakAddsNewLine option for SpannableConfiguration ([#54])
|
||||
* Paragraph text can now explicitly be spanned ([#58])
|
||||
<br>Thanks to [@c-b-h]
|
||||
* Fix table border color if odd background is specified ([#56])
|
||||
* Add table customizations (even and header rows)
|
||||
|
||||
[#55]: https://github.com/noties/Markwon/issues/55
|
||||
[#54]: https://github.com/noties/Markwon/issues/54
|
||||
[#58]: https://github.com/noties/Markwon/issues/58
|
||||
[#56]: https://github.com/noties/Markwon/issues/56
|
||||
[@c-b-h]: https://github.com/c-b-h
|
||||
|
||||
|
||||
# 1.1.0
|
||||
* Update commonmark to 0.11.0 and android-gif to 1.2.14
|
||||
* Add syntax highlight functionality (`library-syntax` module and `markwon-syntax` artifact)
|
||||
* Add headingTypeface, headingTextSizes to SpannableTheme
|
||||
<br>Thanks to [@edenman]
|
||||
* Introduce `MediaDecoder` abstraction to `image-loader` module
|
||||
* Introduce `SpannableFactory`
|
||||
<br>Thanks for idea to [@c-b-h]
|
||||
* Update sample application to use syntax-highlight
|
||||
* Update sample application to use clickable placeholder for GIF media
|
||||
|
||||
[@edenman]: https://github.com/edenman
|
||||
[@c-b-h]: https://github.com/c-b-h
|
||||
|
||||
|
||||
# 1.0.6
|
||||
* Fix bullet list item size (depend on text size and not top-bottom arguments)
|
||||
* Add ability to specify MovementMethod when applying markdown to a TextView
|
||||
* Markdown images size is also resolved via ImageSizeResolver
|
||||
* Moved `ImageSize`, `ImageSizeResolver` and `ImageSizeResolverDef`
|
||||
to `ru.noties.markwon.renderer` package (one level up, previously `ru.noties.markwon.renderer.html`)
|
||||
|
||||
# 1.0.5
|
||||
* Change LinkSpan to extend URLSpan. Allow default linkColor (if not set explicitly)
|
||||
* Fit an image without dimensions to canvas width (and keep ratio)
|
||||
* Add support for separate color for code blocks ([#37])
|
||||
<br>Thanks to [@Arcnor]
|
||||
|
||||
[#37]: https://github.com/noties/Markwon/issues/37
|
||||
[@Arcnor]: https://github.com/Arcnor
|
||||
|
||||
|
||||
# 1.0.4
|
||||
* Fixes [#28] (tables are not rendered when at the end of the markdown)
|
||||
* Adds support for `indented code blocks`
|
||||
<br>Thanks to [@dlew]
|
||||
|
||||
[#28]: https://github.com/noties/Markwon/issues/
|
||||
[@dlew]: https://github.com/dlew
|
||||
|
||||
|
||||
# 1.0.3
|
||||
* Fixed ordered lists (when number width is greater than block margin)
|
||||
|
||||
# 1.0.2
|
||||
* Fixed additional white spaces at the end of parsed markdown
|
||||
* Fixed headings with no underline (levels 1 & 2)
|
||||
* Tables can have no borders
|
||||
|
||||
# 1.0.1
|
||||
* Support for task-lists ([#2])
|
||||
* Spans now are applied in reverse order ([#5] [#10])
|
||||
* Added `SpannableBuilder` to follow the reverse order of spans
|
||||
* Updated `commonmark-java` to `0.10.0`
|
||||
* Fixes [#1]
|
||||
|
||||
[#1]: https://github.com/noties/Markwon/issues/1
|
||||
[#2]: https://github.com/noties/Markwon/issues/2
|
||||
[#5]: https://github.com/noties/Markwon/issues/5
|
||||
[#10]: https://github.com/noties/Markwon/issues/10
|
||||
|
||||
|
||||
# 1.0.0
|
||||
|
||||
Initial release
|
202
LICENSE
202
LICENSE
@ -1,202 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
317
README.md
317
README.md
@ -1,317 +0,0 @@
|
||||

|
||||
|
||||
# Markwon
|
||||
|
||||
[](https://github.com/noties/Markwon/actions)
|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown
|
||||
following [commonmark-spec] with the help of amazing [commonmark-java]
|
||||
library and renders result as _Android-native_ Spannables. **No HTML**
|
||||
is involved as an intermediate step. <u>**No WebView** is required</u>.
|
||||
It's extremely fast, feature-rich and extensible.
|
||||
|
||||
It gives ability to display markdown in all TextView widgets
|
||||
(**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Toasts**
|
||||
and all other places that accept **Spanned content**. Library provides
|
||||
reasonable defaults to display style of a markdown content but also
|
||||
gives all the means to tweak the appearance if desired. All markdown
|
||||
features listed in [commonmark-spec] are supported
|
||||
(including support for **inlined/block HTML code**, **markdown tables**,
|
||||
**images** and **syntax highlight**).
|
||||
|
||||
`Markwon` comes with a [sample application](./app-sample/). It is a
|
||||
collection of library usages that comes with search and source code for
|
||||
each code sample.
|
||||
|
||||
Since version **4.2.0** **Markwon** comes with an [editor](./markwon-editor/) to _highlight_ markdown input
|
||||
as user types (for example in **EditText**).
|
||||
|
||||
[commonmark-spec]: https://spec.commonmark.org/0.28/
|
||||
[commonmark-java]: https://github.com/atlassian/commonmark-java/blob/master/README.md
|
||||
|
||||
## Installation
|
||||
|
||||

|
||||

|
||||
|
||||
```kotlin
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
```
|
||||
|
||||
Full list of available artifacts is present in the [install section](https://noties.github.io/Markwon/docs/v4/install.html)
|
||||
of the [documentation] web-site.
|
||||
|
||||
Please visit [documentation] web-site for further reference.
|
||||
|
||||
|
||||
> You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x)
|
||||
and [3.x.x](https://github.com/noties/Markwon/tree/3.x.x) branches
|
||||
|
||||
|
||||
## Supported markdown features:
|
||||
* Emphasis (`*`, `_`)
|
||||
* Strong emphasis (`**`, `__`)
|
||||
* Strike-through (`~~`)
|
||||
* Headers (`#{1,6}`)
|
||||
* Links (`[]()` && `[][]`)
|
||||
* Images
|
||||
* Thematic break (`---`, `***`, `___`)
|
||||
* Quotes & nested quotes (`>{1,}`)
|
||||
* Ordered & non-ordered lists & nested ones
|
||||
* Inline code
|
||||
* Code blocks
|
||||
* Tables (*with limitations*)
|
||||
* Syntax highlight
|
||||
* LaTeX formulas
|
||||
* HTML
|
||||
* Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
|
||||
* Strong emphasis (`<b>`, `<strong>`)
|
||||
* SuperScript (`<sup>`)
|
||||
* SubScript (`<sub>`)
|
||||
* Underline (`<u>`, `ins`)
|
||||
* Strike-through (`<s>`, `<strike>`, `<del>`)
|
||||
* Link (`a`)
|
||||
* Lists (`ul`, `ol`)
|
||||
* Images (`img` will require configured image loader)
|
||||
* Blockquote (`blockquote`)
|
||||
* Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`)
|
||||
* there is support to render any HTML tag
|
||||
* Task lists:
|
||||
- [ ] Not _done_
|
||||
- [X] **Done** with `X`
|
||||
- [x] ~~and~~ **or** small `x`
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
Taken with default configuration (except for image loading) in [sample app](./app-sample/):
|
||||
|
||||
<a href="./art/mw_light_01.png"><img src="./art/mw_light_01.png" width="30%" /></a>
|
||||
<a href="./art/mw_light_02.png"><img src="./art/mw_light_02.png" width="30%" /></a>
|
||||
<a href="./art/mw_light_03.png"><img src="./art/mw_light_03.png" width="30%" /></a>
|
||||
<a href="./art/mw_dark_01.png"><img src="./art/mw_dark_01.png" width="30%" /></a>
|
||||
|
||||
By default configuration uses TextView textColor for styling, so changing textColor changes style
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
Please visit [documentation] web-site for reference
|
||||
|
||||
[documentation]: https://noties.github.io/Markwon
|
||||
|
||||
|
||||
## Consulting
|
||||
Paid consulting is available. Please reach me out at [markwon+consulting[at]noties.io](mailto:markwon+consulting@noties.io)
|
||||
to discuss your idea or a project
|
||||
|
||||
---
|
||||
|
||||
# Demo
|
||||
Based on [this cheatsheet][cheatsheet]
|
||||
|
||||
---
|
||||
|
||||
## Headers
|
||||
---
|
||||
# Header 1
|
||||
## Header 2
|
||||
### Header 3
|
||||
#### Header 4
|
||||
##### Header 5
|
||||
###### Header 6
|
||||
---
|
||||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
---
|
||||
|
||||
## Lists
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.
|
||||
Note that this line is separate, but within the same paragraph.
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
```javascript
|
||||
var s = "JavaScript syntax highlighting";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
print s
|
||||
```
|
||||
|
||||
```java
|
||||
/**
|
||||
* Helper method to obtain a Parser with registered strike-through & table extensions
|
||||
* & task lists (added in 1.0.1)
|
||||
*
|
||||
* @return a Parser instance that is supported by this library
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Parser createParser() {
|
||||
return new Parser.Builder()
|
||||
.extensions(Arrays.asList(
|
||||
StrikethroughExtension.create(),
|
||||
TablesExtension.create(),
|
||||
TaskListExtension.create()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?android:attr/actionBarSize">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dip"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:textSize="16sp"
|
||||
tools:text="yo\nman" />
|
||||
|
||||
</ScrollView>
|
||||
```
|
||||
|
||||
```
|
||||
No language indicated, so no syntax highlighting.
|
||||
But let's throw in a <b>tag</b>.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tables
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
---
|
||||
|
||||
## Blockquotes
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
Nested quotes
|
||||
> Hello!
|
||||
>> And to you!
|
||||
|
||||
---
|
||||
|
||||
## Inline HTML
|
||||
|
||||
```html
|
||||
<u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
|
||||
```
|
||||
|
||||
<u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
|
||||
|
||||
---
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens (`-`)
|
||||
|
||||
***
|
||||
|
||||
Asterisks (`*`)
|
||||
|
||||
___
|
||||
|
||||
Underscores (`_`)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
```
|
||||
Copyright 2019 Dimitry Ivanov (legal@noties.io)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
|
||||
[cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@ -1,75 +0,0 @@
|
||||
# Markwon sample app
|
||||
|
||||
Collection of sample snippets showing different aspects of `Markwon` library usage. Includes
|
||||
source code of samples, latest stable/snapshot version of the library and search functionality.
|
||||
Additionally can check for updates. Can be used to preview markdown documents from the `Github.com`.
|
||||
|
||||
<a href="../art/sample-screen-01.png"><img src="../art/sample-screen-01.png" width="30%" /></a>
|
||||
<a href="../art/sample-screen-02.png"><img src="../art/sample-screen-02.png" width="30%" /></a>
|
||||
<a href="../art/sample-screen-03.png"><img src="../art/sample-screen-03.png" width="30%" /></a>
|
||||
<a href="../art/sample-screen-04.png"><img src="../art/sample-screen-04.png" width="30%" /></a>
|
||||
|
||||
## Distribution
|
||||
|
||||
Sample app is distributed via special parent-less branch [sample-store](https://github.com/noties/Markwon/tree/sample-store).
|
||||
Inside the app, under version badges, tap `CHECK FOR UPDATES` to check for updates. Sample app
|
||||
is not attached to main libraries versions and can be _released_ independently.
|
||||
|
||||
Application is signed with `keystore.jks`, which fingerprints are:
|
||||
* __SHA1__: `BA:70:A5:D2:40:65:F1:FA:88:90:59:BA:FC:B7:31:81:E6:37:D9:41`
|
||||
* __SHA256__: `82:C9:61:C5:DF:35:B1:CB:29:D5:48:83:FB:EB:9F:3E:7D:52:67:63:4F:D2:CE:0A:2D:70:17:85:FF:48:67:51`
|
||||
|
||||
|
||||
[Download latest APK](https://github.com/noties/Markwon/raw/sample-store/markwon-debug.apk)
|
||||
|
||||
|
||||
## Deeplink
|
||||
|
||||
Sample app handles special `markwon` scheme:
|
||||
* `markwon://sample/{ID}` to open specific sample given the `{ID}`
|
||||
* `markwon://search?q={TEXT TO SEARCH}&a={ARTIFACT}&t={TAG}`
|
||||
|
||||
Please note that search deeplink can have one of type: artifact or tag (if both are specified artifact will be used).
|
||||
|
||||
To test locally:
|
||||
|
||||
```
|
||||
adb shell am start -a android.intent.action.ACTION_VIEW -d markwon://sample/ID
|
||||
```
|
||||
|
||||
Please note that you might need to _url encode_ the `-d` argument
|
||||
|
||||
## Building
|
||||
|
||||
When adding/removing samples _most likely_ a clean build would be required.
|
||||
First, for annotation processor to create `samples.json`. And secondly,
|
||||
in order for Android Gradle plugin to bundle resources referenced via
|
||||
symbolic links (the `sample.json` itself and `io.noties.markwon.app.samples.*` directory)
|
||||
|
||||
```
|
||||
./gradlew :app-s:clean :app-s:asDe
|
||||
```
|
||||
|
||||
## Sample id
|
||||
|
||||
Sample `id` is generated manually when creating new sample. A `Live Template` can be used
|
||||
to simplify generation part (for example `mid` shortcut with a single variable in `Java` and `Kotlin` scopes):
|
||||
|
||||
```
|
||||
groovyScript("new Date().format('YYYYMMddHHmmss', TimeZone.getTimeZone('UTC'))")
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
This app uses [Robolectric](https://robolectric.org)(v3.8) for tests which is incompatible
|
||||
with JDK > 1.8. In order to run tests from command line with IDEA-bundled JDK - a special argument is
|
||||
required:
|
||||
|
||||
```
|
||||
./gradlew :app-s:testDe -Dorg.gradle.java.home="{INSERT BUNDLED JDK PATH HERE}"
|
||||
```
|
||||
|
||||
To obtain bundled JDK:
|
||||
* open `Project Structure...`
|
||||
* open `SDK Location`
|
||||
* copy contents of the field under `JDK Location`
|
@ -1,166 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
def gitSha = { ->
|
||||
def output = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
standardOutput = output
|
||||
}
|
||||
return output.toString().trim()
|
||||
}.memoize()
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion config['compile-sdk']
|
||||
buildToolsVersion config['build-tools']
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'io.noties.markwon.app'
|
||||
minSdkVersion 23
|
||||
targetSdkVersion config['target-sdk']
|
||||
versionCode 1
|
||||
versionName version
|
||||
|
||||
resConfig 'en'
|
||||
|
||||
setProperty("archivesBaseName", "markwon")
|
||||
|
||||
buildConfigField 'String', 'GIT_SHA', "\"${gitSha()}\""
|
||||
buildConfigField 'String', 'GIT_REPOSITORY', '"https://github.com/noties/Markwon"'
|
||||
|
||||
final def scheme = 'markwon'
|
||||
buildConfigField 'String', 'DEEPLINK_SCHEME', "\"$scheme\""
|
||||
manifestPlaceholders += [
|
||||
'deeplink_scheme': scheme
|
||||
]
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
preDexLibraries true
|
||||
javaMaxHeapSize '5g'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs += '../sample-utils/annotations'
|
||||
}
|
||||
}
|
||||
|
||||
// do not sign in CI
|
||||
if (!project.hasProperty('CI')) {
|
||||
signingConfigs {
|
||||
config {
|
||||
|
||||
final def keystoreFile = project.file('keystore.jks')
|
||||
final def keystoreFilePassword = 'MARKWON_KEYSTORE_FILE_PASSWORD'
|
||||
final def keystoreAlias = 'MARKWON_KEY_ALIAS'
|
||||
final def keystoreAliasPassword = 'MARKWON_KEY_ALIAS_PASSWORD'
|
||||
|
||||
final def properties = [
|
||||
keystoreFilePassword,
|
||||
keystoreAlias,
|
||||
keystoreAliasPassword
|
||||
]
|
||||
|
||||
if (!keystoreFile.exists()) {
|
||||
throw new IllegalStateException("No '${keystoreFile.name}' file is found.")
|
||||
}
|
||||
|
||||
final def missingProperties = properties.findAll { !project.hasProperty(it) }
|
||||
if (!missingProperties.isEmpty()) {
|
||||
throw new IllegalStateException("Missing required signing properties: $missingProperties")
|
||||
}
|
||||
|
||||
storeFile keystoreFile
|
||||
storePassword project[keystoreFilePassword]
|
||||
|
||||
keyAlias project[keystoreAlias]
|
||||
keyPassword project[keystoreAliasPassword]
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
release {
|
||||
signingConfig signingConfigs.config
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg('markwon.samples.file', "${projectDir}/samples.json".toString())
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
features = ["parcelize"]
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'org.jetbrains', module: 'annotations-java5'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kapt project(':sample-utils:processor')
|
||||
deps['annotationProcessor'].with {
|
||||
kapt it['prism4j-bundler']
|
||||
}
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
implementation project(':markwon-core')
|
||||
implementation project(':markwon-editor')
|
||||
implementation project(':markwon-ext-latex')
|
||||
implementation project(':markwon-ext-strikethrough')
|
||||
implementation project(':markwon-ext-tables')
|
||||
implementation project(':markwon-ext-tasklist')
|
||||
implementation project(':markwon-html')
|
||||
implementation project(':markwon-image')
|
||||
implementation project(':markwon-inline-parser')
|
||||
implementation project(':markwon-linkify')
|
||||
implementation project(':markwon-recycler')
|
||||
implementation project(':markwon-recycler-table')
|
||||
implementation project(':markwon-simple-ext')
|
||||
implementation project(':markwon-syntax-highlight')
|
||||
|
||||
implementation project(':markwon-image-picasso')
|
||||
implementation project(':markwon-image-glide')
|
||||
implementation project(':markwon-image-coil')
|
||||
|
||||
deps.with {
|
||||
// implementation it['x-appcompat']
|
||||
implementation it['x-recycler-view']
|
||||
implementation it['x-cardview']
|
||||
implementation it['x-fragment']
|
||||
implementation it['okhttp']
|
||||
implementation it['prism4j']
|
||||
implementation it['gson']
|
||||
implementation it['adapt']
|
||||
implementation it['debug']
|
||||
implementation it['android-svg']
|
||||
implementation it['android-gif-impl']
|
||||
implementation it['coil']
|
||||
}
|
||||
|
||||
deps['test'].with {
|
||||
testImplementation it['junit']
|
||||
testImplementation it['robolectric']
|
||||
testImplementation it['mockito']
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# abort on errors
|
||||
set -e
|
||||
|
||||
# build
|
||||
../gradlew :app-sample:clean
|
||||
../gradlew :app-sample:assembleDebug
|
||||
|
||||
# navigate into the build output directory
|
||||
cd ./build/outputs/apk/debug/
|
||||
|
||||
revision=$(git rev-parse --short HEAD)
|
||||
|
||||
echo "output-metadata.json" > ./.gitignore
|
||||
echo "$revision" > ./version
|
||||
|
||||
git init
|
||||
git add -A
|
||||
git commit -m "sample $revision"
|
||||
|
||||
git push -f git@github.com:noties/Markwon.git master:sample-store
|
||||
|
||||
cd -
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,166 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/content_padding"
|
||||
tools:ignore="MissingDefaultResource,HardcodedText">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hey!" />
|
||||
|
||||
<io.noties.markwon.app.widget.FlowLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding"
|
||||
app:fl_spacingHorizontal="@dimen/content_padding"
|
||||
app:fl_spacingVertical="4dip">
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="ext-latex" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="another" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_artifact"
|
||||
android:text="core" />
|
||||
|
||||
</io.noties.markwon.app.widget.FlowLayout>
|
||||
|
||||
<io.noties.markwon.app.widget.FlowLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="@dimen/content_padding"
|
||||
app:fl_spacingHorizontal="@dimen/content_padding"
|
||||
app:fl_spacingVertical="4dip">
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="ext-latex" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="another" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
|
||||
|
||||
<TextView
|
||||
style="@style/ArtifactTagText"
|
||||
android:background="@drawable/bg_tag"
|
||||
android:text="core" />
|
||||
|
||||
</io.noties.markwon.app.widget.FlowLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,78 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.noties.markwon.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
|
||||
|
||||
<activity android:name=".sample.MainActivity">
|
||||
<!-- launcher intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- local deeplink (with custom scheme) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="${deeplink_scheme}" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="noties.io"
|
||||
android:scheme="https" />
|
||||
|
||||
<data android:pathPrefix="/Markwon/app"/>
|
||||
|
||||
<data android:pathPattern="sample/.*" />
|
||||
<data android:pathPattern="search" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".readme.ReadMeActivity"
|
||||
android:exported="true">
|
||||
|
||||
<!-- github markdown files handling -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="github.com"
|
||||
android:scheme="https" />
|
||||
|
||||
<data android:pathPattern=".*\\.md" />
|
||||
<data android:pathPattern=".*\\..*\\.md" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.md" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.md" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1 +0,0 @@
|
||||
../../../../README.md
|
@ -1 +0,0 @@
|
||||
../../main/java/io/noties/markwon/app/samples/
|
@ -1 +0,0 @@
|
||||
../../../samples.json
|
@ -1,58 +0,0 @@
|
||||
package io.noties.markwon.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Build
|
||||
import io.noties.debug.AndroidLogDebugOutput
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.app.readme.ReadMeActivity
|
||||
import io.noties.markwon.app.sample.SampleManager
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Suppress("unused")
|
||||
// `open` is required for tests (to create a spy mockito instance)
|
||||
open class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Debug.init(AndroidLogDebugOutput(BuildConfig.DEBUG))
|
||||
|
||||
executorService = Executors.newCachedThreadPool()
|
||||
sampleManager = SampleManager(this, executorService)
|
||||
|
||||
ensureReadmeShortcut()
|
||||
}
|
||||
|
||||
private fun ensureReadmeShortcut() {
|
||||
if (Build.VERSION.SDK_INT < 25) {
|
||||
return
|
||||
}
|
||||
|
||||
val manager = getSystemService(ShortcutManager::class.java) ?: return
|
||||
|
||||
@Suppress("ReplaceNegatedIsEmptyWithIsNotEmpty")
|
||||
if (!manager.dynamicShortcuts.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
component = ComponentName(this@App, ReadMeActivity::class.java)
|
||||
}
|
||||
|
||||
val shortcut = ShortcutInfo.Builder(this, "readme")
|
||||
.setShortLabel("README")
|
||||
.setIntent(intent)
|
||||
.build()
|
||||
manager.addDynamicShortcuts(mutableListOf(shortcut))
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var executorService: ExecutorService
|
||||
lateinit var sampleManager: SampleManager
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package io.noties.markwon.app.readme
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import io.noties.markwon.image.destination.ImageDestinationProcessor
|
||||
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
|
||||
|
||||
class GithubImageDestinationProcessor(
|
||||
username: String = "noties",
|
||||
repository: String = "Markwon",
|
||||
branch: String = "master"
|
||||
) : ImageDestinationProcessor() {
|
||||
|
||||
private val processor = ImageDestinationProcessorRelativeToAbsolute("https://github.com/$username/$repository/raw/$branch/")
|
||||
|
||||
override fun process(destination: String): String {
|
||||
// process only images without scheme information
|
||||
val uri = Uri.parse(destination)
|
||||
return if (TextUtils.isEmpty(uri.scheme)) {
|
||||
processor.process(destination)
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
package io.noties.markwon.app.readme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.MarkwonConfiguration
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.utils.ReadMeUtils
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.loadReadMe
|
||||
import io.noties.markwon.app.utils.textOrHide
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.recycler.MarkwonAdapter
|
||||
import io.noties.markwon.recycler.SimpleEntry
|
||||
import io.noties.markwon.recycler.table.TableEntry
|
||||
import io.noties.markwon.recycler.table.TableEntryPlugin
|
||||
import io.noties.markwon.syntax.Prism4jThemeDefault
|
||||
import io.noties.markwon.syntax.SyntaxHighlightPlugin
|
||||
import io.noties.prism4j.Prism4j
|
||||
import io.noties.prism4j.annotations.PrismBundle
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.commonmark.ext.gfm.tables.TableBlock
|
||||
import org.commonmark.node.FencedCodeBlock
|
||||
import java.io.IOException
|
||||
|
||||
@PrismBundle(includeAll = true)
|
||||
class ReadMeActivity : Activity() {
|
||||
|
||||
private lateinit var progressBar: View
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_read_me)
|
||||
|
||||
progressBar = findViewById(R.id.progress_bar)
|
||||
|
||||
val data = intent.data
|
||||
|
||||
Debug.i(data)
|
||||
|
||||
initAppBar(data)
|
||||
|
||||
initRecyclerView(data)
|
||||
}
|
||||
|
||||
private val markwon: Markwon
|
||||
get() = Markwon.builder(this)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(TableEntryPlugin.create(this))
|
||||
.usePlugin(SyntaxHighlightPlugin.create(Prism4j(GrammarLocatorDef()), Prism4jThemeDefault.create(0)))
|
||||
.usePlugin(TaskListPlugin.create(this))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(ReadMeImageDestinationPlugin(intent.data))
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
|
||||
builder.on(FencedCodeBlock::class.java) { visitor, block ->
|
||||
// we actually won't be applying code spans here, as our custom view will
|
||||
// draw background and apply mono typeface
|
||||
//
|
||||
// NB the `trim` operation on literal (as code will have a new line at the end)
|
||||
val code = visitor.configuration()
|
||||
.syntaxHighlight()
|
||||
.highlight(block.info, block.literal.trim())
|
||||
visitor.builder().append(code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder.linkResolver(ReadMeLinkResolver())
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
private fun initAppBar(data: Uri?) {
|
||||
val appBar = findViewById<View>(R.id.app_bar)
|
||||
appBar.findViewById<View>(R.id.app_bar_icon).setOnClickListener { onBackPressed() }
|
||||
|
||||
val (title: String, subtitle: String?) = if (data == null) {
|
||||
Pair("README.md", null)
|
||||
} else {
|
||||
Pair(data.lastPathSegment ?: "", data.toString())
|
||||
}
|
||||
|
||||
appBar.findViewById<TextView>(R.id.title).text = title
|
||||
appBar.findViewById<TextView>(R.id.subtitle).textOrHide(subtitle)
|
||||
}
|
||||
|
||||
private fun initRecyclerView(data: Uri?) {
|
||||
|
||||
val adapter = MarkwonAdapter.builder(R.layout.adapter_node, R.id.text_view)
|
||||
.include(FencedCodeBlock::class.java, SimpleEntry.create(R.layout.adapter_node_code_block, R.id.text_view))
|
||||
.include(TableBlock::class.java, TableEntry.create {
|
||||
it
|
||||
.tableLayout(R.layout.adapter_node_table_block, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.view_table_entry_cell)
|
||||
})
|
||||
.build()
|
||||
|
||||
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.itemAnimator = DefaultItemAnimator()
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
load(applicationContext, data) { result ->
|
||||
|
||||
when (result) {
|
||||
is Result.Failure -> Debug.e(result.throwable)
|
||||
is Result.Success -> {
|
||||
val markwon = markwon
|
||||
val node = markwon.parse(result.markdown)
|
||||
if (window != null) {
|
||||
recyclerView.post {
|
||||
adapter.setParsedMarkdown(markwon, node)
|
||||
adapter.notifyDataSetChanged()
|
||||
progressBar.hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Result {
|
||||
data class Success(val markdown: String) : Result()
|
||||
data class Failure(val throwable: Throwable) : Result()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun makeIntent(context: Context): Intent {
|
||||
return Intent(context, ReadMeActivity::class.java)
|
||||
}
|
||||
|
||||
private fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
|
||||
|
||||
if (data == null) {
|
||||
callback.invoke(Result.Success(loadReadMe(context)))
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url(ReadMeUtils.buildRawGithubUrl(data))
|
||||
.build()
|
||||
OkHttpClient().newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
callback.invoke(Result.Failure(e))
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val md = response.body()?.string() ?: ""
|
||||
callback.invoke(Result.Success(md))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (t: Throwable) {
|
||||
callback.invoke(Result.Failure(t))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package io.noties.markwon.app.readme
|
||||
|
||||
import android.net.Uri
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.MarkwonConfiguration
|
||||
import io.noties.markwon.app.utils.ReadMeUtils
|
||||
|
||||
class ReadMeImageDestinationPlugin(private val data: Uri?) : AbstractMarkwonPlugin() {
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
val info = ReadMeUtils.parseInfo(data)
|
||||
if (info == null) {
|
||||
builder.imageDestinationProcessor(GithubImageDestinationProcessor())
|
||||
} else {
|
||||
builder.imageDestinationProcessor(GithubImageDestinationProcessor(
|
||||
username = info.username,
|
||||
repository = info.repository,
|
||||
branch = info.branch
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package io.noties.markwon.app.readme
|
||||
|
||||
import android.view.View
|
||||
import io.noties.markwon.LinkResolverDef
|
||||
import io.noties.markwon.app.utils.ReadMeUtils
|
||||
|
||||
class ReadMeLinkResolver : LinkResolverDef() {
|
||||
|
||||
override fun resolve(view: View, link: String) {
|
||||
val info = ReadMeUtils.parseRepository(link)
|
||||
val url = if (info != null) {
|
||||
ReadMeUtils.buildRepositoryReadMeUrl(info.first, info.second)
|
||||
} else {
|
||||
link
|
||||
}
|
||||
super.resolve(view, url)
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package io.noties.markwon.app.sample
|
||||
|
||||
import android.net.Uri
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.app.BuildConfig
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
|
||||
sealed class Deeplink {
|
||||
data class Sample(val id: String) : Deeplink()
|
||||
data class Search(val search: SampleSearch) : Deeplink()
|
||||
|
||||
companion object {
|
||||
fun parse(data: Uri?): Deeplink? {
|
||||
Debug.i(data)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val data = data ?: return null
|
||||
return when (data.scheme) {
|
||||
// local deeplink with custom scheme (`markwon://`)
|
||||
BuildConfig.DEEPLINK_SCHEME -> {
|
||||
when (data.host) {
|
||||
"sample" -> parseSample(data.lastPathSegment)
|
||||
"search" -> parseSearch(data.query)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
// https deeplink, `https://noties.io/Markwon/sample`
|
||||
"https" -> {
|
||||
// https://noties.io/Markwon/app/sample/ID
|
||||
// https://noties.io/Markwon/app/search?a=core
|
||||
val segments = data.pathSegments
|
||||
if (segments.size >= 3
|
||||
&& "Markwon" == segments[0]
|
||||
&& "app" == segments[1]) {
|
||||
when (segments[2]) {
|
||||
"sample" -> parseSample(data.lastPathSegment)
|
||||
"search" -> parseSearch(data.query)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}.also {
|
||||
Debug.i("parsed: $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSample(id: String?): Sample? {
|
||||
if (id == null) return null
|
||||
return Sample(id)
|
||||
}
|
||||
|
||||
private fun parseSearch(query: String?): Search? {
|
||||
Debug.i("query: '$query'")
|
||||
|
||||
val params = query
|
||||
?.let {
|
||||
// `https:.*` has query with `search?a=core`
|
||||
val index = it.indexOf('?')
|
||||
if (index > -1) {
|
||||
it.substring(index + 1)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
?.split("&")
|
||||
?.map {
|
||||
val (k, v) = it.split("=")
|
||||
Pair(k, v)
|
||||
}
|
||||
?.toMap()
|
||||
?: return null
|
||||
|
||||
Debug.i("params: $params")
|
||||
|
||||
val artifact = params["a"]
|
||||
val tag = params["t"]
|
||||
val search = params["q"]
|
||||
|
||||
Debug.i("artifact: '$artifact', tag: '$tag', search: '$search'")
|
||||
|
||||
val sampleSearch: SampleSearch? = if (artifact != null) {
|
||||
val encodedArtifact = MarkwonArtifact.values()
|
||||
.firstOrNull { it.artifactName() == artifact }
|
||||
if (encodedArtifact != null) {
|
||||
SampleSearch.Artifact(search, encodedArtifact)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else if (tag != null) {
|
||||
SampleSearch.Tag(search, tag)
|
||||
} else if (search != null) {
|
||||
SampleSearch.All(search)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (sampleSearch == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Search(sampleSearch)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package io.noties.markwon.app.sample
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Window
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.app.App
|
||||
import io.noties.markwon.app.sample.ui.SampleFragment
|
||||
import io.noties.markwon.app.sample.ui.SampleListFragment
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
|
||||
.commitNowAllowingStateLoss()
|
||||
|
||||
// process deeplink if we are not restored
|
||||
val deeplink = Deeplink.parse(intent.data)
|
||||
|
||||
val deepLinkFragment: Fragment? = if (deeplink != null) {
|
||||
when (deeplink) {
|
||||
is Deeplink.Sample -> App.sampleManager.sample(deeplink.id)
|
||||
?.let { SampleFragment.init(it) }
|
||||
is Deeplink.Search -> SampleListFragment.init(deeplink.search)
|
||||
}
|
||||
} else null
|
||||
|
||||
if (deepLinkFragment != null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(Window.ID_ANDROID_CONTENT, deepLinkFragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package io.noties.markwon.app.sample
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Sample(
|
||||
val javaClassName: String,
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val artifacts: List<MarkwonArtifact>,
|
||||
val tags: List<String>
|
||||
) : Parcelable {
|
||||
|
||||
enum class Language {
|
||||
JAVA, KOTLIN
|
||||
}
|
||||
|
||||
data class Code(val language: Language, val sourceCode: String)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package io.noties.markwon.app.sample
|
||||
|
||||
import android.content.Context
|
||||
import io.noties.markwon.app.utils.Cancellable
|
||||
import io.noties.markwon.app.utils.SampleUtils
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
class SampleManager(
|
||||
private val context: Context,
|
||||
private val executorService: ExecutorService
|
||||
) {
|
||||
|
||||
private val samples: List<Sample> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
SampleUtils.readSamples(context)
|
||||
}
|
||||
|
||||
fun sample(id: String): Sample? {
|
||||
return samples.firstOrNull { id == it.id }
|
||||
}
|
||||
|
||||
fun samples(search: SampleSearch?, callback: (List<Sample>) -> Unit): Cancellable {
|
||||
|
||||
var action: ((List<Sample>) -> Unit)? = callback
|
||||
|
||||
val future = executorService.submit {
|
||||
|
||||
val source = when (search) {
|
||||
is SampleSearch.Artifact -> samples.filter { it.artifacts.contains(search.artifact) }
|
||||
is SampleSearch.Tag -> samples.filter { it.tags.contains(search.tag) }
|
||||
else -> samples.toList() // just copy all
|
||||
}
|
||||
|
||||
val text = search?.text
|
||||
val results = if (text == null) {
|
||||
// no further filtering, just return the full source here
|
||||
source
|
||||
} else {
|
||||
source.filter { filter(it, text) }
|
||||
}
|
||||
|
||||
action?.invoke(results)
|
||||
}
|
||||
|
||||
return object : Cancellable {
|
||||
override val isCancelled: Boolean
|
||||
get() = future.isDone
|
||||
|
||||
override fun cancel() {
|
||||
action = null
|
||||
future.cancel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if title contains,
|
||||
// if description contains,
|
||||
// if tags contains
|
||||
// if artifacts contains,
|
||||
private fun filter(sample: Sample, text: String): Boolean {
|
||||
return sample.javaClassName.contains(text, true)
|
||||
|| sample.title.contains(text, true)
|
||||
|| sample.description.contains(text, true)
|
||||
|| filterTags(sample.tags, text)
|
||||
|| filterArtifacts(sample.artifacts, text)
|
||||
}
|
||||
|
||||
private fun filterTags(tags: List<String>, text: String): Boolean {
|
||||
return tags.firstOrNull { it.contains(text, true) } != null
|
||||
}
|
||||
|
||||
private fun filterArtifacts(artifacts: List<MarkwonArtifact>, text: String): Boolean {
|
||||
return artifacts.firstOrNull { it.artifactName().contains(text, true) } != null
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package io.noties.markwon.app.sample
|
||||
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
|
||||
sealed class SampleSearch(val text: String?) {
|
||||
class Artifact(text: String?, val artifact: MarkwonArtifact) : SampleSearch(text)
|
||||
class Tag(text: String?, val tag: String) : SampleSearch(text)
|
||||
class All(text: String?) : SampleSearch(text)
|
||||
|
||||
override fun toString(): String {
|
||||
return "SampleSearch(text=$text,type=${javaClass.simpleName})"
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.markwon.app.R
|
||||
|
||||
abstract class MarkwonRecyclerViewSample : MarkwonSample() {
|
||||
|
||||
protected lateinit var context: Context
|
||||
protected lateinit var recyclerView: RecyclerView
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
context = view.context
|
||||
recyclerView = view.findViewById(R.id.recycler_view)
|
||||
render()
|
||||
}
|
||||
|
||||
override val layoutResId: Int
|
||||
get() = R.layout.sample_recycler_view
|
||||
|
||||
abstract fun render()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
abstract class MarkwonSample {
|
||||
|
||||
fun createView(inflater: LayoutInflater, container: ViewGroup?): View {
|
||||
return inflater.inflate(layoutResId, container, false)
|
||||
}
|
||||
|
||||
abstract fun onViewCreated(view: View)
|
||||
|
||||
protected abstract val layoutResId: Int
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
|
||||
import io.noties.markwon.app.R
|
||||
|
||||
abstract class MarkwonTextViewSample : MarkwonSample() {
|
||||
|
||||
protected lateinit var context: Context
|
||||
protected lateinit var scrollView: ScrollView
|
||||
protected lateinit var textView: TextView
|
||||
|
||||
override val layoutResId: Int = R.layout.sample_text_view
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
context = view.context
|
||||
scrollView = view.findViewById(R.id.scroll_view)
|
||||
textView = view.findViewById(R.id.text_view)
|
||||
render()
|
||||
}
|
||||
|
||||
abstract fun render()
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.noties.markwon.app.App
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.readCode
|
||||
import io.noties.markwon.syntax.Prism4jSyntaxHighlight
|
||||
import io.noties.markwon.syntax.Prism4jThemeDefault
|
||||
import io.noties.prism4j.Prism4j
|
||||
import io.noties.prism4j.annotations.PrismBundle
|
||||
|
||||
@PrismBundle(include = ["java", "kotlin"], grammarLocatorClassName = ".GrammarLocatorSourceCode")
|
||||
class SampleCodeFragment : Fragment() {
|
||||
|
||||
private lateinit var progressBar: View
|
||||
private lateinit var textView: TextView
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_sample_code, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar)
|
||||
textView = view.findViewById(R.id.text_view)
|
||||
|
||||
load()
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
App.executorService.submit {
|
||||
val code = sample.readCode(requireContext())
|
||||
val prism = Prism4j(GrammarLocatorSourceCode())
|
||||
val highlight = Prism4jSyntaxHighlight.create(prism, Prism4jThemeDefault.create(0))
|
||||
val language = when (code.language) {
|
||||
Sample.Language.KOTLIN -> "kotlin"
|
||||
Sample.Language.JAVA -> "java"
|
||||
}
|
||||
val text = highlight.highlight(language, code.sourceCode)
|
||||
|
||||
textView.post {
|
||||
//attached
|
||||
if (context != null) {
|
||||
progressBar.hidden = true
|
||||
textView.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val sample: Sample by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val temp: Sample = (arguments!!.getParcelable(ARG_SAMPLE))!!
|
||||
temp
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_SAMPLE = "arg.Sample"
|
||||
|
||||
fun init(sample: Sample): SampleCodeFragment {
|
||||
return SampleCodeFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SAMPLE, sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
import io.noties.markwon.app.utils.active
|
||||
|
||||
class SampleFragment : Fragment() {
|
||||
|
||||
private lateinit var container: ViewGroup
|
||||
|
||||
private var isCodeSelected = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_sample, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
container = view.findViewById(R.id.container)
|
||||
isCodeSelected = savedInstanceState?.getBoolean(KEY_CODE_SELECTED) ?: false
|
||||
|
||||
initAppBar(view)
|
||||
initTabBar(view)
|
||||
}
|
||||
|
||||
private fun initAppBar(view: View) {
|
||||
val appBar: View = view.findViewById(R.id.app_bar)
|
||||
val icon: View = appBar.findViewById(R.id.app_bar_icon)
|
||||
val title: TextView = appBar.findViewById(R.id.app_bar_title)
|
||||
|
||||
icon.setOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
|
||||
title.text = sample.title
|
||||
}
|
||||
|
||||
private fun initTabBar(view: View) {
|
||||
val tabBar: View = view.findViewById(R.id.tab_bar)
|
||||
val preview: View = tabBar.findViewById(R.id.tab_bar_preview)
|
||||
val code: View = tabBar.findViewById(R.id.tab_bar_code)
|
||||
|
||||
preview.setOnClickListener {
|
||||
if (!it.active) {
|
||||
it.active = true
|
||||
code.active = false
|
||||
showPreview()
|
||||
}
|
||||
}
|
||||
|
||||
code.setOnClickListener {
|
||||
if (!it.active) {
|
||||
it.active = true
|
||||
preview.active = false
|
||||
showCode()
|
||||
}
|
||||
}
|
||||
|
||||
// maybe check state (in case of restoration)
|
||||
|
||||
// initial values
|
||||
preview.active = !isCodeSelected
|
||||
code.active = isCodeSelected
|
||||
|
||||
if (isCodeSelected) {
|
||||
showCode()
|
||||
} else {
|
||||
showPreview()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPreview() {
|
||||
isCodeSelected = false
|
||||
showFragment(TAG_PREVIEW, TAG_CODE) { SamplePreviewFragment.init(sample) }
|
||||
}
|
||||
|
||||
private fun showCode() {
|
||||
isCodeSelected = true
|
||||
showFragment(TAG_CODE, TAG_PREVIEW) { SampleCodeFragment.init(sample) }
|
||||
}
|
||||
|
||||
private fun showFragment(showTag: String, hideTag: String, provider: () -> Fragment) {
|
||||
val manager = childFragmentManager
|
||||
manager.beginTransaction().apply {
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
|
||||
val existing = manager.findFragmentByTag(showTag)
|
||||
if (existing != null) {
|
||||
show(existing)
|
||||
} else {
|
||||
add(container.id, provider(), showTag)
|
||||
}
|
||||
|
||||
manager.findFragmentByTag(hideTag)?.also {
|
||||
hide(it)
|
||||
}
|
||||
|
||||
commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBoolean(KEY_CODE_SELECTED, isCodeSelected)
|
||||
}
|
||||
|
||||
private val sample: Sample by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val temp: Sample = (arguments!!.getParcelable(ARG_SAMPLE))!!
|
||||
temp
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_SAMPLE = "arg.Sample"
|
||||
private const val TAG_PREVIEW = "tag.Preview"
|
||||
private const val TAG_CODE = "tag.Code"
|
||||
private const val KEY_CODE_SELECTED = "key.Selected"
|
||||
|
||||
fun init(sample: Sample): SampleFragment {
|
||||
val fragment = SampleFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(ARG_SAMPLE, sample)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -1,462 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.noties.adapt.Adapt
|
||||
import io.noties.adapt.DiffUtilDataSetChanged
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.App
|
||||
import io.noties.markwon.app.BuildConfig
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.readme.ReadMeActivity
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
import io.noties.markwon.app.sample.SampleManager
|
||||
import io.noties.markwon.app.sample.SampleSearch
|
||||
import io.noties.markwon.app.sample.ui.adapt.CheckForUpdateItem
|
||||
import io.noties.markwon.app.sample.ui.adapt.SampleItem
|
||||
import io.noties.markwon.app.sample.ui.adapt.VersionItem
|
||||
import io.noties.markwon.app.utils.Cancellable
|
||||
import io.noties.markwon.app.utils.UpdateUtils
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.onPreDraw
|
||||
import io.noties.markwon.app.utils.recyclerView
|
||||
import io.noties.markwon.app.utils.stackTraceString
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
import io.noties.markwon.app.widget.SearchBar
|
||||
import io.noties.markwon.movement.MovementMethodPlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
class SampleListFragment : Fragment() {
|
||||
|
||||
private val adapt: Adapt = Adapt.create(DiffUtilDataSetChanged.create())
|
||||
private lateinit var markwon: Markwon
|
||||
|
||||
private val type: Type by lazy(LazyThreadSafetyMode.NONE) {
|
||||
parseType(arguments!!)
|
||||
}
|
||||
|
||||
private var search: String? = null
|
||||
|
||||
// postpone state restoration
|
||||
private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null
|
||||
|
||||
private var cancellable: Cancellable? = null
|
||||
private var checkForUpdateCancellable: Cancellable? = null
|
||||
|
||||
private lateinit var progressBar: View
|
||||
|
||||
private val versionItem: VersionItem by lazy(LazyThreadSafetyMode.NONE) {
|
||||
VersionItem()
|
||||
}
|
||||
|
||||
private val sampleManager: SampleManager
|
||||
get() = App.sampleManager
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
context.also {
|
||||
markwon = markwon(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_sample_list, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initAppBar(view)
|
||||
|
||||
val context = requireContext()
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar)
|
||||
|
||||
val searchBar: SearchBar = view.findViewById(R.id.search_bar)
|
||||
searchBar.onSearchListener = {
|
||||
search = it
|
||||
fetch()
|
||||
}
|
||||
|
||||
val recyclerView: RecyclerView = view.findViewById(R.id.recycler_view)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.itemAnimator = DefaultItemAnimator()
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.adapter = adapt
|
||||
|
||||
// // additional padding for RecyclerView
|
||||
// greatly complicates state restoration (items jump and a lot of times state cannot be
|
||||
// even restored (layout manager scrolls to top item and that's it)
|
||||
// searchBar.onPreDraw {
|
||||
// recyclerView.setPadding(
|
||||
// recyclerView.paddingLeft,
|
||||
// recyclerView.paddingTop + searchBar.height,
|
||||
// recyclerView.paddingRight,
|
||||
// recyclerView.paddingBottom
|
||||
// )
|
||||
// }
|
||||
|
||||
val state: State? = arguments?.getParcelable(STATE)
|
||||
val initialSearch = arguments?.getString(ARG_SEARCH)
|
||||
|
||||
// clear it anyway
|
||||
arguments?.remove(ARG_SEARCH)
|
||||
|
||||
Debug.i(state, initialSearch)
|
||||
|
||||
pendingRecyclerScrollPosition = state?.recyclerScrollPosition
|
||||
|
||||
val search = listOf(state?.search, initialSearch)
|
||||
.firstOrNull { it != null }
|
||||
|
||||
if (search != null) {
|
||||
searchBar.search(search)
|
||||
} else {
|
||||
fetch()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
val state = State(
|
||||
search,
|
||||
adapt.recyclerView?.scrollPosition
|
||||
)
|
||||
Debug.i(state)
|
||||
arguments?.putParcelable(STATE, state)
|
||||
|
||||
val cancellable = this.cancellable
|
||||
if (cancellable != null && !cancellable.isCancelled) {
|
||||
cancellable.cancel()
|
||||
this.cancellable = null
|
||||
}
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// not called? yeah, whatever
|
||||
// override fun onSaveInstanceState(outState: Bundle) {
|
||||
// super.onSaveInstanceState(outState)
|
||||
//
|
||||
// val state = State(
|
||||
// search,
|
||||
// adapt.recyclerView?.scrollPosition
|
||||
// )
|
||||
// Debug.i(state)
|
||||
// outState.putParcelable(STATE, state)
|
||||
// }
|
||||
|
||||
private fun initAppBar(view: View) {
|
||||
val appBar = view.findViewById<View>(R.id.app_bar)
|
||||
|
||||
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
|
||||
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
|
||||
val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme)
|
||||
|
||||
val isInitialScreen = fragmentManager?.backStackEntryCount == 0
|
||||
|
||||
appBarIcon.hidden = isInitialScreen
|
||||
appBarIconReadme.hidden = !isInitialScreen
|
||||
|
||||
val type = this.type
|
||||
|
||||
val (text, background) = when (type) {
|
||||
is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
|
||||
is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
|
||||
is Type.All -> Pair(resources.getString(R.string.app_name), 0)
|
||||
}
|
||||
|
||||
appBarTitle.text = text
|
||||
|
||||
if (background != 0) {
|
||||
appBarTitle.setBackgroundResource(background)
|
||||
}
|
||||
|
||||
if (isInitialScreen) {
|
||||
appBarIconReadme.setOnClickListener {
|
||||
context?.let {
|
||||
val intent = ReadMeActivity.makeIntent(it)
|
||||
it.startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
|
||||
appBarIcon.setOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindSamples(samples: List<Sample>, addVersion: Boolean) {
|
||||
|
||||
val items: List<Item<*>> = samples
|
||||
.map {
|
||||
SampleItem(
|
||||
markwon,
|
||||
it,
|
||||
{ artifact -> openArtifact(artifact) },
|
||||
{ tag -> openTag(tag) },
|
||||
{ sample -> openSample(sample) }
|
||||
)
|
||||
}
|
||||
.let {
|
||||
if (addVersion) {
|
||||
val list: List<Item<*>> = it
|
||||
list.toMutableList().apply {
|
||||
add(0, CheckForUpdateItem(this@SampleListFragment::checkForUpdate))
|
||||
add(0, versionItem)
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
adapt.setItems(items)
|
||||
|
||||
val recyclerView = adapt.recyclerView ?: return
|
||||
|
||||
val scrollPosition = pendingRecyclerScrollPosition
|
||||
|
||||
Debug.i(scrollPosition)
|
||||
|
||||
if (scrollPosition != null) {
|
||||
pendingRecyclerScrollPosition = null
|
||||
recyclerView.onPreDraw {
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)
|
||||
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
|
||||
}
|
||||
} else {
|
||||
recyclerView.onPreDraw {
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForUpdate() {
|
||||
val current = checkForUpdateCancellable
|
||||
if (current != null && !current.isCancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
progressBar.hidden = false
|
||||
checkForUpdateCancellable = UpdateUtils.checkForUpdate { result ->
|
||||
progressBar.post {
|
||||
processUpdateResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processUpdateResult(result: UpdateUtils.Result) {
|
||||
val context = context ?: return
|
||||
|
||||
progressBar.hidden = true
|
||||
|
||||
val builder = AlertDialog.Builder(context)
|
||||
|
||||
when (result) {
|
||||
is UpdateUtils.Result.UpdateAvailable -> {
|
||||
val md = """
|
||||
## Update available
|
||||
|
||||
${BuildConfig.GIT_SHA} -> **${result.revision}**
|
||||
|
||||
Would you like to download it?
|
||||
""".trimIndent()
|
||||
builder.setMessage(markwon.toMarkdown(md))
|
||||
builder.setNegativeButton(android.R.string.cancel, null)
|
||||
builder.setPositiveButton("Download") { _, _ ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(result.url))
|
||||
startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
}
|
||||
|
||||
is UpdateUtils.Result.NoUpdate -> {
|
||||
val md = """
|
||||
## No update
|
||||
You are using latest version (**${BuildConfig.GIT_SHA}**)
|
||||
""".trimIndent()
|
||||
builder.setMessage(markwon.toMarkdown(md))
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
|
||||
is UpdateUtils.Result.Error -> {
|
||||
// trimIndent is confused by tabs in stack trace
|
||||
val md = """
|
||||
## Error
|
||||
```
|
||||
${result.throwable.stackTraceString()}
|
||||
```
|
||||
"""
|
||||
builder.setMessage(markwon.toMarkdown(md))
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun openArtifact(artifact: MarkwonArtifact) {
|
||||
Debug.i(artifact)
|
||||
openResultFragment(init(artifact))
|
||||
}
|
||||
|
||||
private fun openTag(tag: String) {
|
||||
Debug.i(tag)
|
||||
openResultFragment(init(tag))
|
||||
}
|
||||
|
||||
private fun openResultFragment(fragment: SampleListFragment) {
|
||||
openFragment(fragment)
|
||||
}
|
||||
|
||||
private fun openSample(sample: Sample) {
|
||||
openFragment(SampleFragment.init(sample))
|
||||
}
|
||||
|
||||
private fun openFragment(fragment: Fragment) {
|
||||
fragmentManager!!.beginTransaction()
|
||||
.setCustomAnimations(R.anim.screen_in, R.anim.screen_out, R.anim.screen_in_pop, R.anim.screen_out_pop)
|
||||
.replace(Window.ID_ANDROID_CONTENT, fragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
|
||||
private fun fetch() {
|
||||
|
||||
val sampleSearch: SampleSearch = when (val type = this.type) {
|
||||
is Type.Artifact -> SampleSearch.Artifact(search, type.artifact)
|
||||
is Type.Tag -> SampleSearch.Tag(search, type.tag)
|
||||
else -> SampleSearch.All(search)
|
||||
}
|
||||
|
||||
Debug.i(sampleSearch)
|
||||
|
||||
// clear current
|
||||
cancellable?.let {
|
||||
if (!it.isCancelled) {
|
||||
it.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
cancellable = sampleManager.samples(sampleSearch) {
|
||||
val addVersion = sampleSearch is SampleSearch.All && TextUtils.isEmpty(sampleSearch.text)
|
||||
bindSamples(it, addVersion)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_ARTIFACT = "arg.Artifact"
|
||||
private const val ARG_TAG = "arg.Tag"
|
||||
private const val ARG_SEARCH = "arg.Search"
|
||||
private const val STATE = "key.State"
|
||||
|
||||
fun init(): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle()
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun init(artifact: MarkwonArtifact): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString(ARG_ARTIFACT, artifact.name)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun init(tag: String): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString(ARG_TAG, tag)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun init(search: SampleSearch): SampleListFragment {
|
||||
val fragment = SampleListFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
|
||||
when (search) {
|
||||
is SampleSearch.Artifact -> putString(ARG_ARTIFACT, search.artifact.name)
|
||||
is SampleSearch.Tag -> putString(ARG_TAG, search.tag)
|
||||
}
|
||||
|
||||
val query = search.text
|
||||
if (query != null) {
|
||||
putString(ARG_SEARCH, query)
|
||||
}
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun markwon(context: Context): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(MovementMethodPlugin.none())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun parseType(arguments: Bundle): Type {
|
||||
val name = arguments.getString(ARG_ARTIFACT)
|
||||
val tag = arguments.getString(ARG_TAG)
|
||||
return when {
|
||||
name != null -> Type.Artifact(MarkwonArtifact.valueOf(name))
|
||||
tag != null -> Type.Tag(tag)
|
||||
else -> Type.All
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private data class State(
|
||||
val search: String?,
|
||||
val recyclerScrollPosition: RecyclerScrollPosition?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
private data class RecyclerScrollPosition(
|
||||
val position: Int,
|
||||
val offset: Int
|
||||
) : Parcelable
|
||||
|
||||
private val RecyclerView.scrollPosition: RecyclerScrollPosition?
|
||||
get() {
|
||||
val holder = findFirstVisibleViewHolder() ?: return null
|
||||
val position = holder.adapterPosition
|
||||
val offset = holder.itemView.top
|
||||
return RecyclerScrollPosition(position, offset)
|
||||
}
|
||||
|
||||
// because findViewHolderForLayoutPosition doesn't work :'(
|
||||
private fun RecyclerView.findFirstVisibleViewHolder(): RecyclerView.ViewHolder? {
|
||||
if (childCount > 0) {
|
||||
val child = getChildAt(0)
|
||||
return findContainingViewHolder(child)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private sealed class Type {
|
||||
class Artifact(val artifact: MarkwonArtifact) : Type()
|
||||
class Tag(val tag: String) : Type()
|
||||
object All : Type()
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
|
||||
class SamplePreviewFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return markwonSample.createView(inflater, container)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
markwonSample.onViewCreated(view)
|
||||
}
|
||||
|
||||
private val markwonSample: MarkwonSample by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val sample: Sample = arguments!!.getParcelable<Sample>(ARG_SAMPLE)!!
|
||||
Class.forName(sample.javaClassName).newInstance() as MarkwonSample
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_SAMPLE = "arg.Sample"
|
||||
|
||||
fun init(sample: Sample): SamplePreviewFragment {
|
||||
return SamplePreviewFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_SAMPLE, sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui.adapt
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.markwon.app.R
|
||||
|
||||
class CheckForUpdateItem(private val action: () -> Unit) : Item<CheckForUpdateItem.Holder>(43L) {
|
||||
|
||||
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(R.layout.adapt_check_for_update, parent, false))
|
||||
}
|
||||
|
||||
override fun render(holder: Holder) {
|
||||
holder.button.setOnClickListener { action() }
|
||||
}
|
||||
|
||||
class Holder(view: View) : Item.Holder(view) {
|
||||
val button: View = requireView(R.id.button)
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui.adapt
|
||||
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.sample.Sample
|
||||
import io.noties.markwon.app.utils.displayName
|
||||
import io.noties.markwon.app.utils.hidden
|
||||
import io.noties.markwon.app.utils.tagDisplayName
|
||||
import io.noties.markwon.app.widget.FlowLayout
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
class SampleItem(
|
||||
private val markwon: Markwon,
|
||||
private val sample: Sample,
|
||||
private val onArtifactClick: (MarkwonArtifact) -> Unit,
|
||||
private val onTagClick: (String) -> Unit,
|
||||
private val onSampleClick: (Sample) -> Unit
|
||||
) : Item<SampleItem.Holder>(sample.id.hashCode().toLong()) {
|
||||
|
||||
// var search: String? = null
|
||||
|
||||
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
|
||||
markwon.toMarkdown(sample.description)
|
||||
}
|
||||
|
||||
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
return Holder(inflater.inflate(R.layout.adapt_sample, parent, false)).apply {
|
||||
description.setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
override fun render(holder: Holder) {
|
||||
holder.apply {
|
||||
title.text = sample.title
|
||||
|
||||
val text = this@SampleItem.text
|
||||
if (text.isEmpty()) {
|
||||
description.text = ""
|
||||
description.hidden = true
|
||||
} else {
|
||||
markwon.setParsedMarkdown(description, text)
|
||||
description.hidden = false
|
||||
}
|
||||
|
||||
// there is no need to display the core artifact (it is implicit),
|
||||
// hide if empty (removed core)
|
||||
artifacts.ensure(sample.artifacts.size, R.layout.view_artifact)
|
||||
.zip(sample.artifacts)
|
||||
.forEach { (view, artifact) ->
|
||||
(view as TextView).text = artifact.displayName
|
||||
view.setOnClickListener {
|
||||
onArtifactClick(artifact)
|
||||
}
|
||||
}
|
||||
|
||||
tags.ensure(sample.tags.size, R.layout.view_tag)
|
||||
.zip(sample.tags)
|
||||
.forEach { (view, tag) ->
|
||||
(view as TextView).text = tag.tagDisplayName
|
||||
view.setOnClickListener {
|
||||
onTagClick(tag)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
onSampleClick(sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder(itemView: View) : Item.Holder(itemView) {
|
||||
val title: TextView = requireView(R.id.title)
|
||||
val description: TextView = requireView(R.id.description)
|
||||
val artifacts: FlowLayout = requireView(R.id.artifacts)
|
||||
val tags: FlowLayout = requireView(R.id.tags)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SampleItem
|
||||
|
||||
if (sample != other.sample) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return sample.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun FlowLayout.ensure(viewsCount: Int, layoutResId: Int): List<View> {
|
||||
if (viewsCount > childCount) {
|
||||
// inflate new views
|
||||
val inflater = LayoutInflater.from(context)
|
||||
for (i in 0 until (viewsCount - childCount)) {
|
||||
addView(inflater.inflate(layoutResId, this, false))
|
||||
}
|
||||
} else {
|
||||
// return requested vies and GONE the rest
|
||||
for (i in viewsCount until childCount) {
|
||||
getChildAt(i).hidden = true
|
||||
}
|
||||
}
|
||||
return (0 until viewsCount).map { getChildAt(it) }
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package io.noties.markwon.app.sample.ui.adapt
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.noties.adapt.Item
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.LinkResolver
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.MarkwonSpansFactory
|
||||
import io.noties.markwon.app.BuildConfig
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.core.CoreProps
|
||||
import io.noties.markwon.core.MarkwonTheme
|
||||
import io.noties.markwon.core.spans.LinkSpan
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.movement.MovementMethodPlugin
|
||||
import org.commonmark.node.Link
|
||||
|
||||
class VersionItem : Item<VersionItem.Holder>(42L) {
|
||||
|
||||
private lateinit var context: Context
|
||||
|
||||
private val markwon: Markwon by lazy(LazyThreadSafetyMode.NONE) {
|
||||
Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(MovementMethodPlugin.link())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
||||
builder.setFactory(Link::class.java) { configuration, props ->
|
||||
LinkSpanNoUnderline(
|
||||
configuration.theme(),
|
||||
CoreProps.LINK_DESTINATION.require(props),
|
||||
configuration.linkResolver()
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val md = """
|
||||
<a href="${BuildConfig.GIT_REPOSITORY}/blob/master/CHANGELOG.md">
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||
</a>
|
||||
""".trimIndent()
|
||||
markwon.toMarkdown(md)
|
||||
}
|
||||
|
||||
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
|
||||
context = parent.context
|
||||
return Holder(inflater.inflate(R.layout.adapt_version, parent, false))
|
||||
}
|
||||
|
||||
override fun render(holder: Holder) {
|
||||
markwon.setParsedMarkdown(holder.textView, text)
|
||||
}
|
||||
|
||||
class Holder(view: View) : Item.Holder(view) {
|
||||
val textView: TextView = requireView(R.id.text_view)
|
||||
}
|
||||
|
||||
class LinkSpanNoUnderline(
|
||||
theme: MarkwonTheme,
|
||||
destination: String,
|
||||
resolver: LinkResolver
|
||||
) : LinkSpan(theme, destination, resolver) {
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
super.updateDrawState(ds)
|
||||
ds.isUnderlineText = false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
[{*.java, *.kt}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
@ -1,51 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.LastLineSpacingSpan;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629125321",
|
||||
title = "Additional spacing after block",
|
||||
description = "Add additional spacing (padding) after last line of a block",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.spacing, Tag.padding, Tag.span}
|
||||
)
|
||||
public class AdditionalSpacingSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Title title title title title title title title title title \n\ntext text text text";
|
||||
|
||||
// please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
|
||||
final int spacing = (int) (128 * context.getResources().getDisplayMetrics().density + .5F);
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
builder.headingBreakHeight(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.appendFactory(
|
||||
Heading.class,
|
||||
(configuration, props) -> new LastLineSpacingSpan(spacing));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.BlockHandlerDef;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629130227",
|
||||
title = "All blocks no padding",
|
||||
description = "Do not render new lines (padding) after all blocks",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.block, Tag.spacing, Tag.padding, Tag.rendering}
|
||||
)
|
||||
public class AllBlocksNoForcedNewLineSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Hello there!\n\n" +
|
||||
"* a first\n" +
|
||||
"* second\n" +
|
||||
"- third\n" +
|
||||
"* * nested one\n\n" +
|
||||
"> block quote\n\n" +
|
||||
"> > and nested one\n\n" +
|
||||
"```java\n" +
|
||||
"final int i = 0;\n" +
|
||||
"```\n\n";
|
||||
|
||||
// extend default block handler
|
||||
final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() {
|
||||
@Override
|
||||
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.blockHandler(blockHandler);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200729090524",
|
||||
title = "Block handler",
|
||||
description = "Custom block delimiters that control new lines after block nodes",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class BlockHandlerSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Heading\n" +
|
||||
"* one\n" +
|
||||
"* two\n" +
|
||||
"* three\n" +
|
||||
"---\n" +
|
||||
"> a quote\n\n" +
|
||||
"```\n" +
|
||||
"code\n" +
|
||||
"```\n" +
|
||||
"some text after";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.blockHandler(new BlockHandlerNoAdditionalNewLines());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class BlockHandlerNoAdditionalNewLines implements MarkwonVisitor.BlockHandler {
|
||||
|
||||
@Override
|
||||
public void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
// ensure that content rendered on a new line
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
// by default markwon here also has:
|
||||
// visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import android.content.Context
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200707102458",
|
||||
title = "Cache Markwon instance",
|
||||
description = "A static cache for `Markwon` instance " +
|
||||
"to be associated with a `Context`",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.cache]
|
||||
)
|
||||
class CacheMarkwonSample : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
render("# First!")
|
||||
render("## Second!!")
|
||||
render("### Third!!!")
|
||||
}
|
||||
|
||||
fun render(md: String) {
|
||||
val markwon = MarkwonCache.with(context)
|
||||
Debug.i("markwon: ${markwon.hashCode()}, md: '$md'")
|
||||
markwon.setMarkdown(textView, md)
|
||||
}
|
||||
}
|
||||
|
||||
object MarkwonCache {
|
||||
private val cache = Collections.synchronizedMap(WeakHashMap<Context, Markwon>())
|
||||
|
||||
fun with(context: Context): Markwon {
|
||||
// yeah, why work as expected? new value is returned each time, no caching occur
|
||||
// kotlin: 1.3.72
|
||||
// intellij plugin: 1.3.72-release-Studio4.0-5
|
||||
// return cache.getOrPut(context) {
|
||||
// // create your markwon instance here
|
||||
// return Markwon.builder(context)
|
||||
// .usePlugin(StrikethroughPlugin.create())
|
||||
// .build()
|
||||
// }
|
||||
|
||||
return cache[context] ?: {
|
||||
Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.build()
|
||||
.also {
|
||||
cache[context] = it
|
||||
}
|
||||
}.invoke()
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.text.style.BulletSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.ListItem;
|
||||
|
||||
import io.noties.debug.Debug;
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.SpanFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20201208150530",
|
||||
title = "Change bullet span",
|
||||
description = "Use a different span implementation to render bullet lists",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.rendering, Tag.spanFactory, Tag.span}
|
||||
)
|
||||
public class ChangeBulletSpanSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"* one\n" +
|
||||
"* two\n" +
|
||||
"* three\n" +
|
||||
"* * four\n" +
|
||||
" * five\n\n" +
|
||||
"- [ ] and task?\n" +
|
||||
"- [x] it is";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
// store original span factory (provides both bullet and ordered lists)
|
||||
final SpanFactory original = builder.getFactory(ListItem.class);
|
||||
|
||||
builder.setFactory(ListItem.class, (configuration, props) -> {
|
||||
if (CoreProps.LIST_ITEM_TYPE.require(props) == CoreProps.ListItemType.BULLET) {
|
||||
// additionally inspect bullet level
|
||||
final int level = CoreProps.BULLET_LIST_ITEM_LEVEL.require(props);
|
||||
Debug.i("rendering bullet list with level: %d", level);
|
||||
|
||||
// return _system_ bullet span, but can be any
|
||||
return new BulletSpan();
|
||||
}
|
||||
return original != null ? original.getSpans(configuration, props) : null;
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Layout
|
||||
import android.text.Spanned
|
||||
import android.text.TextPaint
|
||||
import android.text.style.ClickableSpan
|
||||
import android.text.style.LeadingMarginSpan
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.MarkwonSpansFactory
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
import io.noties.markwon.utils.LeadingMarginUtils
|
||||
import org.commonmark.node.FencedCodeBlock
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20210315112847",
|
||||
title = "Copy code block",
|
||||
description = "Copy contents of fenced code blocks",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.rendering, Tag.block, Tag.spanFactory, Tag.span]
|
||||
)
|
||||
class CopyCodeBlockSample : MarkwonTextViewSample() {
|
||||
|
||||
override fun render() {
|
||||
val md = """
|
||||
# Hello code blocks!
|
||||
```java
|
||||
final int i = 0;
|
||||
final Type t = Type.init()
|
||||
.filter(i -> i.even)
|
||||
.first(null);
|
||||
```
|
||||
bye bye!
|
||||
""".trimIndent()
|
||||
|
||||
val markwon = Markwon.builder(context)
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
|
||||
builder.appendFactory(FencedCodeBlock::class.java) { _, _ ->
|
||||
CopyContentsSpan()
|
||||
}
|
||||
builder.appendFactory(FencedCodeBlock::class.java) { _, _ ->
|
||||
CopyIconSpan(context.getDrawable(R.drawable.ic_code_white_24dp)!!)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
markwon.setMarkdown(textView, md)
|
||||
}
|
||||
|
||||
class CopyContentsSpan : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val spanned = (widget as? TextView)?.text as? Spanned ?: return
|
||||
val start = spanned.getSpanStart(this)
|
||||
val end = spanned.getSpanEnd(this)
|
||||
// by default code blocks have new lines before and after content
|
||||
val contents = spanned.subSequence(start, end).toString().trim()
|
||||
// copy code here
|
||||
Debug.i(contents)
|
||||
}
|
||||
|
||||
override fun updateDrawState(ds: TextPaint) {
|
||||
// do not apply link styling
|
||||
}
|
||||
}
|
||||
|
||||
class CopyIconSpan(val icon: Drawable) : LeadingMarginSpan {
|
||||
|
||||
init {
|
||||
if (icon.bounds.isEmpty) {
|
||||
icon.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeadingMargin(first: Boolean): Int = 0
|
||||
|
||||
override fun drawLeadingMargin(
|
||||
c: Canvas,
|
||||
p: Paint,
|
||||
x: Int,
|
||||
dir: Int,
|
||||
top: Int,
|
||||
baseline: Int,
|
||||
bottom: Int,
|
||||
text: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
first: Boolean,
|
||||
layout: Layout
|
||||
) {
|
||||
|
||||
// called for each line of text, we are interested only in first one
|
||||
if (!LeadingMarginUtils.selfStart(start, text, this)) return
|
||||
|
||||
val save = c.save()
|
||||
try {
|
||||
// horizontal position for icon
|
||||
val w = icon.bounds.width().toFloat()
|
||||
// minus quarter width as padding
|
||||
val left = layout.width - w - (w / 4F)
|
||||
c.translate(left, top.toFloat())
|
||||
icon.draw(c)
|
||||
} finally {
|
||||
c.restoreToCount(save)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,436 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.CustomNode;
|
||||
import org.commonmark.node.Delimited;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||
import org.commonmark.parser.delimiter.DelimiterRun;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629163248",
|
||||
title = "Custom extension",
|
||||
description = "Custom extension that adds an " +
|
||||
"icon from resources and renders it as image with " +
|
||||
"`@ic-name` syntax",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.parsing, Tag.rendering, Tag.plugin, Tag.image, Tag.extension, Tag.span}
|
||||
)
|
||||
public class CustomExtensionSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Hello! @ic-android-black-24\n\n" +
|
||||
"" +
|
||||
"Home 36 black: @ic-home-black-36\n\n" +
|
||||
"" +
|
||||
"Memory 48 black: @ic-memory-black-48\n\n" +
|
||||
"" +
|
||||
"### I AM ANOTHER HEADER\n\n" +
|
||||
"" +
|
||||
"Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64" +
|
||||
"";
|
||||
|
||||
// note that we haven't registered CorePlugin, as it's the only one that can be
|
||||
// implicitly deducted and added automatically. All other plugins require explicit
|
||||
// `usePlugin` call
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(IconPlugin.create(IconSpanProvider.create(context, 0)))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class IconPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) {
|
||||
return new IconPlugin(iconSpanProvider);
|
||||
}
|
||||
|
||||
private final IconSpanProvider iconSpanProvider;
|
||||
|
||||
IconPlugin(@NonNull IconSpanProvider iconSpanProvider) {
|
||||
this.iconSpanProvider = iconSpanProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.customDelimiterProcessor(IconProcessor.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(IconNode.class, (visitor, iconNode) -> {
|
||||
|
||||
final String name = iconNode.name();
|
||||
final String color = iconNode.color();
|
||||
final String size = iconNode.size();
|
||||
|
||||
if (!TextUtils.isEmpty(name)
|
||||
&& !TextUtils.isEmpty(color)
|
||||
&& !TextUtils.isEmpty(size)) {
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.builder().append(name);
|
||||
visitor.setSpans(length, iconSpanProvider.provide(name, color, size));
|
||||
visitor.builder().append(' ');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
return IconProcessor.prepare(markdown);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class IconSpanProvider {
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NonNull
|
||||
public static IconSpanProvider create(@NonNull Context context, @DrawableRes int fallBack) {
|
||||
return new Impl(context, fallBack);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public abstract IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size);
|
||||
|
||||
|
||||
private static class Impl extends IconSpanProvider {
|
||||
|
||||
private final Context context;
|
||||
private final Resources resources;
|
||||
private final int fallBack;
|
||||
|
||||
Impl(@NonNull Context context, @DrawableRes int fallBack) {
|
||||
this.context = context;
|
||||
this.resources = context.getResources();
|
||||
this.fallBack = fallBack;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||
final String resName = iconName(name, color, size);
|
||||
int resId = resources.getIdentifier(resName, "drawable", context.getPackageName());
|
||||
if (resId == 0) {
|
||||
resId = fallBack;
|
||||
}
|
||||
return new IconSpan(getDrawable(resId), IconSpan.ALIGN_CENTER);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
private static String iconName(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||
return "ic_" + name + "_" + color + "_" + size + "dp";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Drawable getDrawable(int resId) {
|
||||
//noinspection ConstantConditions
|
||||
return context.getDrawable(resId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IconSpan extends ReplacementSpan {
|
||||
|
||||
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER})
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
@interface Alignment {
|
||||
}
|
||||
|
||||
public static final int ALIGN_BOTTOM = 0;
|
||||
public static final int ALIGN_BASELINE = 1;
|
||||
public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height
|
||||
|
||||
|
||||
private final Drawable drawable;
|
||||
|
||||
private final int alignment;
|
||||
|
||||
public IconSpan(@NonNull Drawable drawable, @Alignment int alignment) {
|
||||
this.drawable = drawable;
|
||||
this.alignment = alignment;
|
||||
if (drawable.getBounds().isEmpty()) {
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
|
||||
|
||||
final Rect rect = drawable.getBounds();
|
||||
|
||||
if (fm != null) {
|
||||
fm.ascent = -rect.bottom;
|
||||
fm.descent = 0;
|
||||
|
||||
fm.top = fm.ascent;
|
||||
fm.bottom = 0;
|
||||
}
|
||||
|
||||
return rect.right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
|
||||
final int b = bottom - drawable.getBounds().bottom;
|
||||
|
||||
final int save = canvas.save();
|
||||
try {
|
||||
final int translationY;
|
||||
if (ALIGN_CENTER == alignment) {
|
||||
translationY = b - ((bottom - top - drawable.getBounds().height()) / 2);
|
||||
} else if (ALIGN_BASELINE == alignment) {
|
||||
translationY = b - paint.getFontMetricsInt().descent;
|
||||
} else {
|
||||
translationY = b;
|
||||
}
|
||||
canvas.translate(x, translationY);
|
||||
drawable.draw(canvas);
|
||||
} finally {
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IconProcessor implements DelimiterProcessor {
|
||||
|
||||
@NonNull
|
||||
public static IconProcessor create() {
|
||||
return new IconProcessor();
|
||||
}
|
||||
|
||||
// ic-home-black-24
|
||||
private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)");
|
||||
|
||||
private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-";
|
||||
|
||||
/**
|
||||
* Should be used when input string does not wrap icon definition with `@` from both ends.
|
||||
* So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier
|
||||
* and more predictable (cannot specify multiple ending delimiters, as we would require them:
|
||||
* space, newline, end of a document, and a lot of more)
|
||||
*
|
||||
* @param input to process
|
||||
* @return processed string
|
||||
* @see #prepare(StringBuilder)
|
||||
*/
|
||||
@NonNull
|
||||
public static String prepare(@NonNull String input) {
|
||||
final StringBuilder builder = new StringBuilder(input);
|
||||
prepare(builder);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static void prepare(@NonNull StringBuilder builder) {
|
||||
|
||||
int start = builder.indexOf(TO_FIND);
|
||||
int end;
|
||||
|
||||
while (start > -1) {
|
||||
|
||||
end = iconDefinitionEnd(start + TO_FIND.length(), builder);
|
||||
|
||||
// if we match our pattern, append `@` else ignore
|
||||
if (iconDefinitionValid(builder.subSequence(start + 1, end))) {
|
||||
builder.insert(end, '@');
|
||||
}
|
||||
|
||||
// move to next
|
||||
start = builder.indexOf(TO_FIND, end);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getOpeningCharacter() {
|
||||
return IconNode.DELIMITER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getClosingCharacter() {
|
||||
return IconNode.DELIMITER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
|
||||
return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Text opener, Text closer, int delimiterUse) {
|
||||
|
||||
final IconGroupNode iconGroupNode = new IconGroupNode();
|
||||
|
||||
final Node next = opener.getNext();
|
||||
|
||||
boolean handled = false;
|
||||
|
||||
// process only if we have exactly one Text node
|
||||
if (next instanceof Text && next.getNext() == closer) {
|
||||
|
||||
final String text = ((Text) next).getLiteral();
|
||||
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
|
||||
// attempt to match
|
||||
final Matcher matcher = PATTERN.matcher(text);
|
||||
if (matcher.matches()) {
|
||||
final IconNode iconNode = new IconNode(
|
||||
matcher.group(1),
|
||||
matcher.group(2),
|
||||
matcher.group(3)
|
||||
);
|
||||
iconGroupNode.appendChild(iconNode);
|
||||
next.unlink();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
|
||||
// restore delimiters if we didn't match
|
||||
|
||||
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
|
||||
|
||||
Node node;
|
||||
for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
|
||||
node = tmp.getNext();
|
||||
// append a child anyway
|
||||
iconGroupNode.appendChild(tmp);
|
||||
}
|
||||
|
||||
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
|
||||
}
|
||||
|
||||
opener.insertBefore(iconGroupNode);
|
||||
}
|
||||
|
||||
private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) {
|
||||
|
||||
// all spaces, new lines, non-words or digits,
|
||||
|
||||
char c;
|
||||
|
||||
int end = -1;
|
||||
for (int i = index; i < builder.length(); i++) {
|
||||
c = builder.charAt(i);
|
||||
if (Character.isWhitespace(c)
|
||||
|| !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (end == -1) {
|
||||
end = builder.length();
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
private static boolean iconDefinitionValid(@NonNull CharSequence cs) {
|
||||
final Matcher matcher = PATTERN.matcher(cs);
|
||||
return matcher.matches();
|
||||
}
|
||||
}
|
||||
|
||||
class IconNode extends CustomNode implements Delimited {
|
||||
|
||||
public static final char DELIMITER = '@';
|
||||
|
||||
public static final String DELIMITER_STRING = "" + DELIMITER;
|
||||
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String color;
|
||||
|
||||
private final String size;
|
||||
|
||||
public IconNode(@NonNull String name, @NonNull String color, @NonNull String size) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String color() {
|
||||
return color;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOpeningDelimiter() {
|
||||
return DELIMITER_STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClosingDelimiter() {
|
||||
return DELIMITER_STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "IconNode{" +
|
||||
"name='" + name + '\'' +
|
||||
", color='" + color + '\'' +
|
||||
", size='" + size + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
class IconGroupNode extends CustomNode {
|
||||
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629123617",
|
||||
title = "Customize theme",
|
||||
description = "Customize `MarkwonTheme` styling",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.style, Tag.theme, Tag.plugin}
|
||||
)
|
||||
public class CustomizeThemeSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
final String md = "`A code` that is rendered differently\n\n```\nHello!\n```";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
builder
|
||||
.codeBackgroundColor(Color.BLACK)
|
||||
.codeTextColor(Color.RED);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200826122247",
|
||||
title = "Deeplinks",
|
||||
description = "Handling of deeplinks (app handles https scheme to deep link into content)",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.links
|
||||
)
|
||||
public class DeeplinksSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Deeplinks\n\n" +
|
||||
"The [link](https://noties.io/Markwon/app/sample/20200826122247) to self";
|
||||
|
||||
// nothing special is required
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Emphasis;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Text;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||
import org.commonmark.parser.delimiter.DelimiterRun;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630194017",
|
||||
title = "Custom delimiter processor",
|
||||
description = "Custom parsing delimiter processor with `?` character",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.parsing
|
||||
)
|
||||
public class DelimiterProcessorSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"?hello? there!";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.customDelimiterProcessor(new QuestionDelimiterProcessor());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class QuestionDelimiterProcessor implements DelimiterProcessor {
|
||||
|
||||
@Override
|
||||
public char getOpeningCharacter() {
|
||||
return '?';
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getClosingCharacter() {
|
||||
return '?';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
|
||||
if (opener.length() >= 1 && closer.length() >= 1) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Text opener, Text closer, int delimiterUse) {
|
||||
final Node node = new Emphasis();
|
||||
|
||||
Node tmp = opener.getNext();
|
||||
while (tmp != null && tmp != closer) {
|
||||
Node next = tmp.getNext();
|
||||
node.appendChild(tmp);
|
||||
tmp = next;
|
||||
}
|
||||
|
||||
opener.insertAfter(node);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629123308",
|
||||
title = "Disable node from rendering",
|
||||
description = "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
|
||||
artifacts = {MarkwonArtifact.CORE},
|
||||
tags = {Tag.parsing, Tag.rendering}
|
||||
)
|
||||
public class DisableNodeSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
|
||||
// for example to disable rendering of heading:
|
||||
// try commenting this out to see that otherwise headings will be rendered
|
||||
builder.on(Heading.class, null);
|
||||
|
||||
// same method can be used to override existing visitor by specifying
|
||||
// a new NodeVisitor instance
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.core.CorePlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
import org.commonmark.node.BlockQuote
|
||||
import org.commonmark.parser.Parser
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200627075012",
|
||||
title = "Enabled markdown blocks",
|
||||
description = "Modify/inspect enabled by `CorePlugin` block types. " +
|
||||
"Disable quotes or other blocks from being parsed",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.parsing, Tag.block, Tag.plugin]
|
||||
)
|
||||
class EnabledBlockTypesSample : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
val md = """
|
||||
# Heading
|
||||
## Second level
|
||||
> Quote is not handled
|
||||
""".trimIndent()
|
||||
|
||||
val markwon = Markwon.builder(context)
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureParser(builder: Parser.Builder) {
|
||||
// obtain all enabled block types
|
||||
val enabledBlockTypes = CorePlugin.enabledBlockTypes()
|
||||
// it is safe to modify returned collection
|
||||
// remove quotes
|
||||
enabledBlockTypes.remove(BlockQuote::class.java)
|
||||
|
||||
builder.enabledBlockTypes(enabledBlockTypes)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
markwon.setMarkdown(textView, md)
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import io.noties.debug.Debug
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20201111221945",
|
||||
title = "Exclude part of input from parsing",
|
||||
description = "Exclude part of input from parsing by splitting input with delimiters",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.parsing]
|
||||
)
|
||||
class ExcludeFromParsingSample : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
|
||||
// cannot have continuous markdown between parts (so a node started in one part and ended in other)
|
||||
// with this approach
|
||||
// also exclude will start a new block and won't seamlessly continue any existing markdown one (so
|
||||
// if started inside a blockquote, then blockquote would be closed)
|
||||
|
||||
val md = """
|
||||
# Hello
|
||||
|
||||
we are **going** to exclude some parts of this input _from_ parsing
|
||||
|
||||
$EXCLUDE_START
|
||||
what is **good** is that we
|
||||
> do not need to care about blocks or inlines
|
||||
* and
|
||||
* everything
|
||||
* else
|
||||
$EXCLUDE_END
|
||||
|
||||
**then** markdown _again_
|
||||
|
||||
and empty exclude at end: $EXCLUDE_START$EXCLUDE_END
|
||||
""".trimIndent()
|
||||
|
||||
val markwon = Markwon.create(context)
|
||||
val matcher = Pattern.compile(RE, Pattern.MULTILINE).matcher(md)
|
||||
|
||||
val builder by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SpannableStringBuilder()
|
||||
}
|
||||
|
||||
var end: Int = 0
|
||||
|
||||
while (matcher.find()) {
|
||||
val start = matcher.start()
|
||||
Debug.i(end, start, md.substring(end, start))
|
||||
builder.append(markwon.toMarkdown(md.substring(end, start)))
|
||||
builder.append(matcher.group(1))
|
||||
end = matcher.end()
|
||||
}
|
||||
|
||||
if (end != md.length) {
|
||||
builder.append(markwon.toMarkdown(md.substring(end)))
|
||||
}
|
||||
|
||||
markwon.setParsedMarkdown(textView, builder)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EXCLUDE_START = "##IGNORE##"
|
||||
const val EXCLUDE_END = "--IGNORE--"
|
||||
|
||||
const val RE = "${EXCLUDE_START}([\\s\\S]*?)${EXCLUDE_END}"
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.InlineParserFactory;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.BuildConfig;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.inlineparser.InlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629162023",
|
||||
title = "User mention and issue (via text)",
|
||||
description = "Github-like user mention and issue " +
|
||||
"rendering via `CorePlugin.OnTextAddedListener`",
|
||||
artifacts = {MarkwonArtifact.CORE, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.parsing, Tag.textAddedListener, Tag.rendering}
|
||||
)
|
||||
public class GithubUserIssueInlineParsingSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Custom Extension 2\n" +
|
||||
"\n" +
|
||||
"This is an issue #1\n" +
|
||||
"Done by @noties and other @dude";
|
||||
|
||||
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||
// include all current defaults (otherwise will be empty - contain only our inline-processors)
|
||||
// included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults`
|
||||
// .includeDefaults()
|
||||
.addInlineProcessor(new IssueInlineProcessor())
|
||||
.addInlineProcessor(new UserInlineProcessor())
|
||||
.build();
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.inlineParserFactory(inlineParserFactory);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class IssueInlineProcessor extends InlineProcessor {
|
||||
|
||||
private static final Pattern RE = Pattern.compile("\\d+");
|
||||
|
||||
@Override
|
||||
public char specialCharacter() {
|
||||
return '#';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node parse() {
|
||||
final String id = match(RE);
|
||||
if (id != null) {
|
||||
final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null);
|
||||
link.appendChild(text("#" + id));
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createIssueOrPullRequestLinkDestination(@NonNull String id) {
|
||||
return BuildConfig.GIT_REPOSITORY + "/issues/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
class UserInlineProcessor extends InlineProcessor {
|
||||
|
||||
private static final Pattern RE = Pattern.compile("\\w+");
|
||||
|
||||
@Override
|
||||
public char specialCharacter() {
|
||||
return '@';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node parse() {
|
||||
final String user = match(RE);
|
||||
if (user != null) {
|
||||
final Link link = new Link(createUserLinkDestination(user), null);
|
||||
link.appendChild(text("@" + user));
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createUserLinkDestination(@NonNull String user) {
|
||||
return "https://github.com/" + user;
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.RenderProps;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.app.BuildConfig;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CorePlugin;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629162024",
|
||||
title = "User mention and issue (via text)",
|
||||
description = "Github-like user mention and issue " +
|
||||
"rendering via `CorePlugin.OnTextAddedListener`",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.parsing, Tag.textAddedListener, Tag.rendering}
|
||||
)
|
||||
public class GithubUserIssueOnTextAddedSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Custom Extension 2\n" +
|
||||
"\n" +
|
||||
"This is an issue #1\n" +
|
||||
"Done by @noties and other @dude";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(CorePlugin.class, corePlugin ->
|
||||
corePlugin.addOnTextAddedListener(new GithubLinkifyRegexTextAddedListener()));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListener {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("((#\\d+)|(@\\w+))", Pattern.MULTILINE);
|
||||
|
||||
@Override
|
||||
public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) {
|
||||
|
||||
final Matcher matcher = PATTERN.matcher(text);
|
||||
|
||||
String value;
|
||||
String url;
|
||||
int index;
|
||||
|
||||
while (matcher.find()) {
|
||||
|
||||
value = matcher.group(1);
|
||||
|
||||
// detect which one it is
|
||||
if ('#' == value.charAt(0)) {
|
||||
url = createIssueOrPullRequestLink(value.substring(1));
|
||||
} else {
|
||||
url = createUserLink(value.substring(1));
|
||||
}
|
||||
|
||||
// it's important to use `start` value (represents start-index of `text` in the visitor)
|
||||
index = start + matcher.start();
|
||||
|
||||
setLink(visitor, url, index, index + value.length());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String createIssueOrPullRequestLink(@NonNull String number) {
|
||||
// issues and pull-requests on github follow the same pattern and we
|
||||
// cannot know for sure which one it is, but if we use issues for all types,
|
||||
// github will automatically redirect to pull-request if it's the one which is opened
|
||||
return BuildConfig.GIT_REPOSITORY + "/issues/" + number;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String createUserLink(@NonNull String user) {
|
||||
return "https://github.com/" + user;
|
||||
}
|
||||
|
||||
private void setLink(@NonNull MarkwonVisitor visitor, @NonNull String destination, int start, int end) {
|
||||
|
||||
// might a simpler one, but it doesn't respect possible changes to links
|
||||
// visitor.builder().setSpan(
|
||||
// new LinkSpan(visitor.configuration().theme(), destination, visitor.configuration().linkResolver()),
|
||||
// start,
|
||||
// end
|
||||
// );
|
||||
|
||||
// use default handlers for links
|
||||
final MarkwonConfiguration configuration = visitor.configuration();
|
||||
final RenderProps renderProps = visitor.renderProps();
|
||||
|
||||
CoreProps.LINK_DESTINATION.set(renderProps, destination);
|
||||
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
configuration.spansFactory().require(Link.class).getSpans(configuration, renderProps),
|
||||
start,
|
||||
end
|
||||
);
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20201203224611",
|
||||
title = "Color of heading",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class HeadingColorSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
final String md = "" +
|
||||
"# Heading 1\n" +
|
||||
"## Heading 2\n" +
|
||||
"### Heading 3\n" +
|
||||
"#### Heading 4";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.appendFactory(Heading.class, (configuration, props) -> {
|
||||
// here you can also inspect heading level
|
||||
final int level = CoreProps.HEADING_LEVEL.require(props);
|
||||
final int color;
|
||||
if (level == 1) {
|
||||
color = Color.RED;
|
||||
} else if (level == 2) {
|
||||
color = Color.GREEN;
|
||||
} else {
|
||||
color = Color.BLUE;
|
||||
}
|
||||
return new ForegroundColorSpan(color);
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.BlockHandlerDef;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629125924",
|
||||
title = "Heading no padding (block handler)",
|
||||
description = "Process padding (spacing) after heading with a " +
|
||||
"`BlockHandler`",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.block, Tag.spacing, Tag.padding, Tag.heading, Tag.rendering}
|
||||
)
|
||||
public class HeadingNoSpaceBlockHandlerSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Title title title title title title title title title title\n\n" +
|
||||
"text text text text" +
|
||||
"";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.blockHandler(new BlockHandlerDef() {
|
||||
@Override
|
||||
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
if (node instanceof Heading) {
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
// ensure new line but do not force insert one
|
||||
}
|
||||
} else {
|
||||
super.blockEnd(visitor, node);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629125622",
|
||||
title = "Heading no padding",
|
||||
description = "Do not add a new line after heading node",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.spacing, Tag.padding, Tag.spacing, Tag.rendering}
|
||||
)
|
||||
public class HeadingNoSpaceSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Title title title title title title title title title title" +
|
||||
"\n\ntext text text text" +
|
||||
"";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
builder.headingBreakHeight(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Heading.class, (visitor, heading) -> {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(heading);
|
||||
|
||||
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
|
||||
|
||||
visitor.setSpansForNodeOptional(heading, length);
|
||||
|
||||
if (visitor.hasNext(heading)) {
|
||||
visitor.ensureNewLine();
|
||||
// by default Markwon adds a new line here
|
||||
// visitor.forceNewLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629170857",
|
||||
title = "Inline parsing without defaults",
|
||||
description = "Configure inline parser plugin to **not** have any **inline** parsing",
|
||||
artifacts = {MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.parsing}
|
||||
)
|
||||
public class InlinePluginNoDefaultsSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Heading\n" +
|
||||
"`code` inlined and **bold** here";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
|
||||
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
|
||||
// // if anything, they can be included here
|
||||
//// factoryBuilder.includeDefaults()
|
||||
// }))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.image.AsyncDrawableScheduler;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200826084338",
|
||||
title = "Justify text",
|
||||
description = "Justify text with `justificationMode` argument on Oreo (>= 26)",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class JustifyModeSample extends MarkwonTextViewSample {
|
||||
@SuppressLint("WrongConstant")
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
/*
|
||||
nice, API 29 though
|
||||
```
|
||||
Error: Must be one of: LineBreaker.JUSTIFICATION_MODE_NONE, LineBreaker.JUSTIFICATION_MODE_INTER_WORD [WrongConstant]
|
||||
```
|
||||
*/
|
||||
textView.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD);
|
||||
}
|
||||
|
||||
final String md = "" +
|
||||
"# Justify\n\n" +
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
|
||||
"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. **Quisque porta ornare posuere.** Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
|
||||
"";
|
||||
|
||||
if (false) {
|
||||
// specify bufferType to make justificationMode argument be ignored
|
||||
// Actually just calling method with BufferType argument would make
|
||||
// justification gone
|
||||
textView.setText(md, TextView.BufferType.SPANNABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.build();
|
||||
|
||||
if (true) {
|
||||
final Spanned spanned = markwon.toMarkdown(md);
|
||||
|
||||
// NB! the call to `setText` without arguments
|
||||
textView.setText(spanned);
|
||||
|
||||
// if a plugin relies on `afterSetText` then we must manually call it,
|
||||
// for example images are scheduled this way:
|
||||
AsyncDrawableScheduler.schedule(textView);
|
||||
return;
|
||||
}
|
||||
|
||||
// cannot use that
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.BulletList;
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.Prop;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.core.spans.BulletListItemSpan;
|
||||
import io.noties.markwon.core.spans.OrderedListItemSpan;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629130954",
|
||||
title = "Letter ordered list",
|
||||
description = "Render bullet list inside an ordered list with letters instead of bullets",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.rendering, Tag.plugin, Tag.lists}
|
||||
)
|
||||
public class LetterOrderedListSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// bullet list nested in ordered list renders letters instead of bullets
|
||||
final String md = "" +
|
||||
"1. Hello there!\n" +
|
||||
"1. And here is how:\n" +
|
||||
" - First\n" +
|
||||
" - Second\n" +
|
||||
" - Third\n" +
|
||||
" 1. And first here\n\n";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
private static final Prop<String> BULLET_LETTER = Prop.of("my-bullet-letter");
|
||||
|
||||
// or introduce some kind of synchronization if planning to use from multiple threads,
|
||||
// for example via ThreadLocal
|
||||
private final SparseIntArray bulletCounter = new SparseIntArray();
|
||||
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
// clear counter after render
|
||||
bulletCounter.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
// NB that both ordered and bullet lists are represented
|
||||
// by ListItem (must inspect parent to detect the type)
|
||||
builder.on(ListItem.class, (visitor, listItem) -> {
|
||||
// mimic original behaviour (copy-pasta from CorePlugin)
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.visitChildren(listItem);
|
||||
|
||||
final Node parent = listItem.getParent();
|
||||
if (parent instanceof OrderedList) {
|
||||
|
||||
final int start = ((OrderedList) parent).getStartNumber();
|
||||
|
||||
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
|
||||
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
|
||||
|
||||
// after we have visited the children increment start number
|
||||
final OrderedList orderedList = (OrderedList) parent;
|
||||
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
|
||||
|
||||
} else {
|
||||
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
|
||||
|
||||
if (isBulletOrdered(parent)) {
|
||||
// obtain current count value
|
||||
final int count = currentBulletCountIn(parent);
|
||||
BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
|
||||
// update current count value
|
||||
setCurrentBulletCountIn(parent, count + 1);
|
||||
} else {
|
||||
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
|
||||
// clear letter info when regular bullet list is used
|
||||
BULLET_LETTER.clear(visitor.renderProps());
|
||||
}
|
||||
}
|
||||
|
||||
visitor.setSpansForNodeOptional(listItem, length);
|
||||
|
||||
if (visitor.hasNext(listItem)) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(ListItem.class, (configuration, props) -> {
|
||||
final Object spans;
|
||||
|
||||
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
|
||||
final String letter = BULLET_LETTER.get(props);
|
||||
if (!TextUtils.isEmpty(letter)) {
|
||||
// NB, we are using OrderedListItemSpan here!
|
||||
spans = new OrderedListItemSpan(
|
||||
configuration.theme(),
|
||||
letter
|
||||
);
|
||||
} else {
|
||||
spans = new BulletListItemSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
|
||||
+ "." + '\u00a0';
|
||||
|
||||
spans = new OrderedListItemSpan(
|
||||
configuration.theme(),
|
||||
number
|
||||
);
|
||||
}
|
||||
|
||||
return spans;
|
||||
});
|
||||
}
|
||||
|
||||
private int currentBulletCountIn(@NonNull Node parent) {
|
||||
return bulletCounter.get(parent.hashCode(), 0);
|
||||
}
|
||||
|
||||
private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
|
||||
bulletCounter.put(parent.hashCode(), count);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createBulletLetter(int count) {
|
||||
// or lower `a`
|
||||
// `'u00a0` is non-breakable space char
|
||||
return ((char) ('A' + count)) + ".\u00a0";
|
||||
}
|
||||
|
||||
private static int listLevel(@NonNull Node node) {
|
||||
int level = 0;
|
||||
Node parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof ListItem) {
|
||||
level += 1;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
private static boolean isBulletOrdered(@NonNull Node node) {
|
||||
node = node.getParent();
|
||||
while (node != null) {
|
||||
if (node instanceof OrderedList) {
|
||||
return true;
|
||||
}
|
||||
if (node instanceof BulletList) {
|
||||
return false;
|
||||
}
|
||||
node = node.getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.UpdateAppearance;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200702101224",
|
||||
title = "Remove link underline",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.links, Tag.rendering, Tag.span}
|
||||
)
|
||||
public class LinkRemoveUnderlineSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"There are a lot of [links](#) [here](#)";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveUnderlineSpan extends CharacterStyle implements UpdateAppearance {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setUnderlineText(false);
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.LinkResolver;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.LinkSpan;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629122230",
|
||||
title = "Obtain link title",
|
||||
description = "Obtain title (text) of clicked link, `[title](#destination)`",
|
||||
artifacts = {MarkwonArtifact.CORE},
|
||||
tags = {Tag.links, Tag.span}
|
||||
)
|
||||
public class LinkTitleSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Links\n\n" +
|
||||
"[link title](#)";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(Link.class, (configuration, props) ->
|
||||
// create a subclass of markwon LinkSpan
|
||||
new ClickSelfSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.LINK_DESTINATION.require(props),
|
||||
configuration.linkResolver()
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class ClickSelfSpan extends LinkSpan {
|
||||
|
||||
ClickSelfSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull String link,
|
||||
@NonNull LinkResolver resolver) {
|
||||
super(theme, link, resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
Toast.makeText(
|
||||
widget.getContext(),
|
||||
String.format(Locale.ROOT, "clicked link title: '%s'", linkTitle(widget)),
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
super.onClick(widget);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private CharSequence linkTitle(@NonNull View widget) {
|
||||
|
||||
if (!(widget instanceof TextView)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Spanned spanned = (Spanned) ((TextView) widget).getText();
|
||||
final int start = spanned.getSpanStart(this);
|
||||
final int end = spanned.getSpanEnd(this);
|
||||
|
||||
if (start < 0 || end < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return spanned.subSequence(start, end);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629124005",
|
||||
title = "Links without scheme",
|
||||
description = "Links without scheme are considered to be `https`",
|
||||
artifacts = {MarkwonArtifact.CORE},
|
||||
tags = {Tag.links, Tag.defaults}
|
||||
)
|
||||
public class LinkWithoutSchemeSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Links without scheme\n" +
|
||||
"[a link without scheme](github.com) is considered to be `https`.\n" +
|
||||
"Override `LinkResolverDef` to change this functionality" +
|
||||
"";
|
||||
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629171212",
|
||||
title = "No parsing",
|
||||
description = "All commonmark parsing is disabled (both inlines and blocks)",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.parsing, Tag.rendering}
|
||||
)
|
||||
public class NoParsingSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Heading\n" +
|
||||
"[link](#) was _here_ and `then` and it was:\n" +
|
||||
"> a quote\n" +
|
||||
"```java\n" +
|
||||
"final int someJavaCode = 0;\n" +
|
||||
"```\n";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
// disable inline parsing
|
||||
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.enabledBlockTypes(Collections.emptySet());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20201203221806",
|
||||
title = "Ordered list numbers",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class OrderedListNumbersSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "# Ordered lists\n\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"1. hello there\n" +
|
||||
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
|
||||
"1. okay, np\n" +
|
||||
"";
|
||||
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Paragraph;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629122647",
|
||||
title = "Paragraph style",
|
||||
description = "Apply a style (via span) to a paragraph",
|
||||
artifacts = {MarkwonArtifact.CORE},
|
||||
tags = {Tag.paragraph, Tag.style, Tag.span}
|
||||
)
|
||||
public class ParagraphSpanStyle extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "# Hello!\n\nA paragraph?\n\nIt should be!";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
// apply a span to a Paragraph
|
||||
builder.setFactory(Paragraph.class, (configuration, props) ->
|
||||
new ForegroundColorSpan(Color.GREEN));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.PrecomputedFutureTextSetterCompat;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.app.readme.GithubImageDestinationProcessor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonRecyclerViewSample;
|
||||
import io.noties.markwon.app.utils.SampleUtilsKtKt;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.ext.tables.TablePlugin;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200702092446",
|
||||
title = "PrecomputedFutureTextSetterCompat",
|
||||
description = "Usage of `PrecomputedFutureTextSetterCompat` " +
|
||||
"inside a `RecyclerView` with appcompat",
|
||||
artifacts = {MarkwonArtifact.RECYCLER},
|
||||
tags = {Tag.recyclerView, Tag.precomputedText}
|
||||
)
|
||||
public class PrecomputedFutureSample extends MarkwonRecyclerViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
if (!hasAppCompat()) {
|
||||
/*
|
||||
PLEASE COMPILE WITH `APPCOMPAT` dependency
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
final String md = SampleUtilsKtKt.loadReadMe(context);
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.textSetter(PrecomputedFutureTextSetterCompat.create())
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.imageDestinationProcessor(new GithubImageDestinationProcessor());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final MarkwonAdapter adapter = MarkwonAdapter
|
||||
.createTextViewIsRoot(R.layout.adapter_appcompat_default_entry);
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
adapter.setMarkdown(markwon, md);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static boolean hasAppCompat() {
|
||||
try {
|
||||
Class.forName("androidx.appcompat.widget.AppCompatTextView");
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.PrecomputedTextSetterCompat;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200702091654",
|
||||
title = "PrecomputedTextSetterCompat",
|
||||
description = "`TextSetter` to use `PrecomputedTextSetterCompat`",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.precomputedText
|
||||
)
|
||||
public class PrecomputedSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Heading\n" +
|
||||
"**bold** some precomputed spans via `PrecomputedTextSetterCompat`";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.textSetter(PrecomputedTextSetterCompat.create(Executors.newCachedThreadPool()))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629161505",
|
||||
title = "Read more plugin",
|
||||
description = "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.plugin}
|
||||
)
|
||||
public class ReadMorePluginSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"Lorem **ipsum**  sit amet, consectetur adipiscing elit. Morbi vitae enim ut sem aliquet ultrices. Nunc a accumsan orci. Suspendisse tortor ante, lacinia ac scelerisque sed, dictum eget metus. Morbi ante augue, tristique eget quam in, vestibulum rutrum lacus. Nulla aliquam auctor cursus. Nulla at lacus condimentum, viverra lacus eget, sollicitudin ex. Cras efficitur leo dui, sit amet rutrum tellus venenatis et. Sed in facilisis libero. Etiam ultricies, nulla ut venenatis tincidunt, tortor erat tristique ante, non aliquet massa arcu eget nisl. Etiam gravida erat ante, sit amet lobortis mauris commodo nec. Praesent vitae sodales quam. Vivamus condimentum porta suscipit. Donec posuere id felis ac scelerisque. Vestibulum lacinia et leo id lobortis. Sed vitae dolor nec ligula dapibus finibus vel eu libero. Nam tincidunt maximus elit, sit amet tincidunt lacus laoreet malesuada.\n\n" +
|
||||
"here we ";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(new ReadMorePlugin())
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read more plugin based on text length. It is easier to implement than lines (need to adjust
|
||||
* last line to include expand/collapse text).
|
||||
*/
|
||||
class ReadMorePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final int maxLength = 150;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final String labelMore = "Show more...";
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final String labelLess = "...Show less";
|
||||
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
// establish connections with all _dynamic_ content that your markdown supports,
|
||||
// like images, tables, latex, etc
|
||||
registry.require(ImagesPlugin.class);
|
||||
// registry.require(TablePlugin.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
final CharSequence text = textView.getText();
|
||||
if (text.length() < maxLength) {
|
||||
// everything is OK, no need to ellipsize)
|
||||
return;
|
||||
}
|
||||
|
||||
final int breakAt = breakTextAt(text, 0, maxLength);
|
||||
final CharSequence cs = createCollapsedString(text, 0, breakAt);
|
||||
textView.setText(cs);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NonNull
|
||||
private CharSequence createCollapsedString(@NonNull CharSequence text, int start, int end) {
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(text, start, end);
|
||||
|
||||
// NB! each table row is represented as a space character and new-line (so length=2) no
|
||||
// matter how many characters are inside table cells
|
||||
|
||||
// we can _clean_ this builder, for example remove all dynamic content (like images and tables,
|
||||
// but keep them in full/expanded version)
|
||||
//noinspection ConstantConditions
|
||||
if (true) {
|
||||
// it is an implementation detail but _mostly_ dynamic content is implemented as
|
||||
// ReplacementSpans
|
||||
final ReplacementSpan[] spans = builder.getSpans(0, builder.length(), ReplacementSpan.class);
|
||||
if (spans != null) {
|
||||
for (ReplacementSpan span : spans) {
|
||||
builder.removeSpan(span);
|
||||
}
|
||||
}
|
||||
|
||||
// NB! if there will be a table in _preview_ (collapsed) then each row will be represented as a
|
||||
// space and new-line
|
||||
trim(builder);
|
||||
}
|
||||
|
||||
final CharSequence fullText = createFullText(text, builder);
|
||||
|
||||
builder.append(' ');
|
||||
|
||||
final int length = builder.length();
|
||||
builder.append(labelMore);
|
||||
builder.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
((TextView) widget).setText(fullText);
|
||||
}
|
||||
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CharSequence createFullText(@NonNull CharSequence text, @NonNull CharSequence collapsedText) {
|
||||
// full/expanded text can also be different,
|
||||
// for example it can be kept as-is and have no `collapse` functionality (once expanded cannot collapse)
|
||||
// or can contain collapse feature
|
||||
final CharSequence fullText;
|
||||
//noinspection ConstantConditions
|
||||
if (true) {
|
||||
// for example let's allow collapsing
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||
builder.append(' ');
|
||||
|
||||
final int length = builder.length();
|
||||
builder.append(labelLess);
|
||||
builder.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
((TextView) widget).setText(collapsedText);
|
||||
}
|
||||
}, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
fullText = builder;
|
||||
} else {
|
||||
fullText = text;
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
|
||||
private static void trim(@NonNull SpannableStringBuilder builder) {
|
||||
|
||||
// NB! tables use `\u00a0` (non breaking space) which is not reported as white-space
|
||||
|
||||
char c;
|
||||
|
||||
for (int i = 0, length = builder.length(); i < length; i++) {
|
||||
c = builder.charAt(i);
|
||||
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||
if (i > 0) {
|
||||
builder.replace(0, i, "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = builder.length() - 1; i >= 0; i--) {
|
||||
c = builder.charAt(i);
|
||||
if (!Character.isWhitespace(c) && c != '\u00a0') {
|
||||
if (i < builder.length() - 1) {
|
||||
builder.replace(i, builder.length(), "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// depending on your locale these can be different
|
||||
// There is a BreakIterator in Android, but it is not reliable, still theoretically
|
||||
// it should work better than hand-written and hardcoded rules
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static int breakTextAt(@NonNull CharSequence text, int start, int max) {
|
||||
|
||||
int last = start;
|
||||
|
||||
// no need to check for _start_ (anyway will be ignored)
|
||||
for (int i = start + max - 1; i > start; i--) {
|
||||
final char c = text.charAt(i);
|
||||
if (Character.isWhitespace(c)
|
||||
|| c == '.'
|
||||
|| c == ','
|
||||
|| c == '!'
|
||||
|| c == '?') {
|
||||
// include this special character
|
||||
last = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (last <= start) {
|
||||
// when used in subSequence last index is exclusive,
|
||||
// so given max=150 would result in 0-149 subSequence
|
||||
return start + max;
|
||||
}
|
||||
|
||||
return last;
|
||||
}
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.app.readme.GithubImageDestinationProcessor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonRecyclerViewSample;
|
||||
import io.noties.markwon.app.utils.SampleUtilsKtKt;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.recycler.SimpleEntry;
|
||||
import io.noties.markwon.recycler.table.TableEntry;
|
||||
import io.noties.markwon.recycler.table.TableEntryPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200702101750",
|
||||
title = "RecyclerView",
|
||||
description = "Usage with `RecyclerView`",
|
||||
artifacts = {MarkwonArtifact.RECYCLER, MarkwonArtifact.RECYCLER_TABLE},
|
||||
tags = Tag.recyclerView
|
||||
)
|
||||
public class RecyclerSample extends MarkwonRecyclerViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = SampleUtilsKtKt.loadReadMe(context);
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(TableEntryPlugin.create(context))
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.imageDestinationProcessor(new GithubImageDestinationProcessor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> {
|
||||
// we actually won't be applying code spans here, as our custom view will
|
||||
// draw background and apply mono typeface
|
||||
//
|
||||
// NB the `trim` operation on literal (as code will have a new line at the end)
|
||||
final CharSequence code = visitor.configuration()
|
||||
.syntaxHighlight()
|
||||
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim());
|
||||
visitor.builder().append(code);
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_node)
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_node_code_block, R.id.text_view))
|
||||
.include(TableBlock.class, TableEntry.create(builder -> {
|
||||
builder
|
||||
.tableLayout(R.layout.adapter_node_table_block, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.view_table_entry_cell);
|
||||
}))
|
||||
.build();
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
adapter.setMarkdown(markwon, md);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
import io.noties.markwon.utils.ColorUtils;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200813145316",
|
||||
title = "Reddit spoiler",
|
||||
description = "An attempt to implement Reddit spoiler syntax `>! !<`",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.parsing
|
||||
)
|
||||
public class RedditSpoilerSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Reddit spolier\n\n" +
|
||||
"Hello >!ugly so **ugly** !<, how are you?\n\n" +
|
||||
">!a blockquote?!< should not be >!present!< yeah" +
|
||||
"";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new RedditSpoilerPlugin())
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class RedditSpoilerPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
private static final Pattern RE = Pattern.compile(">!.+?!<");
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
// replace all `>!` with `>!` so no blockquote would be parsed (when spoiler starts at new line)
|
||||
return markdown.replaceAll(">!", ">!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
|
||||
applySpoilerSpans((Spannable) markdown);
|
||||
}
|
||||
|
||||
private static void applySpoilerSpans(@NonNull Spannable spannable) {
|
||||
final String text = spannable.toString();
|
||||
final Matcher matcher = RE.matcher(text);
|
||||
|
||||
while (matcher.find()) {
|
||||
|
||||
final RedditSpoilerSpan spoilerSpan = new RedditSpoilerSpan();
|
||||
final ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
spoilerSpan.setRevealed(true);
|
||||
widget.postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
// no op
|
||||
}
|
||||
};
|
||||
|
||||
final int s = matcher.start();
|
||||
final int e = matcher.end();
|
||||
spannable.setSpan(spoilerSpan, s, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(clickableSpan, s, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
// we also can hide original syntax
|
||||
spannable.setSpan(new HideSpoilerSyntaxSpan(), s, s + 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(new HideSpoilerSyntaxSpan(), e - 2, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RedditSpoilerSpan extends CharacterStyle {
|
||||
|
||||
private boolean revealed;
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
if (!revealed) {
|
||||
// use the same text color
|
||||
tp.bgColor = Color.BLACK;
|
||||
tp.setColor(Color.BLACK);
|
||||
} else {
|
||||
// for example keep a bit of black background to remind that it is a spoiler
|
||||
tp.bgColor = ColorUtils.applyAlpha(Color.BLACK, 25);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRevealed(boolean revealed) {
|
||||
this.revealed = revealed;
|
||||
}
|
||||
}
|
||||
|
||||
// we also could make text size smaller (but then MetricAffectingSpan should be used)
|
||||
private static class HideSpoilerSyntaxSpan extends CharacterStyle {
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
// set transparent color
|
||||
tp.setColor(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.core.spans.EmphasisSpan;
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
import io.noties.markwon.simple.ext.SimpleExtPlugin;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630194335",
|
||||
title = "Delimiter processor simple-ext",
|
||||
description = "Custom delimiter processor implemented with a `SimpleExtPlugin`",
|
||||
artifacts = MarkwonArtifact.SIMPLE_EXT,
|
||||
tags = Tag.parsing
|
||||
)
|
||||
public class SimpleExtensionSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# SimpleExt\n" +
|
||||
"\n" +
|
||||
"+let's start with `+`, ??then we can use this, and finally @@this$$??+";
|
||||
|
||||
// NB! we cannot have multiple delimiter processor with the same character
|
||||
// (even if lengths are different)
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(SimpleExtPlugin.create(plugin -> {
|
||||
plugin
|
||||
.addExtension(1, '+', (configuration, props) -> new EmphasisSpan())
|
||||
.addExtension(2, '?', (configuration, props) -> new StrongEmphasisSpan())
|
||||
.addExtension(
|
||||
2,
|
||||
'@',
|
||||
'$',
|
||||
(configuration, props) -> new ForegroundColorSpan(Color.RED)
|
||||
);
|
||||
}))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629125040",
|
||||
title = "Soft break new line",
|
||||
description = "Add a new line for a markdown soft-break node",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.newLine, Tag.softBreak}
|
||||
)
|
||||
public class SoftBreakAddsNewLineSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"Hello there ->(line)\n(break)<- going on and on";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629124706",
|
||||
title = "Soft break adds space",
|
||||
description = "By default a soft break (`\n`) will " +
|
||||
"add a space character instead of new line",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.newLine, Tag.softBreak, Tag.defaults}
|
||||
)
|
||||
public class SoftBreakAddsSpace extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"Hello there ->(line)\n(break)<- going on and on";
|
||||
|
||||
// by default a soft break will add a space (instead of line break)
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package io.noties.markwon.app.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.BlockHandlerDef;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200813154415",
|
||||
title = "Thematic break bottom margin",
|
||||
description = "Do not add a new line after thematic break (with the `BlockHandler`)",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class ThematicBreakBottomMarginSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"# Thematic break and margin\n\n" +
|
||||
"So, what if....\n\n" +
|
||||
"---\n\n" +
|
||||
"And **now**";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.blockHandler(new BlockHandlerDef() {
|
||||
@Override
|
||||
public void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
// also can control block start
|
||||
super.blockStart(visitor, node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
|
||||
// thematic break won't have a new line
|
||||
// similarly you can control other blocks
|
||||
if (!(node instanceof ThematicBreak)) {
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.BuildConfig
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200627074017",
|
||||
title = "Markdown in Toast (with dynamic content)",
|
||||
description = "Display markdown in a `android.widget.Toast` with dynamic content (image)",
|
||||
artifacts = [MarkwonArtifact.CORE, MarkwonArtifact.IMAGE],
|
||||
tags = [Tag.toast, Tag.hack]
|
||||
)
|
||||
class ToastDynamicContentSample : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
val md = """
|
||||
# Head!
|
||||
|
||||

|
||||
|
||||
Do you see an image? ☝️
|
||||
""".trimIndent()
|
||||
|
||||
val markwon = Markwon.builder(context)
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.build()
|
||||
|
||||
val markdown = markwon.toMarkdown(md)
|
||||
|
||||
val toast = Toast.makeText(context, markdown, Toast.LENGTH_LONG)
|
||||
|
||||
// try to obtain textView
|
||||
val textView = toast.textView
|
||||
if (textView != null) {
|
||||
markwon.setParsedMarkdown(textView, markdown)
|
||||
}
|
||||
|
||||
// finally show toast (at this point, if we didn't find TextView it will still
|
||||
// present markdown, just without dynamic content (image))
|
||||
toast.show()
|
||||
}
|
||||
}
|
||||
|
||||
private val Toast.textView: TextView?
|
||||
get() {
|
||||
|
||||
fun find(view: View?): TextView? {
|
||||
|
||||
if (view is TextView) {
|
||||
return view
|
||||
}
|
||||
|
||||
if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
val textView = find(view.getChildAt(i))
|
||||
if (textView != null) {
|
||||
return textView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return find(view)
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package io.noties.markwon.app.samples
|
||||
|
||||
import android.widget.Toast
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200627072642",
|
||||
title = "Markdown in Toast",
|
||||
description = "Display _static_ markdown content in a `android.widget.Toast`",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.toast]
|
||||
)
|
||||
class ToastSample : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
// NB! only _static_ content is going to be displayed,
|
||||
// so, no images, tables or latex in a Toast
|
||||
val md = """
|
||||
# Heading is fine
|
||||
> Even quote if **fine**
|
||||
```
|
||||
finally code works;
|
||||
```
|
||||
_italic_ to put an end to it
|
||||
""".trimIndent()
|
||||
|
||||
val markwon = Markwon.create(context)
|
||||
|
||||
// render raw input to styled markdown
|
||||
val markdown = markwon.toMarkdown(md)
|
||||
|
||||
// Toast accepts CharSequence and allows styling via spans
|
||||
Toast.makeText(context, markdown, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package io.noties.markwon.app.samples.basics;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20210118154116",
|
||||
title = "One line text",
|
||||
description = "Single line text without markdown markup",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class OneLineNoMarkdownSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
textView.setBackgroundColor(0x40ff0000);
|
||||
|
||||
final String md = " Demo text ";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package io.noties.markwon.app.samples.basics
|
||||
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200626152255",
|
||||
title = "Simple",
|
||||
description = "The most primitive and simple way to apply markdown to a `TextView`",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.basics]
|
||||
)
|
||||
class Simple : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
// markdown input
|
||||
val md = """
|
||||
# Heading
|
||||
|
||||
> A quote
|
||||
|
||||
**bold _italic_ bold**
|
||||
""".trimIndent()
|
||||
|
||||
// markwon instance
|
||||
val markwon = Markwon.create(context)
|
||||
|
||||
// apply raw markdown (internally parsed and rendered)
|
||||
markwon.setMarkdown(textView, md)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package io.noties.markwon.app.samples.basics
|
||||
|
||||
import android.text.Spanned
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
|
||||
import io.noties.markwon.core.CorePlugin
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
|
||||
import io.noties.markwon.sample.annotations.Tag
|
||||
import org.commonmark.node.Node
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200626153426",
|
||||
title = "Simple with walk-through",
|
||||
description = "Walk-through for simple use case",
|
||||
artifacts = [MarkwonArtifact.CORE],
|
||||
tags = [Tag.basics]
|
||||
)
|
||||
class SimpleWalkthrough : MarkwonTextViewSample() {
|
||||
override fun render() {
|
||||
val md: String = """
|
||||
# Hello!
|
||||
|
||||
> a quote
|
||||
|
||||
```
|
||||
code block
|
||||
```
|
||||
""".trimIndent()
|
||||
|
||||
// create markwon instance via builder method
|
||||
val markwon: Markwon = Markwon.builder(context)
|
||||
// add required plugins
|
||||
// NB, there is no need to add CorePlugin as it is added automatically
|
||||
.usePlugin(CorePlugin.create())
|
||||
.build()
|
||||
|
||||
// parse markdown into commonmark representation
|
||||
val node: Node = markwon.parse(md)
|
||||
|
||||
// render commonmark node
|
||||
val markdown: Spanned = markwon.render(node)
|
||||
|
||||
// apply it to a TextView
|
||||
markwon.setParsedMarkdown(textView, markdown)
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||
import io.noties.markwon.editor.AbstractEditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629165136",
|
||||
title = "Additional edit span",
|
||||
description = "Additional _edit_ span (span that is present in " +
|
||||
"`EditText` along with punctuation",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor, Tag.span}
|
||||
)
|
||||
public class EditorAdditionalEditSpan extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// An additional span is used to highlight strong-emphasis
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
|
||||
.useEditHandler(new BoldEditHandler())
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
}
|
||||
|
||||
class BoldEditHandler extends AbstractEditHandler<StrongEmphasisSpan> {
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
// Here we define which span is _persisted_ in EditText, it is not removed
|
||||
// from EditText between text changes, but instead - reused (by changing
|
||||
// position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
|
||||
// here also, but I chose Bold to indicate that this span is not the same
|
||||
// as in off-screen rendered markdown
|
||||
builder.persistSpan(Bold.class, Bold::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull StrongEmphasisSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
// Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
|
||||
// because multiple inline markdown nodes can refer to the same text.
|
||||
// For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
|
||||
// and thus will have to manually find actual position in raw user input
|
||||
final MarkwonEditorUtils.Match match =
|
||||
MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
|
||||
if (match != null) {
|
||||
editable.setSpan(
|
||||
// we handle StrongEmphasisSpan and represent it with Bold in EditText
|
||||
// we still could use StrongEmphasisSpan, but it must be accessed
|
||||
// via persistedSpans
|
||||
persistedSpans.get(Bold.class),
|
||||
match.start(),
|
||||
match.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<StrongEmphasisSpan> markdownSpanType() {
|
||||
return StrongEmphasisSpan.class;
|
||||
}
|
||||
}
|
||||
|
||||
class Bold extends MetricAffectingSpan {
|
||||
public Bold() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
update(tp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint textPaint) {
|
||||
update(textPaint);
|
||||
}
|
||||
|
||||
private void update(@NonNull TextPaint paint) {
|
||||
paint.setFakeBoldText(true);
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629165347",
|
||||
title = "Additional plugin",
|
||||
description = "Additional plugin for editor",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER, MarkwonArtifact.EXT_STRIKETHROUGH},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorAdditionalPluginSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// As highlight works based on text-diff, everything that is present in input,
|
||||
// but missing in resulting markdown is considered to be punctuation, this is why
|
||||
// additional plugins do not need special handling
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.build();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629164627",
|
||||
title = "Custom punctuation span",
|
||||
description = "Custom span for punctuation in editor",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor, Tag.span}
|
||||
)
|
||||
public class EditorCustomPunctuationSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// Use own punctuation span
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
|
||||
.punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
}
|
||||
|
||||
class CustomPunctuationSpan extends ForegroundColorSpan {
|
||||
CustomPunctuationSpan() {
|
||||
super(0xFFFF0000); // RED
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630113954",
|
||||
title = "Heading edit handler",
|
||||
description = "Handling of heading node in editor",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorHeadingSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.useEditHandler(new HeadingEditHandler())
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import android.text.method.LinkMovementMethod;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.parser.InlineParserFactory;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.EntityInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629165920",
|
||||
title = "Multiple edit spans",
|
||||
description = "Additional multiple edit spans for editor",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorMultipleEditSpansSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
// for links to be clickable
|
||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||
// no inline images will be parsed
|
||||
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||
// no html tags will be parsed
|
||||
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||
// no entities will be parsed (aka `&` etc)
|
||||
.excludeInlineProcessor(EntityInlineProcessor.class)
|
||||
.build();
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
|
||||
// disable all commonmark-java blocks, only inlines will be parsed
|
||||
// builder.enabledBlockTypes(Collections.emptySet());
|
||||
|
||||
builder.inlineParserFactory(inlineParserFactory);
|
||||
}
|
||||
})
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.build();
|
||||
|
||||
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.useEditHandler(new EmphasisEditHandler())
|
||||
.useEditHandler(new StrongEmphasisEditHandler())
|
||||
.useEditHandler(new StrikethroughEditHandler())
|
||||
.useEditHandler(new CodeEditHandler())
|
||||
.useEditHandler(new BlockQuoteEditHandler())
|
||||
.useEditHandler(new LinkEditHandler(onClick))
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
package io.noties.markwon.app.samples.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;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629170348",
|
||||
title = "Editor new line continuation",
|
||||
description = "Sample of how new line character can be handled " +
|
||||
"in order to add a _continuation_, for example adding a new " +
|
||||
"bullet list item if current line starts with one",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorNewLineContinuationSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
final TextWatcher textWatcher = MarkdownNewLine
|
||||
.wrap(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
|
||||
editText.addTextChangedListener(textWatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629164422",
|
||||
title = "Editor with pre-render (async)",
|
||||
description = "Editor functionality with highlight " +
|
||||
"taking place in another thread",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorPreRenderSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// Process highlight in background thread
|
||||
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor,
|
||||
Executors.newCachedThreadPool(),
|
||||
editText));
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200629164227",
|
||||
title = "Simple editor",
|
||||
description = "Simple usage of editor with markdown highlight",
|
||||
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
|
||||
tags = {Tag.editor}
|
||||
)
|
||||
public class EditorSimpleSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
// Process highlight in-place (right after text has changed)
|
||||
|
||||
// obtain Markwon instance
|
||||
final Markwon markwon = Markwon.create(context);
|
||||
|
||||
// create editor
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
// set edit listener
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.noties.debug.Debug;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
|
||||
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
|
||||
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200908133515",
|
||||
title = "WYSIWG editor",
|
||||
description = "A possible direction to implement what-you-see-is-what-you-get editor",
|
||||
artifacts = MarkwonArtifact.EDITOR,
|
||||
tags = Tag.rendering
|
||||
)
|
||||
public class WYSIWYGEditorSample extends MarkwonEditTextSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
// when automatic line break is inserted and text is inside margin span (blockquote, list, etc)
|
||||
// be prepared to encounter selection bugs (selection would be drawn at the place as is no margin
|
||||
// span is present)
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.build();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.punctuationSpan(HidePunctuationSpan.class, new PersistedSpans.SpanFactory<HidePunctuationSpan>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public HidePunctuationSpan create() {
|
||||
return new HidePunctuationSpan();
|
||||
}
|
||||
})
|
||||
.useEditHandler(new EmphasisEditHandler())
|
||||
.useEditHandler(new StrongEmphasisEditHandler())
|
||||
.useEditHandler(new StrikethroughEditHandler())
|
||||
.useEditHandler(new CodeEditHandler())
|
||||
.useEditHandler(new BlockQuoteEditHandler())
|
||||
.useEditHandler(new LinkEditHandler(new LinkEditHandler.OnClick() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget, @NonNull String link) {
|
||||
Debug.e("clicked: %s", link);
|
||||
}
|
||||
}))
|
||||
.useEditHandler(new HeadingEditHandler())
|
||||
.build();
|
||||
|
||||
// for links to be clickable
|
||||
// NB! markwon MovementMethodPlugin cannot be used here as editor do not execute `beforeSetText`)
|
||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
|
||||
}
|
||||
|
||||
private static class HidePunctuationSpan extends ReplacementSpan {
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
|
||||
// last space (which is swallowed until next non-space character appears)
|
||||
// block quote
|
||||
// code tick
|
||||
|
||||
// Debug.i("text: '%s', %d-%d (%d)", text.subSequence(start, end), start, end, text.length());
|
||||
if (end == text.length()) {
|
||||
// TODO: find first non-space character (not just first one because commonmark allows
|
||||
// arbitrary (0-3) white spaces before content starts
|
||||
|
||||
// TODO: if all white space - render?
|
||||
final char c = text.charAt(start);
|
||||
if ('#' == c
|
||||
|| '>' == c
|
||||
|| '-' == c // TODO: not thematic break
|
||||
|| '+' == c
|
||||
// `*` is fine but only for a list
|
||||
|| isBulletList(text, c, start, end)
|
||||
|| Character.isDigit(c) // assuming ordered list (replacement should only happen for ordered lists)
|
||||
|| Character.isWhitespace(c)) {
|
||||
return (int) (paint.measureText(text, start, end) + 0.5F);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
|
||||
// will be called only when getSize is not 0 (and if it was once reported as 0...)
|
||||
if (end == text.length()) {
|
||||
|
||||
// if first non-space is `*` then check for is bullet
|
||||
// else `**` would be still rendered at the end of the emphasis
|
||||
if (text.charAt(start) == '*'
|
||||
&& !isBulletList(text, '*', start, end)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: inline code last tick received here, handle it (do not highlight)
|
||||
// why can't we have reported width in this method for supplied text?
|
||||
|
||||
// let's use color to make it distinct from the rest of the text for demonstration purposes
|
||||
paint.setColor(0xFFff0000);
|
||||
|
||||
canvas.drawText(text, start, end, x, y, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBulletList(@NonNull CharSequence text, char firstChar, int start, int end) {
|
||||
return '*' == firstChar
|
||||
&& ((end - start == 1) || (Character.isWhitespace(text.charAt(start + 1))));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.BlockQuoteSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull BlockQuoteSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
// todo: here we should actually find a proper ending of a block quote...
|
||||
editable.setSpan(
|
||||
persistedSpans.get(BlockQuoteSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<BlockQuoteSpan> markdownSpanType() {
|
||||
return BlockQuoteSpan.class;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.CodeSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class CodeEditHandler implements EditHandler<CodeSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull CodeSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
final MarkwonEditorUtils.Match match =
|
||||
MarkwonEditorUtils.findDelimited(input, spanStart, "`");
|
||||
if (match != null) {
|
||||
editable.setSpan(
|
||||
persistedSpans.get(CodeSpan.class),
|
||||
match.start(),
|
||||
match.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<CodeSpan> markdownSpanType() {
|
||||
return CodeSpan.class;
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.HeadingSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class HeadingEditHandler implements EditHandler<HeadingSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder
|
||||
.persistSpan(Head1.class, () -> new Head1(theme))
|
||||
.persistSpan(Head2.class, () -> new Head2(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull HeadingSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength
|
||||
) {
|
||||
final Class<?> type;
|
||||
switch (span.getLevel()) {
|
||||
case 1:
|
||||
type = Head1.class;
|
||||
break;
|
||||
case 2:
|
||||
type = Head2.class;
|
||||
break;
|
||||
default:
|
||||
type = null;
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
final int index = input.indexOf('\n', spanStart + spanTextLength);
|
||||
final int end = index < 0
|
||||
? input.length()
|
||||
: index;
|
||||
editable.setSpan(
|
||||
persistedSpans.get(type),
|
||||
spanStart,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<HeadingSpan> markdownSpanType() {
|
||||
return HeadingSpan.class;
|
||||
}
|
||||
|
||||
private static class Head1 extends HeadingSpan {
|
||||
Head1(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Head2 extends HeadingSpan {
|
||||
Head2(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 2);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.core.spans.LinkSpan;
|
||||
import io.noties.markwon.editor.AbstractEditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
|
||||
|
||||
public interface OnClick {
|
||||
void onClick(@NonNull View widget, @NonNull String link);
|
||||
}
|
||||
|
||||
private final OnClick onClick;
|
||||
|
||||
public LinkEditHandler(@NonNull OnClick onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull LinkSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
|
||||
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
|
||||
editLinkSpan.link = span.getLink();
|
||||
|
||||
// First first __letter__ to find link content (scheme start in URL, receiver in email address)
|
||||
// NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
|
||||
// display. For example, we _could_ also look for a digit, but:
|
||||
// * if phone number start with special symbol, we won't have it (`+`, `(`)
|
||||
// * it might interfere with an ordered-list
|
||||
int start = -1;
|
||||
|
||||
for (int i = spanStart, length = input.length(); i < length; i++) {
|
||||
if (Character.isLetter(input.charAt(i))) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (start > -1) {
|
||||
editable.setSpan(
|
||||
editLinkSpan,
|
||||
start,
|
||||
start + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<LinkSpan> markdownSpanType() {
|
||||
return LinkSpan.class;
|
||||
}
|
||||
|
||||
static class EditLinkSpan extends ClickableSpan {
|
||||
|
||||
private final OnClick onClick;
|
||||
|
||||
String link;
|
||||
|
||||
EditLinkSpan(@NonNull OnClick onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (link != null) {
|
||||
onClick.onClick(widget, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import io.noties.markwon.app.R
|
||||
import io.noties.markwon.app.sample.ui.MarkwonSample
|
||||
import io.noties.markwon.core.spans.EmphasisSpan
|
||||
import io.noties.markwon.core.spans.StrongEmphasisSpan
|
||||
import java.util.ArrayList
|
||||
|
||||
abstract class MarkwonEditTextSample : MarkwonSample() {
|
||||
|
||||
protected lateinit var context: Context
|
||||
protected lateinit var editText: EditText
|
||||
|
||||
override val layoutResId: Int
|
||||
get() = R.layout.sample_edit_text
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
context = view.context
|
||||
editText = view.findViewById(R.id.edit_text)
|
||||
initBottomBar(view)
|
||||
render()
|
||||
}
|
||||
|
||||
abstract fun render()
|
||||
|
||||
private fun initBottomBar(view: View) {
|
||||
// all except block-quote wraps if have selection, or inserts at current cursor position
|
||||
val bold: Button = view.findViewById(R.id.bold)
|
||||
val italic: Button = view.findViewById(R.id.italic)
|
||||
val strike: Button = view.findViewById(R.id.strike)
|
||||
val quote: Button = view.findViewById(R.id.quote)
|
||||
val code: Button = view.findViewById(R.id.code)
|
||||
|
||||
addSpan(bold, StrongEmphasisSpan())
|
||||
addSpan(italic, EmphasisSpan())
|
||||
addSpan(strike, StrikethroughSpan())
|
||||
|
||||
bold.setOnClickListener(InsertOrWrapClickListener(editText, "**"))
|
||||
italic.setOnClickListener(InsertOrWrapClickListener(editText, "_"))
|
||||
strike.setOnClickListener(InsertOrWrapClickListener(editText, "~~"))
|
||||
code.setOnClickListener(InsertOrWrapClickListener(editText, "`"))
|
||||
quote.setOnClickListener {
|
||||
val start = editText.selectionStart
|
||||
val end = editText.selectionEnd
|
||||
if (start < 0) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
if (start == end) {
|
||||
editText.text.insert(start, "> ")
|
||||
} else {
|
||||
// wrap the whole selected area in a quote
|
||||
val newLines: MutableList<Int> = ArrayList(3)
|
||||
newLines.add(start)
|
||||
val text = editText.text.subSequence(start, end).toString()
|
||||
var index = text.indexOf('\n')
|
||||
while (index != -1) {
|
||||
newLines.add(start + index + 1)
|
||||
index = text.indexOf('\n', index + 1)
|
||||
}
|
||||
for (i in newLines.indices.reversed()) {
|
||||
editText.text.insert(newLines[i], "> ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSpan(textView: TextView, vararg spans: Any) {
|
||||
val builder = SpannableStringBuilder(textView.text)
|
||||
val end = builder.length
|
||||
for (span in spans) {
|
||||
builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
textView.text = builder
|
||||
}
|
||||
|
||||
private class InsertOrWrapClickListener(
|
||||
private val editText: EditText,
|
||||
private val text: String
|
||||
) : View.OnClickListener {
|
||||
override fun onClick(v: View) {
|
||||
val start = editText.selectionStart
|
||||
val end = editText.selectionEnd
|
||||
if (start < 0) {
|
||||
return
|
||||
}
|
||||
if (start == end) {
|
||||
// insert at current position
|
||||
editText.text.insert(start, text)
|
||||
} else {
|
||||
editText.text.insert(end, text)
|
||||
editText.text.insert(start, text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package io.noties.markwon.app.samples.editor.shared;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.editor.AbstractEditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> {
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull StrikethroughSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
final MarkwonEditorUtils.Match match =
|
||||
MarkwonEditorUtils.findDelimited(input, spanStart, "~~");
|
||||
if (match != null) {
|
||||
editable.setSpan(
|
||||
persistedSpans.get(StrikethroughSpan.class),
|
||||
match.start(),
|
||||
match.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<StrikethroughSpan> markdownSpanType() {
|
||||
return StrikethroughSpan.class;
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import android.text.Layout;
|
||||
import android.text.style.AlignmentSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonConfiguration;
|
||||
import io.noties.markwon.RenderProps;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlTag;
|
||||
import io.noties.markwon.html.tag.SimpleTagHandler;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630114630",
|
||||
title = "Align HTML tag",
|
||||
description = "Implement custom HTML tag handling",
|
||||
artifacts = MarkwonArtifact.HTML,
|
||||
tags = {Tag.rendering, Tag.span, Tag.html}
|
||||
)
|
||||
public class HtmlAlignSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"<align center>We are centered</align>\n" +
|
||||
"\n" +
|
||||
"<align end>We are at the end</align>\n" +
|
||||
"\n" +
|
||||
"<align>We should be at the start</align>\n" +
|
||||
"\n";
|
||||
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
|
||||
.addHandler(new AlignTagHandler()));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class AlignTagHandler extends SimpleTagHandler {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(
|
||||
@NonNull MarkwonConfiguration configuration,
|
||||
@NonNull RenderProps renderProps,
|
||||
@NonNull HtmlTag tag) {
|
||||
|
||||
final Layout.Alignment alignment;
|
||||
|
||||
// html attribute without value, <align center></align>
|
||||
if (tag.attributes().containsKey("center")) {
|
||||
alignment = Layout.Alignment.ALIGN_CENTER;
|
||||
} else if (tag.attributes().containsKey("end")) {
|
||||
alignment = Layout.Alignment.ALIGN_OPPOSITE;
|
||||
} else {
|
||||
// empty value or any other will make regular alignment
|
||||
alignment = Layout.Alignment.ALIGN_NORMAL;
|
||||
}
|
||||
|
||||
return new AlignmentSpan.Standard(alignment);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> supportedTags() {
|
||||
return Collections.singleton("align");
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import android.text.Layout;
|
||||
import android.text.style.AlignmentSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import io.noties.debug.Debug;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlTag;
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import io.noties.markwon.html.TagHandler;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630120101",
|
||||
title = "Center HTML tag",
|
||||
description = "Handling of `center` HTML tag",
|
||||
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
|
||||
tags = {Tag.rendering, Tag.html}
|
||||
)
|
||||
public class HtmlCenterTagSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String html = "<html>\n" +
|
||||
"\n" +
|
||||
"<head></head>\n" +
|
||||
"\n" +
|
||||
"<body>\n" +
|
||||
" <p></p>\n" +
|
||||
" <h3>LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads</h3>\n" +
|
||||
" <p></p>\n" +
|
||||
" <h5>The upper tune was already certified Gold one month after its digital release</h5>\n" +
|
||||
" <p>According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the <span\n" +
|
||||
" style=\"color: #ff9900;\"><strong><a href=\"http://www.lxixsxa.com/\" target=\"_blank\"><span\n" +
|
||||
" style=\"color: #ff9900;\">LiSA</span></a></strong></span>'s 14th single songs,\n" +
|
||||
" <strong>\"ADAMAS\"</strong> (the first OP theme for the TV anime <a href=\"/sword-art-online\"\n" +
|
||||
" target=\"_blank\"><span style=\"color: #ff9900;\"><strong><em>Sword Art Online:\n" +
|
||||
" Alicization</em></strong></span></a>) has been certified <strong>Platinum</strong> for\n" +
|
||||
" surpassing 250,000 downloads.</p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p>As a double A-side single with <strong>\"Akai Wana (who loves it?),\"</strong> <strong>\"ADAMAS\"</strong> was\n" +
|
||||
" released from SACRA Music in Japan on December 12, 2018. Its CD single ranked second in Oricon's weekly single\n" +
|
||||
" chart by selling 35,000 copies in its first week. Meanwhile, the song was released digitally two months prior to\n" +
|
||||
" its CD release, October 8, then reached Gold (100,000 downloads) in the following month.</p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <center>\n" +
|
||||
" <p><strong>\"ADAMAS\"</strong> MV YouTube EDIT ver.:</p>\n" +
|
||||
" <p><iframe src=\"https://www.youtube.com/embed/UeEIl4JlE-g\" frameborder=\"0\" width=\"640\" height=\"360\"></iframe>\n" +
|
||||
" </p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p>Standard edition CD jacket:</p>\n" +
|
||||
" <p><img src=\"https://img1.ak.crunchyroll.com/i/spire2/d7b1d6bc7563224388ef5ffc04a967581589950464_full.jpg\"\n" +
|
||||
" alt=\"\" width=\"640\" height=\"635\"></p>\n" +
|
||||
" </center>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <hr>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p>Source: RIAJ press release</p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p><em>©SACRA MUSIC</em></p>\n" +
|
||||
" <p> </p>\n" +
|
||||
" <p style=\"text-align: center;\"><a href=\"https://got.cr/PremiumTrial-NewsBanner4\"><em><img\n" +
|
||||
" src=\"https://img1.ak.crunchyroll.com/i/spire4/78f5441d927cf160a93e037b567c2b1f1559091520_full.png\"\n" +
|
||||
" alt=\"\" width=\"640\" height=\"43\"></em></a></p>\n" +
|
||||
"</body>\n" +
|
||||
"\n" +
|
||||
"</html>";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(plugin ->
|
||||
plugin.addHandler(new CenterTagHandler())))
|
||||
.usePlugin(new IFrameHtmlPlugin())
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, html);
|
||||
}
|
||||
}
|
||||
|
||||
class CenterTagHandler extends TagHandler {
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull MarkwonVisitor visitor, @NonNull MarkwonHtmlRenderer renderer, @NonNull HtmlTag tag) {
|
||||
Debug.e("center, isBlock: %s", tag.isBlock());
|
||||
if (tag.isBlock()) {
|
||||
visitChildren(visitor, renderer, tag.getAsBlock());
|
||||
}
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> supportedTags() {
|
||||
return Collections.singleton("center");
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user