Compare commits

...

325 Commits

Author SHA1 Message Date
Dimitry Ivanov
2ea148c30a sample, add copy code block 2021-03-15 14:55:59 +03:00
Dimitry Ivanov
205ae7b47a sample, add reddit superscript 2021-02-24 12:36:11 +03:00
Dimitry Ivanov
c54f1154b6 Sample, make tags an enum 2021-02-24 12:14:27 +03:00
Dimitry Ivanov
6f8b8e71f5 Prepare 4.6.2 release 2021-02-09 01:20:53 +03:00
Dimitry Ivanov
50b3168491 Fix deeplinking in sample app 2021-02-05 15:30:45 +03:00
Dimitry Ivanov
b8cb4d1e82 Add funding file and consulting block 2021-02-02 01:19:26 +03:00
Dimitry Ivanov
bd3408beb3 sample, imageSizeResolver usage 2021-02-01 22:10:05 +03:00
Dimitry Ivanov
646e708c82 Failing editor tests 2021-02-01 17:09:13 +03:00
Dimitry Ivanov
3069432bc2 image, add DefaultDownScalingMediaDecoder 2021-01-19 02:54:33 +03:00
Dimitry Ivanov
1bc45e0195 github actions, update NDk version info 2021-01-18 19:32:15 +03:00
Dimitry Ivanov
3e1db2abbe sample, add one line and css style parser samples 2021-01-18 19:19:40 +03:00
Dimitry Ivanov
79cd43bc9c documentation, fix awesome section typo 2021-01-12 12:50:21 +03:00
Dimitry Ivanov
14a8746599 documentation, fix install page changelog 2021-01-12 03:08:52 +03:00
Dimitry Ivanov
48518de658 Add Infinity app to the awesome section 2021-01-12 03:05:03 +03:00
Dimitry Ivanov
910bf311da sample, update check git revision 2020-12-28 16:46:21 +03:00
Dimitry Ivanov
05cdf2c400 Prepare 4.6.1 release 2020-12-28 16:13:23 +03:00
Dimitry Ivanov
eb3a986c48 sample, add TaskListMutateNestedSample 2020-12-28 15:29:06 +03:00
Dimitry Ivanov
273ecdd7cd Sample, image click 2020-12-21 16:11:43 +03:00
Dimitry Ivanov
923b00b6d0 sample, change bullet span 2020-12-08 18:18:12 +03:00
Dimitry Ivanov
89ec2a063f Add 2 samples 2020-12-04 02:08:26 +03:00
Dimitry Ivanov
02aa16a6f2 Add Stack app to awesome section of documentation 2020-11-24 13:31:17 +03:00
Dimitry Ivanov
82cb42813a Fix failing sample 2020-11-12 02:06:49 +03:00
Dimitry Ivanov
08a8cece61 sample, add exclude from parsing 2020-11-12 01:54:33 +03:00
Dimitry Ivanov
ac578906d3 Fix CHANGELOG typo 2020-10-13 15:43:22 +03:00
Dimitry Ivanov
2296d0032d Explicit release management checklist 2020-10-09 17:42:12 +03:00
Dimitry Ivanov
bde188ecd3 Update changelog 2020-10-09 17:20:09 +03:00
Dimitry
62cb8d1f4d
Merge pull request #303 from ubuntudroid/patch-1
Bump coil to 0.13.0
2020-10-09 17:13:27 +03:00
Dimitry Ivanov
c4cb4420b1 core - CustomTypefaceSpan polishing 2020-10-09 16:47:46 +03:00
Cyrus Bakhtiari-Haftlang
132756af4e Added checks before generating styled typeface
Other changes:
  • Added flag for enabling merging of styles
2020-10-09 15:31:01 +03:00
Cyrus Bakhtiari-Haftlang
89e89c766e Resolves style issues in CustomTypefaceSpan
When two or more `CustomTypefaceSpan` are applied on a same character
sequence, previous `TextPaint`'s style was not respected.

Example:
--------
**Strong Emphasis _strong emphasis and emphasis combined_**

Before the inner text will only have the font applied without respecting
that the outer text is strong.

Related [bug report](https://issuetracker.google.com/issues/169658958)
on Google:
2020-10-09 15:30:35 +03:00
Sven Bendel
537d8e715d
Bump coil to 0.13.0
Coil has reverted to looking up image data in its cache on the main thread as opposed to the changes introduced in 0.12.0 due to the downsides of the background thread approach. This more sane approach is probably usually also what markwon users would expect.
2020-10-08 17:26:12 +02:00
Dimitry Ivanov
7002dbfb8d Prepare 4.6.0 release 2020-09-18 23:55:54 +03:00
Dimitry Ivanov
63ed271133 sample, add wysiwyg sample 2020-09-18 23:42:20 +03:00
Dimitry Ivanov
dcd9d428ee ext-tasklist, changed task list parser implementation 2020-09-02 23:42:06 +03:00
Dimitry Ivanov
905c9fa159 app-sample, fix duplicate ids for samples 2020-08-31 23:46:13 +03:00
Dimitry Ivanov
f8eaac6197 ext-tables, table aware movement method 2020-08-31 23:03:51 +03:00
Dimitry Ivanov
4c3fba8929 sample, add deeplink sample 2020-08-26 15:38:20 +03:00
Dimitry Ivanov
2356dd4618 sample, add regular coil sample 2020-08-26 13:21:48 +03:00
Dimitry Ivanov
8cea2e0202 sample, update deploy script 2020-08-26 12:49:53 +03:00
Dimitry Ivanov
d41137f6cf Merge branch 'master' of https://github.com/noties/Markwon 2020-08-26 12:49:36 +03:00
Dimitry
fa286287c8
Merge pull request #284 from magnusvs/upgrade-coil-0.12.0
Update Coil to 0.12.0 and fix breaking changes
2020-08-26 12:49:15 +03:00
Dimitry Ivanov
bc7890c603 sample, fix lint issue 2020-08-26 12:44:42 +03:00
Dimitry Ivanov
5162c13bf7 inline-parser, revert parsing index when processor returns null 2020-08-26 12:38:49 +03:00
Dimitry Ivanov
949962ee0b sample, add justify sample 2020-08-26 12:31:12 +03:00
magnusvs
12be227620 Update Coil to 0.12.0 and fix breaking changes 2020-08-24 09:08:10 +02:00
Dimitry Ivanov
65309e684c Update glide version 4.11.0 2020-08-20 11:01:36 +03:00
Dimitry Ivanov
aa2ff41831 Update version 4.5.1 2020-08-14 11:00:55 +03:00
Dimitry Ivanov
602891566d Prepare 4.5.1 release 2020-08-13 19:39:27 +03:00
Dimitry Ivanov
854fa744c7 sample, add thematic break bottom margin sample 2020-08-13 18:51:39 +03:00
Dimitry Ivanov
06413aaf36 sample, add reddit spoiler sample 2020-08-13 18:29:35 +03:00
Dimitry Ivanov
55e640af9c image-coil, change api dependency to base 2020-08-13 17:48:44 +03:00
Dimitry Ivanov
d26da7c1a0 ext-tables fix coumn width rounding issue 2020-08-10 12:12:48 +03:00
Dimitry Ivanov
df89c06f22 4.5.1-SNAPSHOT 2020-08-10 11:53:54 +03:00
Dimitry Ivanov
be667e3b45 image-coil fix image loading 2020-08-10 11:48:16 +03:00
Dimitry
961ff32c9a
Merge pull request #275 from ubuntudroid/bugfix/reproduce-image-loading-on-second-view
Use singleton coil instance in recycler view sample to reproduce image loading issue on second view
2020-08-10 11:06:43 +03:00
Dimitry Ivanov
2adfeeba51 Prepare 4.5.0 release 2020-08-05 12:32:32 +03:00
Sven Bendel
0a365840e1 Use singleton coil instance in recycler view sample to reproduce image loading issue on second view 2020-08-03 17:56:36 +02:00
Dimitry Ivanov
e4bfe9f790 Sample, fix unit tests 2020-08-03 17:23:02 +03:00
Dimitry Ivanov
28c2a7047d Sample, add coild inside RecyclerView sample 2020-08-03 16:27:19 +03:00
Dimitry Ivanov
ac05b07123 Sample, native and HTML image sample 2020-08-03 15:19:05 +03:00
Dimitry
72f6174db9
Merge pull request #268 from noties/f/sample-app
Sample app unification
2020-08-03 14:41:04 +03:00
Dimitry Ivanov
76fccb15b6 Sample, update samples 2020-07-30 23:23:28 +03:00
Dimitry Ivanov
eab8a80177 Sample, remove migration script 2020-07-30 23:17:30 +03:00
Dimitry Ivanov
782353cd65 Sample, migrate error in id generation 2020-07-30 23:17:09 +03:00
Dimitry Ivanov
01143fd5a5 Sample, py script to migrate sample ids 2020-07-30 23:13:54 +03:00
Dimitry Ivanov
0227615cb2 Sample, generated id README section 2020-07-29 15:05:55 +03:00
Dimitry Ivanov
3a25a1b911 Docs, add local deeplink to NotFound 2020-07-29 14:44:49 +03:00
Dimitry Ivanov
6953b2cfde Update web links to samples 2020-07-29 14:22:25 +03:00
Dimitry Ivanov
3f9ac3d5f4 Eject vuepress theme to customize 404 page to include sample app deeplinking 2020-07-29 13:44:53 +03:00
Dimitry Ivanov
f0808e6997 Sample, handle noties.io deeplinks alongside with custom markwon scheme 2020-07-29 13:20:28 +03:00
Dimitry Ivanov
0a6c5df0f2 Sample, add block handler sample 2020-07-29 12:12:33 +03:00
Dimitry Ivanov
aafbd8585c Update after merging 270 PR 2020-07-29 12:00:11 +03:00
Dimitry
6ec7132273
Merge pull request #270 from seatgeek/dallas/add_link_underline_option
Adding support for disabling the underlining of links
2020-07-29 11:52:31 +03:00
Dallas Gutauckis
74c632e8db Adding support for disabling the underlining of links 2020-07-23 19:39:02 -04:00
Dimitry Ivanov
04b1d695ab Add explicit NDK version for CI builds 2020-07-16 18:25:59 +03:00
Dimitry Ivanov
2318dc02ff Updategithub workflows to include gradle stacktraces 2020-07-16 17:56:28 +03:00
Dimitry Ivanov
2d6c3afed2 Update build tools and fix sample app signing in CI 2020-07-16 17:41:23 +03:00
Dimitry Ivanov
11bec3d37e Add sample app signing (also ignore in CI) 2020-07-16 16:32:09 +03:00
Dimitry Ivanov
a330b57be8 Update README 2020-07-16 16:22:26 +03:00
Dimitry Ivanov
ae01404b14 Update documentation code snippets 2020-07-16 12:14:12 +03:00
Dimitry Ivanov
7d76cb8eac Change release management 2020-07-16 11:58:41 +03:00
Dimitry Ivanov
df23339dba Sample app, all samples test 2020-07-11 19:54:03 +03:00
Dimitry Ivanov
c96ea690f6 Sample app, update check 2020-07-11 15:52:01 +03:00
Dimitry Ivanov
086494bd97 Sample, staic context-aware cache 2020-07-07 23:08:50 +03:00
Dimitry Ivanov
78ec885294 Sample app deeplinking 2020-07-03 16:16:25 +03:00
Dimitry Ivanov
9dce1c8533 Removed app and sample modules 2020-07-02 17:20:04 +03:00
Dimitry Ivanov
12a73d982c Finished moving samples to app-sample 2020-07-02 17:12:22 +03:00
Dimitry Ivanov
0b1544feae Sample app, moving more samples 2020-06-30 20:28:09 +03:00
Dimitry Ivanov
bc58790704 Sample app, adding moving samples 2020-06-29 20:21:18 +03:00
Dimitry Ivanov
25740d7389 Sample app, adding samples 2020-06-27 11:24:55 +03:00
Dimitry Ivanov
377d8c6deb Sample app another smple 2020-06-26 18:43:35 +03:00
Dimitry Ivanov
dc139319a0 Sample app, first real sample 2020-06-26 18:30:51 +03:00
Dimitry Ivanov
bea6d6aeec MakwonReducer filter out LinkReferenceDefinition 2020-06-26 17:56:42 +03:00
Dimitry Ivanov
1a90f1e609 Sample app process outgoing repositories links 2020-06-26 17:51:59 +03:00
Dimitry Ivanov
186390805a Merge branch 'develop' into f/sample-app 2020-06-26 17:12:17 +03:00
Dimitry Ivanov
45d205ba8c Sample app readme functionality 2020-06-26 17:12:13 +03:00
Dimitry Ivanov
860d70d6d1 Sample app, adding and reading samples 2020-06-25 19:04:47 +03:00
Dimitry Ivanov
66f77f35fe Sample app, bind list results, search 2020-06-18 15:39:21 +03:00
Dimitry Ivanov
7e8ed3ea0b sample app (WIP) 2020-06-12 19:42:56 +03:00
Dimitry Ivanov
2076b83675 Create samples symbolic file 2020-06-12 15:52:50 +03:00
Dimitry Ivanov
ba85ea0e98 Prepare annotation processor 2020-06-12 15:47:09 +03:00
Dimitry Ivanov
cd7aae7c9e Working with new sample application 2020-06-11 19:14:53 +03:00
Dimitry Ivanov
05b78e936b Sample, read more plugin 2020-06-11 17:12:22 +03:00
Dimitry Ivanov
07310127be Add initial design file 2020-06-10 12:04:18 +03:00
Dimitry Ivanov
1ba2da3757 AsyncDrawable resume Animatable if was playing before 2020-06-09 17:04:27 +03:00
Dimitry Ivanov
9347208746 AsyncDrawable remove deprecated hasKnownDimentions 2020-06-09 15:47:22 +03:00
Dimitry Ivanov
19b6763a23 Link title sample 2020-06-09 15:23:06 +03:00
Dimitry Ivanov
bdb47c73f2 BangInlineProcessor reset state if no image match found 2020-06-09 15:14:54 +03:00
Dimitry Ivanov
7f6d85e1fb Fix GlideImagesPlugin.create(Context) factory method 2020-06-09 13:36:07 +03:00
Dimitry Ivanov
dfa21f68e2 Add core plugin explicit movement method test 2020-05-28 14:49:18 +03:00
Dimitry Ivanov
94aef9934e sample add TriggerParentHotspot 2020-05-28 14:07:13 +03:00
Dimitry Ivanov
b1a0f3b739 hasExplicitMovementMethodPlugin sample 2020-05-28 13:20:05 +03:00
Dimitry Ivanov
8e332712fe CorePlugin hasExplicitMovementMethodPlugin configuration 2020-05-28 13:03:50 +03:00
Dimitry Ivanov
03770cfe2d SimpleTagHandler visit children if tag is block 2020-05-28 11:54:15 +03:00
Dimitry Ivanov
6103ec0574 4.4.1-SNAPSHOT 2020-05-19 12:57:17 +03:00
Dimitry
3ab015175b
Merge pull request #248 from noties/v4.4.0
V4.4.0
2020-05-19 10:17:46 +03:00
Dimitry Ivanov
c2c59041f5 Prepare 4.4.0 release 2020-05-14 12:23:47 +03:00
Dimitry Ivanov
d42ae41409 Add fallbackToRawInputWhenEmpty configuration 2020-05-12 14:41:24 +03:00
Dimitry Ivanov
c450765ab4 Add Senstone Portable Voice Assistant to awesome section 2020-05-12 13:51:51 +03:00
Dimitry Ivanov
477078470b jlatex, do not use deprecated fitCanvas method 2020-05-12 13:36:01 +03:00
Dimitry Ivanov
171b6d40a0 ImageDestinationProcessor (before UrlProcessor), limit usage to images only 2020-05-12 13:33:59 +03:00
Dimitry Ivanov
21152f368f Update changelog 2020-05-12 12:30:54 +03:00
Dimitry
e59911cfde
Merge pull request #244 from tylerbwong/update-coil
Update Coil Version
2020-05-12 12:23:17 +03:00
Tyler Wong
e386880978 Fix deprecation 2020-05-09 22:10:54 -07:00
Tyler Wong
7d49afaac7 Update Coil 2020-05-09 22:02:45 -07:00
Dimitry Ivanov
c661eb486d Update jlatexmath-android dependency 2020-04-30 16:30:33 +03:00
Dimitry Ivanov
851172a785 Fix table row span width calculation 2020-04-28 19:19:38 +03:00
Dimitry Ivanov
5451a2722e Clean up the underline investigations 2020-04-28 18:22:37 +03:00
Dimitry Ivanov
a135e07f16 Expose enabled block types in CorePlugin 2020-04-28 18:14:07 +03:00
Dimitry Ivanov
ab83dad618 Update jlatexmath implicit dependency 0.1.2 2020-04-28 18:00:00 +03:00
Dimitry Ivanov
bc38768539 Images support inside table cells 2020-04-28 17:58:41 +03:00
Dimitry Ivanov
b497f872e5 TextViewSpan and TextLayoutSpan 2020-04-28 16:57:43 +03:00
Dimitry Ivanov
3006f8d486 prepare 4.4.0-SNAPSHOT version 2020-04-28 16:56:28 +03:00
Dimitry Ivanov
fc73e08bea Merge with master 2020-04-01 11:38:35 +03:00
Dimitry
a26c13c93a
Merge pull request #226 from noties/v4.3.1
V4.3.1
2020-04-01 09:53:56 +02:00
Dimitry Ivanov
0f968662a8 Prepare 4.3.1 release 2020-04-01 09:48:37 +03:00
Dimitry Ivanov
0ae3a3d66e Update changelog 2020-03-31 23:41:10 +03:00
Dimitry
b48b0889da
Merge pull request #221 from KirkBushman/master
added PrecomputedFutureTextSetterCompat
2020-03-31 22:30:41 +02:00
Dimitry Ivanov
ddfa9c98b8 Table plugin, better borders 2020-03-29 17:10:23 +03:00
Dimitry Ivanov
33f0dcb841 Fix links in ext-table 2020-03-29 10:36:51 +03:00
luca
a6bd102e82 changed noop on PrecomputedFutureTextSetterCompat to error throw 2020-03-28 17:51:06 +01:00
Dimitry Ivanov
f47124a2ac Sample, add heading handler 2020-03-26 16:16:54 +03:00
Dimitry Ivanov
3ef41b1b81 Update sample (toc plugin) 2020-03-25 21:15:15 +03:00
Dimitry Ivanov
3ee62a724c Update sample with anchor and bullet-letter plugins 2020-03-24 12:16:03 +03:00
Dimitry Ivanov
54e5b27d59 Update sample 2020-03-24 09:55:28 +03:00
Dimitry Ivanov
392333806a image, GifSupport and SvgSupport Class.forName 2020-03-24 09:26:36 +03:00
Dimitry Ivanov
abeb5044af Update CHANGELOG 2020-03-24 08:58:39 +03:00
luca
c4a2bb94e2 added PrecomputedFutureTextSetterCompat with tests in the sample app 2020-03-23 13:24:28 +01:00
Dimitry
cca24090c1
Merge pull request #216 from francescocervone/feature/fix-dexguard-optimization-issue
Fix DexGuard optimization issue
2020-03-23 12:00:32 +02:00
Dimitry Ivanov
d1479dba8d Begin 4.3.1-SNAPSHOT 2020-03-23 12:17:04 +03:00
Francesco Cervone
9c469be176 Fix DexGuard optimization issue 2020-03-19 16:29:12 +01:00
Dimitry Ivanov
8e3d898b40 Remove spanshot reference from changelog 2020-03-18 17:02:49 +03:00
Dimitry
924abae784
Merge pull request #215 from noties/v.4.3.0
V4.3.0
2020-03-18 14:31:28 +02:00
Dimitry Ivanov
fe3d567619 Prepare 4.3.0 release 2020-03-18 14:20:58 +03:00
Dimitry Ivanov
b5a30a55b3 Update samples 2020-03-18 13:53:56 +03:00
Dimitry Ivanov
815f733892 Sample, anchor links 2020-03-16 10:30:44 +03:00
Dimitry Ivanov
c425773c84 Add remote views sample 2020-03-12 10:25:18 +03:00
Dimitry Ivanov
0b813e43f7 BlockHandler abstraction in core module 2020-03-11 12:43:20 +03:00
Dimitry Ivanov
69c2d1255c Stabilizing latex API 2020-03-09 17:46:50 +03:00
Dimitry Ivanov
c90675d67b Latex, default text color if not specified explicitly 2020-03-09 14:54:05 +03:00
Dimitry Ivanov
86d34cef6f Documentation, add changelog to install section 2020-03-08 14:48:41 +03:00
Dimitry Ivanov
d31940a290 Pull from origin (linkify compat) 2020-03-08 13:19:03 +03:00
Dimitry Ivanov
20d2bebd2b Merge branch 'develop' of https://github.com/noties/Markwon into develop 2020-03-08 13:15:13 +03:00
Dimitry Ivanov
a94090a746 latex, expose text color customization 2020-03-08 13:03:40 +03:00
Dimitry Ivanov
12c7c8909b Non empty bounds for AsyncDrawable when no dimensions available 2020-03-08 12:45:31 +03:00
Dimitry Ivanov
db660d2a40 latex block parsing tests 2020-03-07 14:14:10 +03:00
Dimitry
9cefe57532
Merge pull request #202 from drakeet/master
Allow LinkifyPlugin to use LinkifyCompat through config
2020-03-05 10:49:22 +02:00
Drakeet
8c04748597 Small change: add @LinkifyMask to LinkifyCompatTextAddedListener 2020-03-04 23:10:31 +08:00
Drakeet
b047f8131b LinkifyCompatTextAddedListener 2020-03-04 23:05:40 +08:00
Dimitry Ivanov
5c3763a9a1 JLatexMathPlugin error handler non-null latex argument 2020-03-02 10:46:18 +03:00
Dimitry Ivanov
a1f12641c3 JLatexMathPlugin add error handling 2020-03-02 10:22:04 +03:00
Dimitry Ivanov
c98f456744 LinkResolverDef defaults to https when no scheme is available 2020-03-02 09:54:58 +03:00
Dimitry Ivanov
fa83b05724 Merge branch 'develop' of https://github.com/noties/Markwon into develop 2020-03-02 09:27:20 +03:00
Dimitry
c8dfd9800b
Merge pull request #203 from drakeet/PureWriter
Add Pure Writer to list of awesome section
2020-03-02 08:26:57 +02:00
Dimitry Ivanov
3ac21a7ab3 Clarify CHANGELOG for LinkResolver rename 2020-03-02 09:26:44 +03:00
Drakeet
3068cb6987 Add Pure Writer to list of awesome section 2020-02-29 21:21:15 +08:00
Drakeet
f887cb132b Add java doc notes for the useCompat 2020-02-29 21:05:42 +08:00
Drakeet
d7f52607ab Allow LinkifyPlugin to use LinkifyCompat through config 2020-02-29 20:50:30 +08:00
Dimitry Ivanov
823c26448a Clean up README 2020-02-26 17:37:33 +03:00
Dimitry Ivanov
9532d32e8d Add SoftBreakAddsNewLinePlugin plugin in core module 2020-02-26 17:35:58 +03:00
Dimitry Ivanov
cc35c35581 Update CHANGELOG for upcoming 4.3.0 version 2020-02-26 17:29:49 +03:00
Dimitry Ivanov
8da8a37178 Update sample application 2020-02-26 17:18:42 +03:00
Dimitry Ivanov
f61e0b7b20 Merge branch 'f/latex-inline' into develop 2020-02-26 17:04:50 +03:00
Dimitry
8d3f0e908d
Merge pull request #200 from noties/v4.2.2
V4.2.2
2020-02-26 15:31:18 +02:00
Dimitry Ivanov
047ff864f1 Remove SNAPSHOT mention 2020-02-26 16:23:55 +03:00
Dimitry Ivanov
39370707ee Prepare 4.2.2 release 2020-02-26 16:17:40 +03:00
Dimitry Ivanov
1c08e3f240 LatexPlugin now depens on inline-parser plugin 2020-02-26 16:10:20 +03:00
Dimitry Ivanov
74682ae605 MarkwonInlineParserPlugin 2020-02-26 15:54:08 +03:00
Dimitry Ivanov
a80ff09e15 Update sample configuration for latex block_and_inline renderMode 2020-02-26 15:09:10 +03:00
Dimitry Ivanov
c7494a9225 Latex, introduce theme and render-mode 2020-02-26 13:40:28 +03:00
Dimitry Ivanov
8d483fe49d Sample, add task list toggle 2020-02-26 09:51:33 +03:00
Dimitry Ivanov
7af0ead3a3 Working with latex plugin 2020-02-14 18:35:44 +03:00
Dimitry Ivanov
976dfa3162 MarkwonSpansFactory append-prepend methods 2020-02-14 12:55:03 +03:00
Dimitry Ivanov
f7f8f6d1ee Fix empty code input in syntax-highlight 2020-02-14 11:33:24 +03:00
Dimitry Ivanov
d78b278b86 Latex inline parsing WIP 2020-02-10 22:25:20 +03:00
Dimitry Ivanov
33701a179f Fix AsyncDrawable display when it has placeholder with empty bounds 2020-02-08 15:08:54 +03:00
Dimitry
c939c0fa5c
Merge pull request #191 from noties/v4.2.1
V4.2.1
2020-02-02 16:59:13 +02:00
Dimitry Ivanov
34f71f13d2 Prepare 4.2.1 release 2020-02-02 17:51:11 +03:00
Dimitry Ivanov
a298016ac2 Working with inline latex parsing 2020-02-02 17:46:18 +03:00
Dimitry Ivanov
ef97b0bc25 Revert android-gif-drawable version 2020-01-14 16:58:01 +03:00
Dimitry Ivanov
d8b3d02368 Update android gradle plugin 3.5.3 2020-01-14 16:43:58 +03:00
Dimitry Ivanov
130a60265b Update android-gif-drawable compileOnly dependency version 2020-01-14 16:40:42 +03:00
Dimitry Ivanov
6d9121b54d Update images plugin documentation page 2020-01-14 16:33:08 +03:00
Dimitry Ivanov
b55b1f0dcc Reduce number of invalidations in AsyncDrawable 2020-01-14 16:05:28 +03:00
Dimitry Ivanov
2e7d0aa46b Sample handling of details HTML tag 2019-12-23 17:31:27 +03:00
Dimitry Ivanov
17756a1137 Update documentation index page 2019-12-20 12:46:41 +03:00
Dimitry Ivanov
70113b7b16 Update index page for documentation 2019-12-19 18:09:21 +03:00
Dimitry Ivanov
7c0d86e0a6 4.2.1-SNAPSHOT 2019-11-15 18:15:42 +03:00
Dimitry
b844f4db6c
Merge pull request #175 from noties/v4.2.0
V4.2.0
2019-11-15 17:49:05 +03:00
Dimitry Ivanov
39177057af Prepare 4.2.0 release 2019-11-15 17:32:49 +03:00
Dimitry Ivanov
00d60e2399 FactoryBuilderNoDefaults abstraction 2019-11-15 17:23:42 +03:00
Dimitry Ivanov
efa3473cfb Fix tests 2019-11-14 18:06:59 +03:00
Dimitry Ivanov
6c4ffd1778 Update to commonmark-java 0.13.0 2019-11-14 17:38:24 +03:00
Dimitry Ivanov
d2e4730179 Add inline processor to custom extension sample 2019-11-14 16:19:02 +03:00
Dimitry Ivanov
36089699d4 Update android gradle plugin to 3.5.2 2019-11-14 16:04:23 +03:00
Dimitry Ivanov
3c23140ac0 Commonmark-java inline parser implementation 2019-11-14 15:30:35 +03:00
Dimitry Ivanov
d6fe069728 Update CHANGELOG 2019-11-14 15:27:39 +03:00
Dimitry
6b9e79ce5e
Merge pull request #174 from tylerbwong/image-coil
Add CoilImagesPlugin Module
2019-11-14 15:18:20 +03:00
Tyler Wong
d1d0876d6d
Add author to javadoc 2019-11-13 10:22:28 -08:00
Tyler Wong
de04e5069b
Update to kotlin syntax highlighting 2019-11-13 10:21:32 -08:00
Dimitry Ivanov
136c6bd51b Add spec test to inline-parser module 2019-11-13 14:52:58 +03:00
Dimitry Ivanov
f2f5026694 Add sample and documentation for inline-parser 2019-11-13 14:38:49 +03:00
Dimitry Ivanov
93a14b4731 Created inline-parser module 2019-11-13 13:38:59 +03:00
Tyler Wong
fb4e2c089f Add more create functions 2019-11-13 00:16:19 -08:00
Tyler Wong
4fa1ac718f Add ImageLoader API 2019-11-12 23:46:01 -08:00
Tyler Wong
bf61d8c627 Update docs 2019-11-12 23:37:28 -08:00
Tyler Wong
5fe1e07b39 Expose LoadRequest instead 2019-11-12 23:35:09 -08:00
Tyler Wong
0c305fa0ba Expose LoadRequestBuilder 2019-11-12 23:23:28 -08:00
Tyler Wong
1983b1b46e Remove extra create function 2019-11-12 22:08:14 -08:00
Tyler Wong
a1182e209a Add CoilImagesPlugin 2019-11-12 22:00:33 -08:00
Dimitry Ivanov
e95defb67c It is alive 2019-11-12 17:32:54 +03:00
Dimitry Ivanov
75c3aa8102 Update editor documentation page 2019-11-11 17:15:18 +03:00
Dimitry Ivanov
c6fd779f33 Stabilizing editor API 2019-11-11 17:08:41 +03:00
Dimitry Ivanov
a6201b1b35 LinkifyPlugin is thread-safe 2019-11-08 18:03:46 +03:00
Dimitry Ivanov
681a7f68d7 Proper delimiters matching and autolinking support 2019-11-08 17:55:11 +03:00
Dimitry Ivanov
5e3ace0c29 Add SvgPictureMediaDecoder 2019-11-07 17:52:52 +03:00
Dimitry Ivanov
4b2d38b92f Minor clean up 2019-11-07 17:48:34 +03:00
Dimitry Ivanov
0fabf7daff Fix core tests 2019-11-07 17:36:49 +03:00
Dimitry Ivanov
2488c1047b Add editor documentation page 2019-11-07 17:30:20 +03:00
Dimitry Ivanov
bd53c014a1 Added editor tests 2019-11-07 16:40:55 +03:00
Dimitry Ivanov
f1e750b305 Add editor sample 2019-11-07 14:02:07 +03:00
Dimitry Ivanov
8768e8a33c Editor implementation 2019-11-07 00:27:41 +03:00
Dimitry Ivanov
870733ee2a Update snapshot version to 4.1.3-SNAPSHOT 2019-11-05 13:20:06 +03:00
Dimitry
ba22ca88e2
Merge pull request #172 from noties/v4.1.2
4.1.2
2019-10-16 15:34:36 +03:00
Dimitry Ivanov
bc3a7b75d2 Prepare 4.1.2 release 2019-10-16 15:16:50 +03:00
Dimitry Ivanov
003b5e90b4 Github actions fix workflow 2019-10-16 14:22:50 +03:00
Dimitry Ivanov
c9e1bb0965 Another attempt at publishing snapshot via github actions 2019-10-16 14:19:43 +03:00
Dimitry Ivanov
b22a840dbe Fix re-use of render-props for visitor 2019-10-15 21:28:07 +03:00
Dimitry Ivanov
204b803245 4.1.2 2019-10-11 09:29:56 +03:00
Dimitry Ivanov
caddddc710 Merge branch 'master' into develop 2019-08-30 00:56:05 +03:00
Dimitry Ivanov
f9f8d36c02 Another try with github actions (add individual develop and pull-request workflows) 2019-08-30 00:49:36 +03:00
Dimitry Ivanov
883f5967de Merge branch 'master' into develop 2019-08-29 15:09:39 +03:00
Dimitry Ivanov
9d61454858 Add individual build checks for master and not-master branches 2019-08-29 15:08:42 +03:00
Dimitry Ivanov
27d835846e Sync with master 2019-08-29 14:51:25 +03:00
Dimitry Ivanov
aee6e49b1f Update github workflows (single build) 2019-08-29 14:46:41 +03:00
Dimitry
4348555b75
Merge pull request #162 from noties/develop
* `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
2019-08-29 14:30:12 +03:00
Dimitry Ivanov
529e9a88ca Remove snapshot publishing on push to develop 2019-08-29 14:16:27 +03:00
Dimitry Ivanov
5cbdbe1759 Change snapshot workflow env variables names (uppercase) 2019-08-29 14:07:51 +03:00
Dimitry Ivanov
0a7356ecf8 Prepare 4.1.1 release 2019-08-29 13:54:53 +03:00
Dimitry Ivanov
008faa6f49 Define release and snapshot github workflows 2019-08-29 13:53:34 +03:00
Dimitry Ivanov
02e83a62db Update 4.1.1-SNAPSHOT changelog 2019-08-28 13:33:05 +03:00
Dimitry Ivanov
4406a5faaf Introduce MarkwonVisitorFactory 2019-08-28 13:24:55 +03:00
Dimitry Ivanov
3c77448682 Update android gradle plugin 3.5.0 2019-08-28 13:07:25 +03:00
Dimitry Ivanov
1ab1b8b87a Add code block info prop 2019-08-26 14:41:33 +03:00
Dimitry Ivanov
1b7fbfb77f ImagesPlugin print a warning for SVG and GIF instead of full stacktrace 2019-08-26 13:53:39 +03:00
Dimitry Ivanov
fa01a50ae8 ImagesPlugin remove stacktrace printing when checking for SVG and GIF dependencies 2019-08-26 13:06:21 +03:00
Dimitry Ivanov
b3e7749c7a Fix missing subsequent table-blocks spacing 2019-08-24 13:22:44 +03:00
Dimitry Ivanov
6a06e56c1c Fix MarkdownRenderer 2019-08-06 19:30:25 +03:00
Dimitry Ivanov
6c8f1c04bb Update documentation web-site 2019-08-06 19:27:20 +03:00
Dimitry
3fe514aeea
Merge pull request #154 from noties/develop
4.1.0
2019-08-06 18:31:11 +03:00
Dimitry Ivanov
a2d35a1553 Prepare 4.1.0 release 2019-08-06 18:13:13 +03:00
Dimitry Ivanov
b6fa66914f AsyncDrawable defer invalidation 2019-08-01 12:44:26 +03:00
Dimitry Ivanov
2a43797023 TablePlugin defer table-row invalidation 2019-08-01 12:29:46 +03:00
Dimitry Ivanov
620da87694 Removed loggin statement in precomputed-text-setter-compat 2019-07-27 15:40:38 +03:00
Dimitry Ivanov
54335dce6e Change text-setter to use precomputed-text-compat (androix.core) 2019-07-27 15:23:49 +03:00
Dimitry Ivanov
7e12552060 Add TextSetter interface 2019-07-26 15:35:44 +03:00
Dimitry Ivanov
822f16510e Prepare 4.0.2 release 2019-07-15 14:22:57 +03:00
Dimitry Ivanov
9d09bd4236 Update CHANGELOG for 4.0.2-SNAPSHOT 2019-07-04 17:37:48 +03:00
Dimitry Ivanov
879dde1382 Scale down LaTeX formula if exceed canvas width (keep ratio) 2019-07-04 17:26:03 +03:00
Dimitry Ivanov
8daa59709b Sanitize latex text placeholder (no new lines) 2019-07-04 17:17:33 +03:00
Dimitry Ivanov
85f201702e Prepare 4.0.1 release 2019-07-03 12:01:59 +03:00
Dimitry Ivanov
c68aeabcf9 Fix JLatexMathPlugin (backgroundProvider null) 2019-07-03 12:00:02 +03:00
Dimitry
24151dff7d
Merge pull request #146 from noties/v4.0.0
V4.0.0
2019-07-01 20:03:17 +03:00
Dimitry Ivanov
aa64aa7020 Update documentation to 4.0.0 2019-07-01 19:50:33 +03:00
Dimitry Ivanov
1407ae0cf8 Prepare 4.0.0 release 2019-07-01 15:24:33 +03:00
Dimitry Ivanov
f99952ec01 Ensure README in all modules 2019-06-26 19:05:23 +03:00
Dimitry Ivanov
b7606c7ee7 Update CHANGELOG 2019-06-26 18:48:00 +03:00
Dimitry Ivanov
386254f962 Preparing 4.0.0 release 2019-06-26 17:57:33 +03:00
Dimitry Ivanov
d65a1809ca Move CHANGELOG from documentation to the root of repository 2019-06-26 13:58:33 +03:00
Dimitry Ivanov
18b1d5b0bb Update recipes documentation page 2019-06-26 13:49:35 +03:00
Dimitry Ivanov
06c2763ac6 Add LastLineSpacingSpan 2019-06-26 12:57:34 +03:00
Dimitry Ivanov
eca93dd27c Add simple-ext documentation 2019-06-24 15:08:43 +03:00
Dimitry Ivanov
a082e9ed44 Add simple-ext module 2019-06-24 14:35:50 +03:00
Dimitry Ivanov
fdb0f76e13 Add html sample 2019-06-24 13:47:44 +03:00
Dimitry Ivanov
ffb5848c3c Moved LinkResolver to independent entity 2019-06-22 15:20:46 +03:00
Dimitry Ivanov
213f5cf281 ImageSizeResolver change (accept async-drawable) 2019-06-22 12:09:53 +03:00
Dimitry Ivanov
4fec46fb4d Fix async-drawable tests 2019-06-21 00:56:43 +03:00
Dimitry Ivanov
d630039196 Fix wrong fitCanvas config for JLatexMathPlugin 2019-06-20 16:19:43 +03:00
Dimitry Ivanov
6ed641fa47 Fix j-latex-plugin background config 2019-06-20 16:06:57 +03:00
Dimitry Ivanov
5c78f1d515 Image, apply bounds for result drawable if empty 2019-06-19 00:28:22 +03:00
Dimitry Ivanov
a3ebae3b87 Renamed DefaultImageMediaDecoder to DefaultMediaDecoder 2019-06-17 23:01:02 +03:00
Dimitry Ivanov
8944f39592 Working with documentation v4 2019-06-17 14:08:33 +03:00
Dimitry Ivanov
8b0edc32c3 Started with v4 documentation 2019-06-12 18:45:28 +03:00
Dimitry Ivanov
14591508b5 Update awesome section for documentation 2019-06-12 16:51:48 +03:00
Dimitry Ivanov
512814ac4c SVG and GIF dependencies check 2019-06-11 18:53:31 +03:00
Dimitry Ivanov
9aade9d6ca Migrate to androidx 2019-06-11 18:42:14 +03:00
Dimitry Ivanov
a2a5857f06 registryImpl test 2019-06-08 15:50:07 +03:00
Dimitry Ivanov
173425ed53 Image loader tests 2019-06-05 15:17:53 +03:00
Dimitry Ivanov
ab4c80dca5 Update dependencies 2019-06-05 00:35:39 +03:00
Dimitry Ivanov
13536302cc Package name and maven group-id change io.noties.markwon 2019-06-05 00:20:36 +03:00
Dimitry Ivanov
79b99abb24 Add HtmlConfigure for HtmlPlugin 2019-06-04 23:44:52 +03:00
Dimitry Ivanov
4b918bf094 Unified html and image modules 2019-06-04 17:18:42 +03:00
Dimitry Ivanov
dba07e3f3c Linkify perf notice 2019-06-04 16:39:07 +03:00
Dimitry Ivanov
df0177af95 Move HTML specifics to html module 2019-06-04 16:32:35 +03:00
Dimitry Ivanov
f3476ca5cc Use Call.Factory instead of OkHttpClient in images module 2019-06-04 15:41:05 +03:00
Dimitry Ivanov
6bf04e38ad Add tests for markwon-image module 2019-06-03 15:28:52 +03:00
Dimitry Ivanov
cedb3971a0 text-added-listener for core-plugin and linkify module 2019-06-02 22:12:40 +03:00
Dimitry Ivanov
2e35ef53bb Ensure explicit dependencies for SVG and GIF 2019-05-30 15:28:39 +03:00
Dimitry Ivanov
0b0d3c4753 Add image loader module based on Glide 2019-05-30 00:08:47 +03:00
Dimitry Ivanov
19091b5675 Add image loading plugin based on picasso library 2019-05-29 18:13:45 +03:00
Dimitry Ivanov
e35d3ad044 Update jlatex plugin to be independent of images 2019-05-29 16:53:14 +03:00
Dimitry Ivanov
64af306e53 Fix test compilation 2019-05-28 19:01:41 +03:00
Dimitry Ivanov
5bf21bc940 Moved image loading into separate module 2019-05-28 18:50:03 +03:00
Dimitry Ivanov
661f72da0f Moving image loading functionality to standalone module 2019-05-28 16:15:25 +03:00
Dimitry Ivanov
453880bd62 AsyncDrawableLoader improvements (multiple images with the same source) 2019-05-23 14:56:15 +03:00
857 changed files with 39060 additions and 9206 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
#repo: https://github.com/noties/Markwon
custom: ["https://paypal.me/dimitryivanov"]

18
.github/workflows/build.yml vendored Normal file
View 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
View 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

View File

@ -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
View 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` -&gt; `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 -&gt; defer table invalidation (via `View.post`), so only one invalidation
happens with each draw-call
* AsyncDrawableSpan -&gt; 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` -&gt; `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 &amp; 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

View File

@ -2,7 +2,7 @@
# Markwon
[![Build Status](https://travis-ci.org/noties/Markwon.svg?branch=master)](https://travis-ci.org/noties/Markwon)
[![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](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
![stable](https://img.shields.io/maven-central/v/ru.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/ru.noties.markwon/core.svg?label=snapshot)
![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot)
```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
View 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
View 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
View 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

Binary file not shown.

1477
app-sample/samples.json Normal file

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View File

@ -0,0 +1 @@
../../main/java/io/noties/markwon/app/samples/

View File

@ -0,0 +1 @@
../../../samples.json

View 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
}
}

View File

@ -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
}
}
}

View File

@ -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))
}
}
}

View File

@ -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
))
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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})"
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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} -&gt; **${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()
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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) }
}

View File

@ -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">
![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable)
![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot)
![changelog](https://fonts.gstatic.com/s/i/materialicons/open_in_browser/v6/24px.svg?download=true)
</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
}
}
}

View File

@ -0,0 +1,3 @@
[{*.java, *.kt}]
indent_style = space
indent_size = 2

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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()
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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}"
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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** ![dolor](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4) 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 ![are](https://avatars2.githubusercontent.com/u/30618885?s=460&v=4)";
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;
}
}

View File

@ -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();
}
}

View File

@ -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 `&gt;!` so no blockquote would be parsed (when spoiler starts at new line)
return markdown.replaceAll(">!", "&gt;!");
}
@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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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!
![alt](${BuildConfig.GIT_REPOSITORY}/raw/master/art/markwon_logo.png)
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)
}

View File

@ -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()
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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
}
}

View File

@ -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));
}
}

View File

@ -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 `&amp;` 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));
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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))));
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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>&nbsp;(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>&nbsp;</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>&nbsp;</p>\n" +
" <p>&nbsp;</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>&nbsp;</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>&nbsp;&nbsp;</p>\n" +
" <hr>\n" +
" <p>&nbsp;</p>\n" +
" <p>Source: RIAJ press release</p>\n" +
" <p>&nbsp;</p>\n" +
" <p><em>©SACRA MUSIC</em></p>\n" +
" <p>&nbsp;</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");
}
}

View File

@ -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");
}
}
}

View File

@ -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" +
"![img](" + BuildConfig.GIT_REPOSITORY + "/raw/master/art/markwon_logo.png)\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);
}
}
}
}
}

View File

@ -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("<", "&lt;")
.replaceAll(">", "&gt;");
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -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);
}
}

View File

@ -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