Compare commits
325 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2ea148c30a | ||
![]() |
205ae7b47a | ||
![]() |
c54f1154b6 | ||
![]() |
6f8b8e71f5 | ||
![]() |
50b3168491 | ||
![]() |
b8cb4d1e82 | ||
![]() |
bd3408beb3 | ||
![]() |
646e708c82 | ||
![]() |
3069432bc2 | ||
![]() |
1bc45e0195 | ||
![]() |
3e1db2abbe | ||
![]() |
79cd43bc9c | ||
![]() |
14a8746599 | ||
![]() |
48518de658 | ||
![]() |
910bf311da | ||
![]() |
05cdf2c400 | ||
![]() |
eb3a986c48 | ||
![]() |
273ecdd7cd | ||
![]() |
923b00b6d0 | ||
![]() |
89ec2a063f | ||
![]() |
02aa16a6f2 | ||
![]() |
82cb42813a | ||
![]() |
08a8cece61 | ||
![]() |
ac578906d3 | ||
![]() |
2296d0032d | ||
![]() |
bde188ecd3 | ||
![]() |
62cb8d1f4d | ||
![]() |
c4cb4420b1 | ||
![]() |
132756af4e | ||
![]() |
89e89c766e | ||
![]() |
537d8e715d | ||
![]() |
7002dbfb8d | ||
![]() |
63ed271133 | ||
![]() |
dcd9d428ee | ||
![]() |
905c9fa159 | ||
![]() |
f8eaac6197 | ||
![]() |
4c3fba8929 | ||
![]() |
2356dd4618 | ||
![]() |
8cea2e0202 | ||
![]() |
d41137f6cf | ||
![]() |
fa286287c8 | ||
![]() |
bc7890c603 | ||
![]() |
5162c13bf7 | ||
![]() |
949962ee0b | ||
![]() |
12be227620 | ||
![]() |
65309e684c | ||
![]() |
aa2ff41831 | ||
![]() |
602891566d | ||
![]() |
854fa744c7 | ||
![]() |
06413aaf36 | ||
![]() |
55e640af9c | ||
![]() |
d26da7c1a0 | ||
![]() |
df89c06f22 | ||
![]() |
be667e3b45 | ||
![]() |
961ff32c9a | ||
![]() |
2adfeeba51 | ||
![]() |
0a365840e1 | ||
![]() |
e4bfe9f790 | ||
![]() |
28c2a7047d | ||
![]() |
ac05b07123 | ||
![]() |
72f6174db9 | ||
![]() |
76fccb15b6 | ||
![]() |
eab8a80177 | ||
![]() |
782353cd65 | ||
![]() |
01143fd5a5 | ||
![]() |
0227615cb2 | ||
![]() |
3a25a1b911 | ||
![]() |
6953b2cfde | ||
![]() |
3f9ac3d5f4 | ||
![]() |
f0808e6997 | ||
![]() |
0a6c5df0f2 | ||
![]() |
aafbd8585c | ||
![]() |
6ec7132273 | ||
![]() |
74c632e8db | ||
![]() |
04b1d695ab | ||
![]() |
2318dc02ff | ||
![]() |
2d6c3afed2 | ||
![]() |
11bec3d37e | ||
![]() |
a330b57be8 | ||
![]() |
ae01404b14 | ||
![]() |
7d76cb8eac | ||
![]() |
df23339dba | ||
![]() |
c96ea690f6 | ||
![]() |
086494bd97 | ||
![]() |
78ec885294 | ||
![]() |
9dce1c8533 | ||
![]() |
12a73d982c | ||
![]() |
0b1544feae | ||
![]() |
bc58790704 | ||
![]() |
25740d7389 | ||
![]() |
377d8c6deb | ||
![]() |
dc139319a0 | ||
![]() |
bea6d6aeec | ||
![]() |
1a90f1e609 | ||
![]() |
186390805a | ||
![]() |
45d205ba8c | ||
![]() |
860d70d6d1 | ||
![]() |
66f77f35fe | ||
![]() |
7e8ed3ea0b | ||
![]() |
2076b83675 | ||
![]() |
ba85ea0e98 | ||
![]() |
cd7aae7c9e | ||
![]() |
05b78e936b | ||
![]() |
07310127be | ||
![]() |
1ba2da3757 | ||
![]() |
9347208746 | ||
![]() |
19b6763a23 | ||
![]() |
bdb47c73f2 | ||
![]() |
7f6d85e1fb | ||
![]() |
dfa21f68e2 | ||
![]() |
94aef9934e | ||
![]() |
b1a0f3b739 | ||
![]() |
8e332712fe | ||
![]() |
03770cfe2d | ||
![]() |
6103ec0574 | ||
![]() |
3ab015175b | ||
![]() |
c2c59041f5 | ||
![]() |
d42ae41409 | ||
![]() |
c450765ab4 | ||
![]() |
477078470b | ||
![]() |
171b6d40a0 | ||
![]() |
21152f368f | ||
![]() |
e59911cfde | ||
![]() |
e386880978 | ||
![]() |
7d49afaac7 | ||
![]() |
c661eb486d | ||
![]() |
851172a785 | ||
![]() |
5451a2722e | ||
![]() |
a135e07f16 | ||
![]() |
ab83dad618 | ||
![]() |
bc38768539 | ||
![]() |
b497f872e5 | ||
![]() |
3006f8d486 | ||
![]() |
fc73e08bea | ||
![]() |
a26c13c93a | ||
![]() |
0f968662a8 | ||
![]() |
0ae3a3d66e | ||
![]() |
b48b0889da | ||
![]() |
ddfa9c98b8 | ||
![]() |
33f0dcb841 | ||
![]() |
a6bd102e82 | ||
![]() |
f47124a2ac | ||
![]() |
3ef41b1b81 | ||
![]() |
3ee62a724c | ||
![]() |
54e5b27d59 | ||
![]() |
392333806a | ||
![]() |
abeb5044af | ||
![]() |
c4a2bb94e2 | ||
![]() |
cca24090c1 | ||
![]() |
d1479dba8d | ||
![]() |
9c469be176 | ||
![]() |
8e3d898b40 | ||
![]() |
924abae784 | ||
![]() |
fe3d567619 | ||
![]() |
b5a30a55b3 | ||
![]() |
815f733892 | ||
![]() |
c425773c84 | ||
![]() |
0b813e43f7 | ||
![]() |
69c2d1255c | ||
![]() |
c90675d67b | ||
![]() |
86d34cef6f | ||
![]() |
d31940a290 | ||
![]() |
20d2bebd2b | ||
![]() |
a94090a746 | ||
![]() |
12c7c8909b | ||
![]() |
db660d2a40 | ||
![]() |
9cefe57532 | ||
![]() |
8c04748597 | ||
![]() |
b047f8131b | ||
![]() |
5c3763a9a1 | ||
![]() |
a1f12641c3 | ||
![]() |
c98f456744 | ||
![]() |
fa83b05724 | ||
![]() |
c8dfd9800b | ||
![]() |
3ac21a7ab3 | ||
![]() |
3068cb6987 | ||
![]() |
f887cb132b | ||
![]() |
d7f52607ab | ||
![]() |
823c26448a | ||
![]() |
9532d32e8d | ||
![]() |
cc35c35581 | ||
![]() |
8da8a37178 | ||
![]() |
f61e0b7b20 | ||
![]() |
8d3f0e908d | ||
![]() |
047ff864f1 | ||
![]() |
39370707ee | ||
![]() |
1c08e3f240 | ||
![]() |
74682ae605 | ||
![]() |
a80ff09e15 | ||
![]() |
c7494a9225 | ||
![]() |
8d483fe49d | ||
![]() |
7af0ead3a3 | ||
![]() |
976dfa3162 | ||
![]() |
f7f8f6d1ee | ||
![]() |
d78b278b86 | ||
![]() |
33701a179f | ||
![]() |
c939c0fa5c | ||
![]() |
34f71f13d2 | ||
![]() |
a298016ac2 | ||
![]() |
ef97b0bc25 | ||
![]() |
d8b3d02368 | ||
![]() |
130a60265b | ||
![]() |
6d9121b54d | ||
![]() |
b55b1f0dcc | ||
![]() |
2e7d0aa46b | ||
![]() |
17756a1137 | ||
![]() |
70113b7b16 | ||
![]() |
7c0d86e0a6 | ||
![]() |
b844f4db6c | ||
![]() |
39177057af | ||
![]() |
00d60e2399 | ||
![]() |
efa3473cfb | ||
![]() |
6c4ffd1778 | ||
![]() |
d2e4730179 | ||
![]() |
36089699d4 | ||
![]() |
3c23140ac0 | ||
![]() |
d6fe069728 | ||
![]() |
6b9e79ce5e | ||
![]() |
d1d0876d6d | ||
![]() |
de04e5069b | ||
![]() |
136c6bd51b | ||
![]() |
f2f5026694 | ||
![]() |
93a14b4731 | ||
![]() |
fb4e2c089f | ||
![]() |
4fa1ac718f | ||
![]() |
bf61d8c627 | ||
![]() |
5fe1e07b39 | ||
![]() |
0c305fa0ba | ||
![]() |
1983b1b46e | ||
![]() |
a1182e209a | ||
![]() |
e95defb67c | ||
![]() |
75c3aa8102 | ||
![]() |
c6fd779f33 | ||
![]() |
a6201b1b35 | ||
![]() |
681a7f68d7 | ||
![]() |
5e3ace0c29 | ||
![]() |
4b2d38b92f | ||
![]() |
0fabf7daff | ||
![]() |
2488c1047b | ||
![]() |
bd53c014a1 | ||
![]() |
f1e750b305 | ||
![]() |
8768e8a33c | ||
![]() |
870733ee2a | ||
![]() |
ba22ca88e2 | ||
![]() |
bc3a7b75d2 | ||
![]() |
003b5e90b4 | ||
![]() |
c9e1bb0965 | ||
![]() |
b22a840dbe | ||
![]() |
204b803245 | ||
![]() |
caddddc710 | ||
![]() |
f9f8d36c02 | ||
![]() |
883f5967de | ||
![]() |
9d61454858 | ||
![]() |
27d835846e | ||
![]() |
aee6e49b1f | ||
![]() |
4348555b75 | ||
![]() |
529e9a88ca | ||
![]() |
5cbdbe1759 | ||
![]() |
0a7356ecf8 | ||
![]() |
008faa6f49 | ||
![]() |
02e83a62db | ||
![]() |
4406a5faaf | ||
![]() |
3c77448682 | ||
![]() |
1ab1b8b87a | ||
![]() |
1b7fbfb77f | ||
![]() |
fa01a50ae8 | ||
![]() |
b3e7749c7a | ||
![]() |
6a06e56c1c | ||
![]() |
6c8f1c04bb | ||
![]() |
3fe514aeea | ||
![]() |
a2d35a1553 | ||
![]() |
b6fa66914f | ||
![]() |
2a43797023 | ||
![]() |
620da87694 | ||
![]() |
54335dce6e | ||
![]() |
7e12552060 | ||
![]() |
822f16510e | ||
![]() |
9d09bd4236 | ||
![]() |
879dde1382 | ||
![]() |
8daa59709b | ||
![]() |
85f201702e | ||
![]() |
c68aeabcf9 | ||
![]() |
24151dff7d | ||
![]() |
aa64aa7020 | ||
![]() |
1407ae0cf8 | ||
![]() |
f99952ec01 | ||
![]() |
b7606c7ee7 | ||
![]() |
386254f962 | ||
![]() |
d65a1809ca | ||
![]() |
18b1d5b0bb | ||
![]() |
06c2763ac6 | ||
![]() |
eca93dd27c | ||
![]() |
a082e9ed44 | ||
![]() |
fdb0f76e13 | ||
![]() |
ffb5848c3c | ||
![]() |
213f5cf281 | ||
![]() |
4fec46fb4d | ||
![]() |
d630039196 | ||
![]() |
6ed641fa47 | ||
![]() |
5c78f1d515 | ||
![]() |
a3ebae3b87 | ||
![]() |
8944f39592 | ||
![]() |
8b0edc32c3 | ||
![]() |
14591508b5 | ||
![]() |
512814ac4c | ||
![]() |
9aade9d6ca | ||
![]() |
a2a5857f06 | ||
![]() |
173425ed53 | ||
![]() |
ab4c80dca5 | ||
![]() |
13536302cc | ||
![]() |
79b99abb24 | ||
![]() |
4b918bf094 | ||
![]() |
dba07e3f3c | ||
![]() |
df0177af95 | ||
![]() |
f3476ca5cc | ||
![]() |
6bf04e38ad | ||
![]() |
cedb3971a0 | ||
![]() |
2e35ef53bb | ||
![]() |
0b0d3c4753 | ||
![]() |
19091b5675 | ||
![]() |
e35d3ad044 | ||
![]() |
64af306e53 | ||
![]() |
5bf21bc940 | ||
![]() |
661f72da0f | ||
![]() |
453880bd62 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
#repo: https://github.com/noties/Markwon
|
||||
|
||||
custom: ["https://paypal.me/dimitryivanov"]
|
18
.github/workflows/build.yml
vendored
Normal file
18
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
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
Normal file
17
.github/workflows/pull-request.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
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
|
21
.travis.yml
21
.travis.yml
@ -1,21 +0,0 @@
|
||||
# https://docs.travis-ci.com/user/languages/android/
|
||||
language: android
|
||||
jdk: openjdk8
|
||||
sudo: false
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- tools
|
||||
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
|
||||
branches:
|
||||
except:
|
||||
- gh-pages
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
475
CHANGELOG.md
Normal file
475
CHANGELOG.md
Normal file
@ -0,0 +1,475 @@
|
||||
# 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
|
41
README.md
41
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
# Markwon
|
||||
|
||||
[](https://travis-ci.org/noties/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]
|
||||
@ -19,29 +19,33 @@ 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
|
||||
|
||||
<sup>*</sup>*This file is displayed by default in the [sample-apk] (`markwon-sample-{latest-version}-debug.apk`) application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark*
|
||||
|
||||
[sample-apk]: https://github.com/noties/Markwon/releases
|
||||
|
||||
## Installation
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
```groovy
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
```kotlin
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
```
|
||||
|
||||
Full list of available artifacts is present in the [install section](https://noties.github.io/Markwon/docs/v3/install.html)
|
||||
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) branch
|
||||
> 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:
|
||||
@ -80,7 +84,7 @@ Please visit [documentation] web-site for further reference.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Taken with default configuration (except for image loading):
|
||||
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>
|
||||
@ -97,14 +101,10 @@ Please visit [documentation] web-site for reference
|
||||
|
||||
[documentation]: https://noties.github.io/Markwon
|
||||
|
||||
---
|
||||
|
||||
## Applications using Markwon
|
||||
|
||||
* [Partiko](https://partiko.app)
|
||||
* [FairNote Notepad](https://play.google.com/store/apps/details?id=com.rgiskard.fairnote)
|
||||
* [Boxcryptor](https://www.boxcryptor.com)
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
@ -217,7 +217,6 @@ public static Parser createParser() {
|
||||
android:layout_margin="16dip"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:textSize="16sp"
|
||||
tools:context="ru.noties.markwon.MainActivity"
|
||||
tools:text="yo\nman" />
|
||||
|
||||
</ScrollView>
|
||||
@ -296,7 +295,7 @@ Underscores (`_`)
|
||||
## License
|
||||
|
||||
```
|
||||
Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru)
|
||||
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.
|
||||
|
75
app-sample/README.md
Normal file
75
app-sample/README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# 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`
|
166
app-sample/build.gradle
Normal file
166
app-sample/build.gradle
Normal file
@ -0,0 +1,166 @@
|
||||
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']
|
||||
}
|
||||
}
|
24
app-sample/deploy.sh
Executable file
24
app-sample/deploy.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/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 -
|
BIN
app-sample/keystore.jks
Normal file
BIN
app-sample/keystore.jks
Normal file
Binary file not shown.
1477
app-sample/samples.json
Normal file
1477
app-sample/samples.json
Normal file
File diff suppressed because it is too large
Load Diff
166
app-sample/src/debug/res/layout/flowlayout_preview.xml
Normal file
166
app-sample/src/debug/res/layout/flowlayout_preview.xml
Normal file
@ -0,0 +1,166 @@
|
||||
<?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>
|
78
app-sample/src/main/AndroidManifest.xml
Normal file
78
app-sample/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?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
app-sample/src/main/assets/samples
Symbolic link
1
app-sample/src/main/assets/samples
Symbolic link
@ -0,0 +1 @@
|
||||
../../main/java/io/noties/markwon/app/samples/
|
1
app-sample/src/main/assets/samples.json
Symbolic link
1
app-sample/src/main/assets/samples.json
Symbolic link
@ -0,0 +1 @@
|
||||
../../../samples.json
|
58
app-sample/src/main/java/io/noties/markwon/app/App.kt
Normal file
58
app-sample/src/main/java/io/noties/markwon/app/App.kt
Normal file
@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
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})"
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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()
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
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
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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()
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,462 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
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) }
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
[{*.java, *.kt}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,436 @@
|
||||
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 {
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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}"
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
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))));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
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");
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.noties.debug.Debug;
|
||||
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.CssInlineStyleParser;
|
||||
import io.noties.markwon.html.CssProperty;
|
||||
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 = "20210118155530",
|
||||
title = "CSS attributes in HTML",
|
||||
description = "Parse CSS attributes of HTML tags with `CssInlineStyleParser`",
|
||||
artifacts = MarkwonArtifact.HTML,
|
||||
tags = Tag.html
|
||||
)
|
||||
public class HtmlCssStyleParserSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
|
||||
final String md = "# CSS\n\n" +
|
||||
"<span style=\"background-color: #ff0000;\">this has red background</span> and then\n\n" +
|
||||
"this <span style=\"color: #00ff00;\">is green</span>";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(plugin -> plugin.addHandler(new SpanTagHandler())))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
|
||||
private static class SpanTagHandler extends SimpleTagHandler {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
|
||||
final String style = tag.attributes().get("style");
|
||||
if (TextUtils.isEmpty(style)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int color = 0;
|
||||
int backgroundColor = 0;
|
||||
|
||||
for (CssProperty property : CssInlineStyleParser.create().parse(style)) {
|
||||
switch (property.key()) {
|
||||
|
||||
case "color":
|
||||
color = Color.parseColor(property.value());
|
||||
break;
|
||||
|
||||
case "background-color":
|
||||
backgroundColor = Color.parseColor(property.value());
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.i("unexpected CSS property: %s", property);
|
||||
}
|
||||
}
|
||||
|
||||
final List<Object> spans = new ArrayList<>(3);
|
||||
|
||||
if (color != 0) {
|
||||
spans.add(new ForegroundColorSpan(color));
|
||||
}
|
||||
if (backgroundColor != 0) {
|
||||
spans.add(new BackgroundColorSpan(backgroundColor));
|
||||
}
|
||||
|
||||
return spans.toArray();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> supportedTags() {
|
||||
return Collections.singleton("span");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,428 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.app.BuildConfig;
|
||||
import io.noties.markwon.app.R;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonSample;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlTag;
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import io.noties.markwon.html.TagHandler;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
import io.noties.markwon.utils.LeadingMarginUtils;
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630120752",
|
||||
title = "Details HTML tag",
|
||||
description = "Handling of `details` HTML tag",
|
||||
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
|
||||
tags = {Tag.image, Tag.rendering, Tag.html}
|
||||
)
|
||||
public class HtmlDetailsSample extends MarkwonSample {
|
||||
|
||||
private Context context;
|
||||
private ViewGroup content;
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.sample_html_details;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NotNull View view) {
|
||||
context = view.getContext();
|
||||
content = view.findViewById(R.id.content);
|
||||
render();
|
||||
}
|
||||
|
||||
private void render() {
|
||||
final String md = "# Hello\n\n<details>\n" +
|
||||
" <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" +
|
||||
" <p>\n\n" +
|
||||
"<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" +
|
||||
"## *formatted* **heading** with [a](link)\n" +
|
||||
"```java\n" +
|
||||
"code block\n" +
|
||||
"```\n" +
|
||||
"\n" +
|
||||
" <details>\n" +
|
||||
" <summary><small>nested</small> stuff</summary><p>\n" +
|
||||
"<!-- alternative placement of p shown above -->\n" +
|
||||
"\n" +
|
||||
"* list\n" +
|
||||
"* with\n" +
|
||||
"\n\n" +
|
||||
"\n\n" +
|
||||
"" +
|
||||
" 1. nested\n" +
|
||||
" 1. items\n" +
|
||||
"\n" +
|
||||
" ```java\n" +
|
||||
" // including code\n" +
|
||||
" ```\n" +
|
||||
" 1. blocks\n" +
|
||||
"\n" +
|
||||
"<details><summary>The 3rd!</summary>\n\n" +
|
||||
"**bold** _em_\n</details>" +
|
||||
" </p></details>\n" +
|
||||
"</p></details>\n\n" +
|
||||
"and **this** *is* how...";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(plugin ->
|
||||
plugin.addHandler(new DetailsTagHandler())))
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.build();
|
||||
|
||||
final Spanned spanned = markwon.toMarkdown(md);
|
||||
final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class);
|
||||
|
||||
// if we have no details, proceed as usual (single text-view)
|
||||
if (spans == null || spans.length == 0) {
|
||||
// no details
|
||||
final TextView textView = appendTextView();
|
||||
markwon.setParsedMarkdown(textView, spanned);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<DetailsElement> list = new ArrayList<>();
|
||||
|
||||
for (DetailsParsingSpan span : spans) {
|
||||
final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list);
|
||||
if (e != null) {
|
||||
list.add(e);
|
||||
}
|
||||
}
|
||||
|
||||
for (DetailsElement element : list) {
|
||||
initDetails(element, spanned);
|
||||
}
|
||||
|
||||
sort(list);
|
||||
|
||||
|
||||
TextView textView;
|
||||
int start = 0;
|
||||
|
||||
for (DetailsElement element : list) {
|
||||
|
||||
if (element.start != start) {
|
||||
// subSequence and add new TextView
|
||||
textView = appendTextView();
|
||||
textView.setText(subSequenceTrimmed(spanned, start, element.start));
|
||||
}
|
||||
|
||||
// now add details TextView
|
||||
textView = appendTextView();
|
||||
initDetailsTextView(markwon, textView, element);
|
||||
|
||||
start = element.end;
|
||||
}
|
||||
|
||||
if (start != spanned.length()) {
|
||||
// another textView with rest content
|
||||
textView = appendTextView();
|
||||
textView.setText(subSequenceTrimmed(spanned, start, spanned.length()));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView appendTextView() {
|
||||
final View view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.view_html_details_text_view, content, false);
|
||||
final TextView textView = view.findViewById(R.id.text_view);
|
||||
content.addView(view);
|
||||
return textView;
|
||||
}
|
||||
|
||||
private void initDetailsTextView(
|
||||
@NonNull Markwon markwon,
|
||||
@NonNull TextView textView,
|
||||
@NonNull DetailsElement element) {
|
||||
|
||||
// minor optimization
|
||||
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
|
||||
|
||||
// so, each element with children is a details tag
|
||||
// there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans
|
||||
// final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
final SpannableBuilder builder = new SpannableBuilder();
|
||||
append(builder, markwon, textView, element, element);
|
||||
markwon.setParsedMarkdown(textView, builder.spannableStringBuilder());
|
||||
}
|
||||
|
||||
private void append(
|
||||
@NonNull SpannableBuilder builder,
|
||||
@NonNull Markwon markwon,
|
||||
@NonNull TextView textView,
|
||||
@NonNull DetailsElement root,
|
||||
@NonNull DetailsElement element) {
|
||||
if (!element.children.isEmpty()) {
|
||||
|
||||
final int start = builder.length();
|
||||
|
||||
// builder.append(element.content);
|
||||
builder.append(subSequenceTrimmed(element.content, 0, element.content.length()));
|
||||
|
||||
builder.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
element.expanded = !element.expanded;
|
||||
|
||||
initDetailsTextView(markwon, textView, root);
|
||||
}
|
||||
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (element.expanded) {
|
||||
for (DetailsElement child : element.children) {
|
||||
append(builder, markwon, textView, root, child);
|
||||
}
|
||||
}
|
||||
|
||||
builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start);
|
||||
|
||||
} else {
|
||||
builder.append(element.content);
|
||||
}
|
||||
}
|
||||
|
||||
// if null -> remove from where it was processed,
|
||||
// else replace from where it was processed with a new one (can become expandable)
|
||||
@Nullable
|
||||
private static DetailsElement settle(
|
||||
@NonNull DetailsElement element,
|
||||
@NonNull List<? extends DetailsElement> elements) {
|
||||
for (DetailsElement e : elements) {
|
||||
if (element.start > e.start && element.end <= e.end) {
|
||||
final DetailsElement settled = settle(element, e.children);
|
||||
if (settled != null) {
|
||||
|
||||
// the thing is we must balance children if done like this
|
||||
// let's just create a tree actually, so we are easier to modify
|
||||
final Iterator<DetailsElement> iterator = e.children.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element));
|
||||
if (balanced == null) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// add to our children
|
||||
e.children.add(element);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) {
|
||||
int end = element.end;
|
||||
for (int i = element.children.size() - 1; i >= 0; i--) {
|
||||
final DetailsElement child = element.children.get(i);
|
||||
if (child.end < end) {
|
||||
element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end)));
|
||||
}
|
||||
initDetails(child, spanned);
|
||||
end = child.start;
|
||||
}
|
||||
|
||||
final int start = (element.start + element.content.length());
|
||||
if (end != start) {
|
||||
element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void sort(@NonNull List<DetailsElement> elements) {
|
||||
Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start));
|
||||
for (DetailsElement element : elements) {
|
||||
sort(element.children);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) {
|
||||
|
||||
while (start < end) {
|
||||
|
||||
final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start));
|
||||
final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1));
|
||||
|
||||
if (!isStartEmpty && !isEndEmpty) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isStartEmpty) {
|
||||
start += 1;
|
||||
}
|
||||
if (isEndEmpty) {
|
||||
end -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return cs.subSequence(start, end);
|
||||
}
|
||||
|
||||
private static class DetailsElement {
|
||||
|
||||
final int start;
|
||||
final int end;
|
||||
final CharSequence content;
|
||||
final List<DetailsElement> children = new ArrayList<>(0);
|
||||
|
||||
boolean expanded;
|
||||
|
||||
DetailsElement(int start, int end, @NonNull CharSequence content) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "DetailsElement{" +
|
||||
"start=" + start +
|
||||
", end=" + end +
|
||||
", content=" + toStringContent(content) +
|
||||
", children=" + children +
|
||||
", expanded=" + expanded +
|
||||
'}';
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String toStringContent(@NonNull CharSequence cs) {
|
||||
return cs.toString().replaceAll("\n", "\\n");
|
||||
}
|
||||
}
|
||||
|
||||
private static class DetailsTagHandler extends TagHandler {
|
||||
|
||||
@Override
|
||||
public void handle(
|
||||
@NonNull MarkwonVisitor visitor,
|
||||
@NonNull MarkwonHtmlRenderer renderer,
|
||||
@NonNull HtmlTag tag) {
|
||||
|
||||
int summaryEnd = -1;
|
||||
|
||||
for (HtmlTag child : tag.getAsBlock().children()) {
|
||||
|
||||
if (!child.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("summary".equals(child.name())) {
|
||||
summaryEnd = child.end();
|
||||
}
|
||||
|
||||
final TagHandler tagHandler = renderer.tagHandler(child.name());
|
||||
if (tagHandler != null) {
|
||||
tagHandler.handle(visitor, renderer, child);
|
||||
} else if (child.isBlock()) {
|
||||
visitChildren(visitor, renderer, child.getAsBlock());
|
||||
}
|
||||
}
|
||||
|
||||
if (summaryEnd > -1) {
|
||||
visitor.builder().setSpan(new DetailsParsingSpan(
|
||||
subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd)
|
||||
), tag.start(), tag.end());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> supportedTags() {
|
||||
return Collections.singleton("details");
|
||||
}
|
||||
}
|
||||
|
||||
private static class DetailsParsingSpan {
|
||||
|
||||
final CharSequence summary;
|
||||
|
||||
DetailsParsingSpan(@NonNull CharSequence summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DetailsSpan implements LeadingMarginSpan {
|
||||
|
||||
private final DetailsElement element;
|
||||
private final int blockMargin;
|
||||
private final int blockQuoteWidth;
|
||||
|
||||
private final Rect rect = new Rect();
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) {
|
||||
this.element = element;
|
||||
this.blockMargin = theme.getBlockMargin();
|
||||
this.blockQuoteWidth = theme.getBlockQuoteWidth();
|
||||
this.paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return blockMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||
|
||||
if (LeadingMarginUtils.selfStart(start, text, this)) {
|
||||
rect.set(x, top, x + blockMargin, bottom);
|
||||
if (element.expanded) {
|
||||
paint.setColor(Color.GREEN);
|
||||
} else {
|
||||
paint.setColor(Color.RED);
|
||||
}
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
c.drawRect(rect, paint);
|
||||
|
||||
} else {
|
||||
|
||||
if (element.expanded) {
|
||||
final int l = (blockMargin - blockQuoteWidth) / 2;
|
||||
rect.set(x + l, top, x + l + blockQuoteWidth, bottom);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(Color.GRAY);
|
||||
c.drawRect(rect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
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.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630171424",
|
||||
title = "Disable HTML",
|
||||
description = "Disable HTML via replacing special `<` and `>` symbols",
|
||||
artifacts = MarkwonArtifact.CORE,
|
||||
tags = {Tag.html, Tag.rendering, Tag.parsing, Tag.plugin}
|
||||
)
|
||||
public class HtmlDisableSanitizeSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "# Html <b>disabled</b>\n\n" +
|
||||
"<em>emphasis <strong>strong</strong>\n\n" +
|
||||
"<p>paragraph <img src='hey.jpg' /></p>\n\n" +
|
||||
"<test></test>\n\n" +
|
||||
"<test>";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
return markdown
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
|
||||
import io.noties.markwon.html.HtmlEmptyTagReplacement;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlTag;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630115725",
|
||||
title = "HTML empty tag replacement",
|
||||
description = "Render custom content when HTML tag contents is empty, " +
|
||||
"in case of self-closed HTML tags or tags without content (closed " +
|
||||
"right after opened)",
|
||||
artifacts = MarkwonArtifact.HTML,
|
||||
tags = {Tag.rendering, Tag.html}
|
||||
)
|
||||
public class HtmlEmptyTagReplacementSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"<empty></empty> the `<empty></empty>` is replaced?";
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create(plugin -> {
|
||||
plugin.emptyTagReplacement(new HtmlEmptyTagReplacement() {
|
||||
@Nullable
|
||||
@Override
|
||||
public String replace(@NonNull HtmlTag tag) {
|
||||
if ("empty".equals(tag.name())) {
|
||||
return "REPLACED_EMPTY_WITH_IT";
|
||||
}
|
||||
return super.replace(tag);
|
||||
}
|
||||
});
|
||||
}))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package io.noties.markwon.app.samples.html;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
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.html.HtmlPlugin;
|
||||
import io.noties.markwon.html.HtmlTag;
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||
import io.noties.markwon.html.TagHandler;
|
||||
import io.noties.markwon.sample.annotations.MarkwonArtifact;
|
||||
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
|
||||
import io.noties.markwon.sample.annotations.Tag;
|
||||
|
||||
@MarkwonSampleInfo(
|
||||
id = "20200630115103",
|
||||
title = "Enhance custom HTML tag",
|
||||
description = "Custom HTML tag implementation " +
|
||||
"that _enhances_ a part of text given start and end indices",
|
||||
artifacts = MarkwonArtifact.HTML,
|
||||
tags = {Tag.rendering, Tag.span, Tag.html}
|
||||
)
|
||||
public class HtmlEnhanceSample extends MarkwonTextViewSample {
|
||||
@Override
|
||||
public void render() {
|
||||
final String md = "" +
|
||||
"<enhance start=\"5\" end=\"12\">This is text that must be enhanced, at least a part of it</enhance>";
|
||||
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
|
||||
.addHandler(new EnhanceTagHandler((int) (textView.getTextSize() * 2 + .05F))));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
}
|
||||
|
||||
class EnhanceTagHandler extends TagHandler {
|
||||
|
||||
private final int enhanceTextSize;
|
||||
|
||||
EnhanceTagHandler(@Px int enhanceTextSize) {
|
||||
this.enhanceTextSize = enhanceTextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(
|
||||
@NonNull MarkwonVisitor visitor,
|
||||
@NonNull MarkwonHtmlRenderer renderer,
|
||||
@NonNull HtmlTag tag) {
|
||||
|
||||
// we require start and end to be present
|
||||
final int start = parsePosition(tag.attributes().get("start"));
|
||||
final int end = parsePosition(tag.attributes().get("end"));
|
||||
|
||||
if (start > -1 && end > -1) {
|
||||
visitor.builder().setSpan(
|
||||
new AbsoluteSizeSpan(enhanceTextSize),
|
||||
tag.start() + start,
|
||||
tag.start() + end
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> supportedTags() {
|
||||
return Collections.singleton("enhance");
|
||||
}
|
||||
|
||||
private static int parsePosition(@Nullable String value) {
|
||||
int position;
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
try {
|
||||
position = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
position = -1;
|
||||
}
|
||||
} else {
|
||||
position = -1;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
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