Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

922 changed files with 5900 additions and 73586 deletions

3
.github/FUNDING.yml vendored
View File

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

View File

@ -1,4 +0,0 @@
* **Markwon version**: _{REQUIRED}_
1. Please specify expected/actual behavior
2. Please specify conditions/steps to reproduce (layout, code, markdown used, etc)

View File

@ -1,18 +0,0 @@
name: Build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew build -Prelease -PCI --stacktrace

View File

@ -1,17 +0,0 @@
name: Pull request checks
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew build -PCI --stacktrace

10
.gitignore vendored
View File

@ -1,10 +0,0 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/captures
.externalNativeBuild
**/build
**/dist
**/node_modules

22
404.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,475 +0,0 @@
# Changelog
# 4.6.2
#### Added
* `image` - `DefaultDownScalingMediaDecoder` which scales displayed images down ([#329])
[#329]: https://github.com/noties/Markwon/issues/329
# 4.6.1
#### Changed
* `core` - `CustomTypefaceSpan` new `mergeStyles` functionality and new factory method([#298])<br>Thanks [@c-b-h]
* `image-coil` - update `Coil` to `0.13.0` ([#303])<br>Thanks [@ubuntudroid]
#### Deprecated
* `core` - `CustomTypefaceSpan(Typeface)` constructor, use `CustomTypefaceSpan.create(Typeface)`
or `CustomTypefaceSpan.create(Typeface, boolean)` factory methods instead
[#298]: https://github.com/noties/Markwon/pull/298
[#303]: https://github.com/noties/Markwon/pull/303
[@c-b-h]: https://github.com/c-b-h
[@ubuntudroid]: https://github.com/ubuntudroid
# 4.6.0
#### Added
* `ext-tables` - `TableAwareMovementMethod` a special movement method to handle clicks inside tables ([#289])
#### Changed
* `ext-tasklist` - changed implementation to be in line with GFM (Github flavored markdown),
task list item is a regular list item (BulletList and OrderedList can contain it).
Internal implementation changed from block parsing to node post processing ([#291])
* `image-glide` - update to `4.11.0` version
* `inline-parser` - revert parsing index when `InlineProcessor` returns `null` as result
* `image-coil` - update `Coil` to `0.12.0` ([Coil changelog](https://coil-kt.github.io/coil/changelog/)) ([#284])<br>Thanks [@magnusvs]
[#284]: https://github.com/noties/Markwon/pull/284
[#289]: https://github.com/noties/Markwon/issues/289
[#291]: https://github.com/noties/Markwon/issues/291
[@magnusvs]: https://github.com/magnusvs
# 4.5.1
#### Changed
* `image-coil` - use `coil-base` as `api` dependency (would require explicit `coil` dependency) ([#274])
#### Fixed
* `image-coil` - deliver image result if it loaded before request disposable is created ([#272])
* `ext-tables` - fix column width rounding issue
[#272]: https://github.com/noties/Markwon/issues/272
[#274]: https://github.com/noties/Markwon/issues/274
# 4.5.0
#### Added
* `core` - `MovementMethodPlugin.none()`, `MovementMethodPlugin.link()` factory methods
* `core` - `CorePlugin` `hasExplicitMovementMethod` configuration method to **not** add implicit `LinkMovementMethod` in `afterSetText`
* `core` - `MarkwonTheme` `isLinkedUnderlined` attribute for links([#270])<br>Thanks to [@dallasgutauckis]
* `ext-latex` - `JLatexMathTheme.Padding.of(int,int,int,int)` factory method
* `app-sample` - example application
#### Changed
* `html` - `SimpleTagHandler` visits children tags if supplied tag is block one ([#235])
* `inline-parser` - `BangInlineProcessor` properly returns `null` if no image node is found (possible to define other inline parsers that use `!` as special character)
* `image` - `AsyncDrawable` won't trigger loading if it has result (aim: `RecyclerView` due to multiple attach/detach events of a View)
* `image` - `AsyncDrawable` will resume result if it is `Animatable` and was playing before detach event (aim: `RecyclerView`) ([#241])
* `core` - `MarkwonReducer` filter out `LinkReferenceDefinition` nodes
#### Fixed
* `image-glide` cache `RequestManager` in `GlideImagesPlugin#create(Context)` factory method ([#259])
#### Deprecated
* `core` - `MovementMethodPlugin.create()` use explicit `MovementMethodPlugin.link()` instead
#### Removed
* `image` - `AsyncDrawable#hasKnownDimentions` (deprecated in `4.2.1`)
* `app` and `sample` applications (merged together in a `app-sample` single app)
[#235]: https://github.com/noties/Markwon/issues/235
[#241]: https://github.com/noties/Markwon/issues/241
[#259]: https://github.com/noties/Markwon/issues/259
[#270]: https://github.com/noties/Markwon/pull/270
[@dallasgutauckis]: https://github.com/dallasgutauckis
# 4.4.0
* `TextViewSpan` to obtain `TextView` in which markdown is displayed (applied by `CorePlugin`)
* `TextLayoutSpan` to obtain `Layout` in which markdown is displayed (applied by `TablePlugin`, more specifically `TableRowSpan` to propagate layout in which cell content is displayed)
* `HtmlEmptyTagReplacement` now is configurable by `HtmlPlugin`, `iframe` handling ([#235])
* `AsyncDrawable` now uses `TextView` width without padding instead of width of canvas
* Support for images inside table cells (`ext-tables` module)
* Expose `enabledBlockTypes` in `CorePlugin`
* Update `jlatexmath-android` dependency ([#225])
* Update `image-coil` module (Coil version `0.10.1`) ([#244])<br>Thanks to [@tylerbwong]
* Rename `UrlProcessor` to `ImageDestinationProcessor` (`io.noties.markwon.urlprocessor` -&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

202
LICENSE
View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

317
README.md
View File

@ -1,317 +0,0 @@
![logo](./art/markwon_logo.png)
# 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]
library and renders result as _Android-native_ Spannables. **No HTML**
is involved as an intermediate step. <u>**No WebView** is required</u>.
It's extremely fast, feature-rich and extensible.
It gives ability to display markdown in all TextView widgets
(**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Toasts**
and all other places that accept **Spanned content**. Library provides
reasonable defaults to display style of a markdown content but also
gives all the means to tweak the appearance if desired. All markdown
features listed in [commonmark-spec] are supported
(including support for **inlined/block HTML code**, **markdown tables**,
**images** and **syntax highlight**).
`Markwon` comes with a [sample application](./app-sample/). It is a
collection of library usages that comes with search and source code for
each code sample.
Since version **4.2.0** **Markwon** comes with an [editor](./markwon-editor/) to _highlight_ markdown input
as user types (for example in **EditText**).
[commonmark-spec]: https://spec.commonmark.org/0.28/
[commonmark-java]: https://github.com/atlassian/commonmark-java/blob/master/README.md
## Installation
![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)
```kotlin
implementation "io.noties.markwon:core:${markwonVersion}"
```
Full list of available artifacts is present in the [install section](https://noties.github.io/Markwon/docs/v4/install.html)
of the [documentation] web-site.
Please visit [documentation] web-site for further reference.
> You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x)
and [3.x.x](https://github.com/noties/Markwon/tree/3.x.x) branches
## Supported markdown features:
* Emphasis (`*`, `_`)
* Strong emphasis (`**`, `__`)
* Strike-through (`~~`)
* Headers (`#{1,6}`)
* Links (`[]()` && `[][]`)
* Images
* Thematic break (`---`, `***`, `___`)
* Quotes & nested quotes (`>{1,}`)
* Ordered & non-ordered lists & nested ones
* Inline code
* Code blocks
* Tables (*with limitations*)
* Syntax highlight
* LaTeX formulas
* HTML
* Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)
* Strong emphasis (`<b>`, `<strong>`)
* SuperScript (`<sup>`)
* SubScript (`<sub>`)
* Underline (`<u>`, `ins`)
* Strike-through (`<s>`, `<strike>`, `<del>`)
* Link (`a`)
* Lists (`ul`, `ol`)
* Images (`img` will require configured image loader)
* Blockquote (`blockquote`)
* Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`)
* there is support to render any HTML tag
* Task lists:
- [ ] Not _done_
- [X] **Done** with `X`
- [x] ~~and~~ **or** small `x`
---
## Screenshots
Taken with default configuration (except for image loading) in [sample app](./app-sample/):
<a href="./art/mw_light_01.png"><img src="./art/mw_light_01.png" width="30%" /></a>
<a href="./art/mw_light_02.png"><img src="./art/mw_light_02.png" width="30%" /></a>
<a href="./art/mw_light_03.png"><img src="./art/mw_light_03.png" width="30%" /></a>
<a href="./art/mw_dark_01.png"><img src="./art/mw_dark_01.png" width="30%" /></a>
By default configuration uses TextView textColor for styling, so changing textColor changes style
---
## Documentation
Please visit [documentation] web-site for reference
[documentation]: https://noties.github.io/Markwon
## Consulting
Paid consulting is available. Please reach me out at [markwon+consulting[at]noties.io](mailto:markwon+consulting@noties.io)
to discuss your idea or a project
---
# Demo
Based on [this cheatsheet][cheatsheet]
---
## Headers
---
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
---
## Emphasis
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
---
## Lists
1. First ordered list item
2. Another item
* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
1. Ordered sub-list
4. And another item.
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
To have a line break without a paragraph, you will need to use two trailing spaces.
Note that this line is separate, but within the same paragraph.
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
---
## Links
[I'm an inline-style link](https://www.google.com)
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](../blob/master/LICENSE)
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
---
## Code
Inline `code` has `back-ticks around` it.
```javascript
var s = "JavaScript syntax highlighting";
alert(s);
```
```python
s = "Python syntax highlighting"
print s
```
```java
/**
* Helper method to obtain a Parser with registered strike-through &amp; table extensions
* &amp; task lists (added in 1.0.1)
*
* @return a Parser instance that is supported by this library
* @since 1.0.0
*/
@NonNull
public static Parser createParser() {
return new Parser.Builder()
.extensions(Arrays.asList(
StrikethroughExtension.create(),
TablesExtension.create(),
TaskListExtension.create()
))
.build();
}
```
```xml
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?android:attr/actionBarSize">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dip"
android:lineSpacingExtra="2dip"
android:textSize="16sp"
tools:text="yo\nman" />
</ScrollView>
```
```
No language indicated, so no syntax highlighting.
But let's throw in a <b>tag</b>.
```
---
## Tables
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell.
The outer pipes (|) are optional, and you don't need to make the
raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
---
## Blockquotes
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
Nested quotes
> Hello!
>> And to you!
---
## Inline HTML
```html
<u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
```
<u><i>H<sup>T<sub>M</sub></sup><b><s>L</s></b></i></u>
---
## Horizontal Rule
Three or more...
---
Hyphens (`-`)
***
Asterisks (`*`)
___
Underscores (`_`)
## License
```
Copyright 2019 Dimitry Ivanov (legal@noties.io)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
[cheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,75 +0,0 @@
# Markwon sample app
Collection of sample snippets showing different aspects of `Markwon` library usage. Includes
source code of samples, latest stable/snapshot version of the library and search functionality.
Additionally can check for updates. Can be used to preview markdown documents from the `Github.com`.
<a href="../art/sample-screen-01.png"><img src="../art/sample-screen-01.png" width="30%" /></a>
<a href="../art/sample-screen-02.png"><img src="../art/sample-screen-02.png" width="30%" /></a>
<a href="../art/sample-screen-03.png"><img src="../art/sample-screen-03.png" width="30%" /></a>
<a href="../art/sample-screen-04.png"><img src="../art/sample-screen-04.png" width="30%" /></a>
## Distribution
Sample app is distributed via special parent-less branch [sample-store](https://github.com/noties/Markwon/tree/sample-store).
Inside the app, under version badges, tap `CHECK FOR UPDATES` to check for updates. Sample app
is not attached to main libraries versions and can be _released_ independently.
Application is signed with `keystore.jks`, which fingerprints are:
* __SHA1__: `BA:70:A5:D2:40:65:F1:FA:88:90:59:BA:FC:B7:31:81:E6:37:D9:41`
* __SHA256__: `82:C9:61:C5:DF:35:B1:CB:29:D5:48:83:FB:EB:9F:3E:7D:52:67:63:4F:D2:CE:0A:2D:70:17:85:FF:48:67:51`
[Download latest APK](https://github.com/noties/Markwon/raw/sample-store/markwon-debug.apk)
## Deeplink
Sample app handles special `markwon` scheme:
* `markwon://sample/{ID}` to open specific sample given the `{ID}`
* `markwon://search?q={TEXT TO SEARCH}&a={ARTIFACT}&t={TAG}`
Please note that search deeplink can have one of type: artifact or tag (if both are specified artifact will be used).
To test locally:
```
adb shell am start -a android.intent.action.ACTION_VIEW -d markwon://sample/ID
```
Please note that you might need to _url encode_ the `-d` argument
## Building
When adding/removing samples _most likely_ a clean build would be required.
First, for annotation processor to create `samples.json`. And secondly,
in order for Android Gradle plugin to bundle resources referenced via
symbolic links (the `sample.json` itself and `io.noties.markwon.app.samples.*` directory)
```
./gradlew :app-s:clean :app-s:asDe
```
## Sample id
Sample `id` is generated manually when creating new sample. A `Live Template` can be used
to simplify generation part (for example `mid` shortcut with a single variable in `Java` and `Kotlin` scopes):
```
groovyScript("new Date().format('YYYYMMddHHmmss', TimeZone.getTimeZone('UTC'))")
```
## Tests
This app uses [Robolectric](https://robolectric.org)(v3.8) for tests which is incompatible
with JDK > 1.8. In order to run tests from command line with IDEA-bundled JDK - a special argument is
required:
```
./gradlew :app-s:testDe -Dorg.gradle.java.home="{INSERT BUNDLED JDK PATH HERE}"
```
To obtain bundled JDK:
* open `Project Structure...`
* open `SDK Location`
* copy contents of the field under `JDK Location`

View File

@ -1,166 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
def gitSha = { ->
def output = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = output
}
return output.toString().trim()
}.memoize()
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
applicationId 'io.noties.markwon.app'
minSdkVersion 23
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
resConfig 'en'
setProperty("archivesBaseName", "markwon")
buildConfigField 'String', 'GIT_SHA', "\"${gitSha()}\""
buildConfigField 'String', 'GIT_REPOSITORY', '"https://github.com/noties/Markwon"'
final def scheme = 'markwon'
buildConfigField 'String', 'DEEPLINK_SCHEME', "\"$scheme\""
manifestPlaceholders += [
'deeplink_scheme': scheme
]
}
dexOptions {
preDexLibraries true
javaMaxHeapSize '5g'
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
sourceSets {
main {
java.srcDirs += '../sample-utils/annotations'
}
}
// do not sign in CI
if (!project.hasProperty('CI')) {
signingConfigs {
config {
final def keystoreFile = project.file('keystore.jks')
final def keystoreFilePassword = 'MARKWON_KEYSTORE_FILE_PASSWORD'
final def keystoreAlias = 'MARKWON_KEY_ALIAS'
final def keystoreAliasPassword = 'MARKWON_KEY_ALIAS_PASSWORD'
final def properties = [
keystoreFilePassword,
keystoreAlias,
keystoreAliasPassword
]
if (!keystoreFile.exists()) {
throw new IllegalStateException("No '${keystoreFile.name}' file is found.")
}
final def missingProperties = properties.findAll { !project.hasProperty(it) }
if (!missingProperties.isEmpty()) {
throw new IllegalStateException("Missing required signing properties: $missingProperties")
}
storeFile keystoreFile
storePassword project[keystoreFilePassword]
keyAlias project[keystoreAlias]
keyPassword project[keystoreAliasPassword]
}
}
buildTypes {
debug {
signingConfig signingConfigs.config
}
release {
signingConfig signingConfigs.config
}
}
}
}
kapt {
arguments {
arg('markwon.samples.file', "${projectDir}/samples.json".toString())
}
}
androidExtensions {
features = ["parcelize"]
}
configurations.all {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
dependencies {
kapt project(':sample-utils:processor')
deps['annotationProcessor'].with {
kapt it['prism4j-bundler']
}
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation project(':markwon-core')
implementation project(':markwon-editor')
implementation project(':markwon-ext-latex')
implementation project(':markwon-ext-strikethrough')
implementation project(':markwon-ext-tables')
implementation project(':markwon-ext-tasklist')
implementation project(':markwon-html')
implementation project(':markwon-image')
implementation project(':markwon-inline-parser')
implementation project(':markwon-linkify')
implementation project(':markwon-recycler')
implementation project(':markwon-recycler-table')
implementation project(':markwon-simple-ext')
implementation project(':markwon-syntax-highlight')
implementation project(':markwon-image-picasso')
implementation project(':markwon-image-glide')
implementation project(':markwon-image-coil')
deps.with {
// implementation it['x-appcompat']
implementation it['x-recycler-view']
implementation it['x-cardview']
implementation it['x-fragment']
implementation it['okhttp']
implementation it['prism4j']
implementation it['gson']
implementation it['adapt']
implementation it['debug']
implementation it['android-svg']
implementation it['android-gif-impl']
implementation it['coil']
}
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
}
}

View File

@ -1,24 +0,0 @@
#!/usr/bin/env sh
# abort on errors
set -e
# build
../gradlew :app-sample:clean
../gradlew :app-sample:assembleDebug
# navigate into the build output directory
cd ./build/outputs/apk/debug/
revision=$(git rev-parse --short HEAD)
echo "output-metadata.json" > ./.gitignore
echo "$revision" > ./version
git init
git add -A
git commit -m "sample $revision"
git push -f git@github.com:noties/Markwon.git master:sample-store
cd -

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,166 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/content_padding"
tools:ignore="MissingDefaultResource,HardcodedText">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hey!" />
<io.noties.markwon.app.widget.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/content_padding"
android:paddingBottom="@dimen/content_padding"
app:fl_spacingHorizontal="@dimen/content_padding"
app:fl_spacingVertical="4dip">
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="ext-latex" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="another" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_artifact"
android:text="core" />
</io.noties.markwon.app.widget.FlowLayout>
<io.noties.markwon.app.widget.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/content_padding"
android:paddingBottom="@dimen/content_padding"
app:fl_spacingHorizontal="@dimen/content_padding"
app:fl_spacingVertical="4dip">
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="ext-latex" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="another" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="corejksjk sjkdf sdhjf sdjhf sjjksdjkjksd sdjksd hjsd hsdhjhjs shjsdhjhjsdhj sdj dshjs dhjsd sdhj sdjhsd hjsdh sjhd sdhjsd jhsd sdhj sdjhsd hjsd sjdh s" />
<TextView
style="@style/ArtifactTagText"
android:background="@drawable/bg_tag"
android:text="core" />
</io.noties.markwon.app.widget.FlowLayout>
</LinearLayout>

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.noties.markwon.app">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
<activity android:name=".sample.MainActivity">
<!-- launcher intent -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- local deeplink (with custom scheme) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="${deeplink_scheme}" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="noties.io"
android:scheme="https" />
<data android:pathPrefix="/Markwon/app"/>
<data android:pathPattern="sample/.*" />
<data android:pathPattern="search" />
</intent-filter>
</activity>
<activity
android:name=".readme.ReadMeActivity"
android:exported="true">
<!-- github markdown files handling -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="github.com"
android:scheme="https" />
<data android:pathPattern=".*\\.md" />
<data android:pathPattern=".*\\..*\\.md" />
<data android:pathPattern=".*\\..*\\..*\\.md" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.md" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1 +0,0 @@
../../../../README.md

View File

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

View File

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

View File

@ -1,58 +0,0 @@
package io.noties.markwon.app
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.os.Build
import io.noties.debug.AndroidLogDebugOutput
import io.noties.debug.Debug
import io.noties.markwon.app.readme.ReadMeActivity
import io.noties.markwon.app.sample.SampleManager
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@Suppress("unused")
// `open` is required for tests (to create a spy mockito instance)
open class App : Application() {
override fun onCreate() {
super.onCreate()
Debug.init(AndroidLogDebugOutput(BuildConfig.DEBUG))
executorService = Executors.newCachedThreadPool()
sampleManager = SampleManager(this, executorService)
ensureReadmeShortcut()
}
private fun ensureReadmeShortcut() {
if (Build.VERSION.SDK_INT < 25) {
return
}
val manager = getSystemService(ShortcutManager::class.java) ?: return
@Suppress("ReplaceNegatedIsEmptyWithIsNotEmpty")
if (!manager.dynamicShortcuts.isEmpty()) {
return
}
val intent = Intent(Intent.ACTION_VIEW).apply {
component = ComponentName(this@App, ReadMeActivity::class.java)
}
val shortcut = ShortcutInfo.Builder(this, "readme")
.setShortLabel("README")
.setIntent(intent)
.build()
manager.addDynamicShortcuts(mutableListOf(shortcut))
}
companion object {
lateinit var executorService: ExecutorService
lateinit var sampleManager: SampleManager
}
}

View File

@ -1,25 +0,0 @@
package io.noties.markwon.app.readme
import android.net.Uri
import android.text.TextUtils
import io.noties.markwon.image.destination.ImageDestinationProcessor
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
class GithubImageDestinationProcessor(
username: String = "noties",
repository: String = "Markwon",
branch: String = "master"
) : ImageDestinationProcessor() {
private val processor = ImageDestinationProcessorRelativeToAbsolute("https://github.com/$username/$repository/raw/$branch/")
override fun process(destination: String): String {
// process only images without scheme information
val uri = Uri.parse(destination)
return if (TextUtils.isEmpty(uri.scheme)) {
processor.process(destination)
} else {
destination
}
}
}

View File

@ -1,178 +0,0 @@
package io.noties.markwon.app.readme
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.noties.debug.Debug
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.app.R
import io.noties.markwon.app.utils.ReadMeUtils
import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.loadReadMe
import io.noties.markwon.app.utils.textOrHide
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.recycler.MarkwonAdapter
import io.noties.markwon.recycler.SimpleEntry
import io.noties.markwon.recycler.table.TableEntry
import io.noties.markwon.recycler.table.TableEntryPlugin
import io.noties.markwon.syntax.Prism4jThemeDefault
import io.noties.markwon.syntax.SyntaxHighlightPlugin
import io.noties.prism4j.Prism4j
import io.noties.prism4j.annotations.PrismBundle
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.commonmark.ext.gfm.tables.TableBlock
import org.commonmark.node.FencedCodeBlock
import java.io.IOException
@PrismBundle(includeAll = true)
class ReadMeActivity : Activity() {
private lateinit var progressBar: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_read_me)
progressBar = findViewById(R.id.progress_bar)
val data = intent.data
Debug.i(data)
initAppBar(data)
initRecyclerView(data)
}
private val markwon: Markwon
get() = Markwon.builder(this)
.usePlugin(ImagesPlugin.create())
.usePlugin(HtmlPlugin.create())
.usePlugin(TableEntryPlugin.create(this))
.usePlugin(SyntaxHighlightPlugin.create(Prism4j(GrammarLocatorDef()), Prism4jThemeDefault.create(0)))
.usePlugin(TaskListPlugin.create(this))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(ReadMeImageDestinationPlugin(intent.data))
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder.on(FencedCodeBlock::class.java) { visitor, block ->
// we actually won't be applying code spans here, as our custom view will
// draw background and apply mono typeface
//
// NB the `trim` operation on literal (as code will have a new line at the end)
val code = visitor.configuration()
.syntaxHighlight()
.highlight(block.info, block.literal.trim())
visitor.builder().append(code)
}
}
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver(ReadMeLinkResolver())
}
})
.build()
private fun initAppBar(data: Uri?) {
val appBar = findViewById<View>(R.id.app_bar)
appBar.findViewById<View>(R.id.app_bar_icon).setOnClickListener { onBackPressed() }
val (title: String, subtitle: String?) = if (data == null) {
Pair("README.md", null)
} else {
Pair(data.lastPathSegment ?: "", data.toString())
}
appBar.findViewById<TextView>(R.id.title).text = title
appBar.findViewById<TextView>(R.id.subtitle).textOrHide(subtitle)
}
private fun initRecyclerView(data: Uri?) {
val adapter = MarkwonAdapter.builder(R.layout.adapter_node, R.id.text_view)
.include(FencedCodeBlock::class.java, SimpleEntry.create(R.layout.adapter_node_code_block, R.id.text_view))
.include(TableBlock::class.java, TableEntry.create {
it
.tableLayout(R.layout.adapter_node_table_block, R.id.table_layout)
.textLayoutIsRoot(R.layout.view_table_entry_cell)
})
.build()
val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.adapter = adapter
load(applicationContext, data) { result ->
when (result) {
is Result.Failure -> Debug.e(result.throwable)
is Result.Success -> {
val markwon = markwon
val node = markwon.parse(result.markdown)
if (window != null) {
recyclerView.post {
adapter.setParsedMarkdown(markwon, node)
adapter.notifyDataSetChanged()
progressBar.hidden = true
}
}
}
}
}
}
private sealed class Result {
data class Success(val markdown: String) : Result()
data class Failure(val throwable: Throwable) : Result()
}
companion object {
fun makeIntent(context: Context): Intent {
return Intent(context, ReadMeActivity::class.java)
}
private fun load(context: Context, data: Uri?, callback: (Result) -> Unit) = try {
if (data == null) {
callback.invoke(Result.Success(loadReadMe(context)))
} else {
val request = Request.Builder()
.get()
.url(ReadMeUtils.buildRawGithubUrl(data))
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback.invoke(Result.Failure(e))
}
override fun onResponse(call: Call, response: Response) {
val md = response.body()?.string() ?: ""
callback.invoke(Result.Success(md))
}
})
}
} catch (t: Throwable) {
callback.invoke(Result.Failure(t))
}
}
}

View File

@ -1,21 +0,0 @@
package io.noties.markwon.app.readme
import android.net.Uri
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.app.utils.ReadMeUtils
class ReadMeImageDestinationPlugin(private val data: Uri?) : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
val info = ReadMeUtils.parseInfo(data)
if (info == null) {
builder.imageDestinationProcessor(GithubImageDestinationProcessor())
} else {
builder.imageDestinationProcessor(GithubImageDestinationProcessor(
username = info.username,
repository = info.repository,
branch = info.branch
))
}
}
}

View File

@ -1,18 +0,0 @@
package io.noties.markwon.app.readme
import android.view.View
import io.noties.markwon.LinkResolverDef
import io.noties.markwon.app.utils.ReadMeUtils
class ReadMeLinkResolver : LinkResolverDef() {
override fun resolve(view: View, link: String) {
val info = ReadMeUtils.parseRepository(link)
val url = if (info != null) {
ReadMeUtils.buildRepositoryReadMeUrl(info.first, info.second)
} else {
link
}
super.resolve(view, url)
}
}

View File

@ -1,106 +0,0 @@
package io.noties.markwon.app.sample
import android.net.Uri
import io.noties.debug.Debug
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.sample.annotations.MarkwonArtifact
sealed class Deeplink {
data class Sample(val id: String) : Deeplink()
data class Search(val search: SampleSearch) : Deeplink()
companion object {
fun parse(data: Uri?): Deeplink? {
Debug.i(data)
@Suppress("NAME_SHADOWING")
val data = data ?: return null
return when (data.scheme) {
// local deeplink with custom scheme (`markwon://`)
BuildConfig.DEEPLINK_SCHEME -> {
when (data.host) {
"sample" -> parseSample(data.lastPathSegment)
"search" -> parseSearch(data.query)
else -> null
}
}
// https deeplink, `https://noties.io/Markwon/sample`
"https" -> {
// https://noties.io/Markwon/app/sample/ID
// https://noties.io/Markwon/app/search?a=core
val segments = data.pathSegments
if (segments.size >= 3
&& "Markwon" == segments[0]
&& "app" == segments[1]) {
when (segments[2]) {
"sample" -> parseSample(data.lastPathSegment)
"search" -> parseSearch(data.query)
else -> null
}
} else {
null
}
}
else -> null
}.also {
Debug.i("parsed: $it")
}
}
private fun parseSample(id: String?): Sample? {
if (id == null) return null
return Sample(id)
}
private fun parseSearch(query: String?): Search? {
Debug.i("query: '$query'")
val params = query
?.let {
// `https:.*` has query with `search?a=core`
val index = it.indexOf('?')
if (index > -1) {
it.substring(index + 1)
} else {
it
}
}
?.split("&")
?.map {
val (k, v) = it.split("=")
Pair(k, v)
}
?.toMap()
?: return null
Debug.i("params: $params")
val artifact = params["a"]
val tag = params["t"]
val search = params["q"]
Debug.i("artifact: '$artifact', tag: '$tag', search: '$search'")
val sampleSearch: SampleSearch? = if (artifact != null) {
val encodedArtifact = MarkwonArtifact.values()
.firstOrNull { it.artifactName() == artifact }
if (encodedArtifact != null) {
SampleSearch.Artifact(search, encodedArtifact)
} else {
null
}
} else if (tag != null) {
SampleSearch.Tag(search, tag)
} else if (search != null) {
SampleSearch.All(search)
} else {
null
}
if (sampleSearch == null) {
return null
}
return Search(sampleSearch)
}
}
}

View File

@ -1,42 +0,0 @@
package io.noties.markwon.app.sample
import android.os.Bundle
import android.view.Window
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import io.noties.debug.Debug
import io.noties.markwon.app.App
import io.noties.markwon.app.sample.ui.SampleFragment
import io.noties.markwon.app.sample.ui.SampleListFragment
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(Window.ID_ANDROID_CONTENT) == null) {
supportFragmentManager.beginTransaction()
.add(Window.ID_ANDROID_CONTENT, SampleListFragment.init())
.commitNowAllowingStateLoss()
// process deeplink if we are not restored
val deeplink = Deeplink.parse(intent.data)
val deepLinkFragment: Fragment? = if (deeplink != null) {
when (deeplink) {
is Deeplink.Sample -> App.sampleManager.sample(deeplink.id)
?.let { SampleFragment.init(it) }
is Deeplink.Search -> SampleListFragment.init(deeplink.search)
}
} else null
if (deepLinkFragment != null) {
supportFragmentManager.beginTransaction()
.replace(Window.ID_ANDROID_CONTENT, deepLinkFragment)
.addToBackStack(null)
.commit()
}
}
}
}

View File

@ -1,22 +0,0 @@
package io.noties.markwon.app.sample
import android.os.Parcelable
import io.noties.markwon.sample.annotations.MarkwonArtifact
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Sample(
val javaClassName: String,
val id: String,
val title: String,
val description: String,
val artifacts: List<MarkwonArtifact>,
val tags: List<String>
) : Parcelable {
enum class Language {
JAVA, KOTLIN
}
data class Code(val language: Language, val sourceCode: String)
}

View File

@ -1,75 +0,0 @@
package io.noties.markwon.app.sample
import android.content.Context
import io.noties.markwon.app.utils.Cancellable
import io.noties.markwon.app.utils.SampleUtils
import io.noties.markwon.sample.annotations.MarkwonArtifact
import java.util.concurrent.ExecutorService
class SampleManager(
private val context: Context,
private val executorService: ExecutorService
) {
private val samples: List<Sample> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
SampleUtils.readSamples(context)
}
fun sample(id: String): Sample? {
return samples.firstOrNull { id == it.id }
}
fun samples(search: SampleSearch?, callback: (List<Sample>) -> Unit): Cancellable {
var action: ((List<Sample>) -> Unit)? = callback
val future = executorService.submit {
val source = when (search) {
is SampleSearch.Artifact -> samples.filter { it.artifacts.contains(search.artifact) }
is SampleSearch.Tag -> samples.filter { it.tags.contains(search.tag) }
else -> samples.toList() // just copy all
}
val text = search?.text
val results = if (text == null) {
// no further filtering, just return the full source here
source
} else {
source.filter { filter(it, text) }
}
action?.invoke(results)
}
return object : Cancellable {
override val isCancelled: Boolean
get() = future.isDone
override fun cancel() {
action = null
future.cancel(true)
}
}
}
// if title contains,
// if description contains,
// if tags contains
// if artifacts contains,
private fun filter(sample: Sample, text: String): Boolean {
return sample.javaClassName.contains(text, true)
|| sample.title.contains(text, true)
|| sample.description.contains(text, true)
|| filterTags(sample.tags, text)
|| filterArtifacts(sample.artifacts, text)
}
private fun filterTags(tags: List<String>, text: String): Boolean {
return tags.firstOrNull { it.contains(text, true) } != null
}
private fun filterArtifacts(artifacts: List<MarkwonArtifact>, text: String): Boolean {
return artifacts.firstOrNull { it.artifactName().contains(text, true) } != null
}
}

View File

@ -1,13 +0,0 @@
package io.noties.markwon.app.sample
import io.noties.markwon.sample.annotations.MarkwonArtifact
sealed class SampleSearch(val text: String?) {
class Artifact(text: String?, val artifact: MarkwonArtifact) : SampleSearch(text)
class Tag(text: String?, val tag: String) : SampleSearch(text)
class All(text: String?) : SampleSearch(text)
override fun toString(): String {
return "SampleSearch(text=$text,type=${javaClass.simpleName})"
}
}

View File

@ -1,23 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import io.noties.markwon.app.R
abstract class MarkwonRecyclerViewSample : MarkwonSample() {
protected lateinit var context: Context
protected lateinit var recyclerView: RecyclerView
override fun onViewCreated(view: View) {
context = view.context
recyclerView = view.findViewById(R.id.recycler_view)
render()
}
override val layoutResId: Int
get() = R.layout.sample_recycler_view
abstract fun render()
}

View File

@ -1,16 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
abstract class MarkwonSample {
fun createView(inflater: LayoutInflater, container: ViewGroup?): View {
return inflater.inflate(layoutResId, container, false)
}
abstract fun onViewCreated(view: View)
protected abstract val layoutResId: Int
}

View File

@ -1,26 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.content.Context
import android.view.View
import android.widget.ScrollView
import android.widget.TextView
import io.noties.markwon.app.R
abstract class MarkwonTextViewSample : MarkwonSample() {
protected lateinit var context: Context
protected lateinit var scrollView: ScrollView
protected lateinit var textView: TextView
override val layoutResId: Int = R.layout.sample_text_view
override fun onViewCreated(view: View) {
context = view.context
scrollView = view.findViewById(R.id.scroll_view)
textView = view.findViewById(R.id.text_view)
render()
}
abstract fun render()
}

View File

@ -1,75 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import io.noties.markwon.app.App
import io.noties.markwon.app.R
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.readCode
import io.noties.markwon.syntax.Prism4jSyntaxHighlight
import io.noties.markwon.syntax.Prism4jThemeDefault
import io.noties.prism4j.Prism4j
import io.noties.prism4j.annotations.PrismBundle
@PrismBundle(include = ["java", "kotlin"], grammarLocatorClassName = ".GrammarLocatorSourceCode")
class SampleCodeFragment : Fragment() {
private lateinit var progressBar: View
private lateinit var textView: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_sample_code, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
progressBar = view.findViewById(R.id.progress_bar)
textView = view.findViewById(R.id.text_view)
load()
}
private fun load() {
App.executorService.submit {
val code = sample.readCode(requireContext())
val prism = Prism4j(GrammarLocatorSourceCode())
val highlight = Prism4jSyntaxHighlight.create(prism, Prism4jThemeDefault.create(0))
val language = when (code.language) {
Sample.Language.KOTLIN -> "kotlin"
Sample.Language.JAVA -> "java"
}
val text = highlight.highlight(language, code.sourceCode)
textView.post {
//attached
if (context != null) {
progressBar.hidden = true
textView.text = text
}
}
}
}
private val sample: Sample by lazy(LazyThreadSafetyMode.NONE) {
val temp: Sample = (arguments!!.getParcelable(ARG_SAMPLE))!!
temp
}
companion object {
private const val ARG_SAMPLE = "arg.Sample"
fun init(sample: Sample): SampleCodeFragment {
return SampleCodeFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_SAMPLE, sample)
}
}
}
}
}

View File

@ -1,135 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import io.noties.markwon.app.R
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.utils.active
class SampleFragment : Fragment() {
private lateinit var container: ViewGroup
private var isCodeSelected = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_sample, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container = view.findViewById(R.id.container)
isCodeSelected = savedInstanceState?.getBoolean(KEY_CODE_SELECTED) ?: false
initAppBar(view)
initTabBar(view)
}
private fun initAppBar(view: View) {
val appBar: View = view.findViewById(R.id.app_bar)
val icon: View = appBar.findViewById(R.id.app_bar_icon)
val title: TextView = appBar.findViewById(R.id.app_bar_title)
icon.setOnClickListener {
activity?.onBackPressed()
}
title.text = sample.title
}
private fun initTabBar(view: View) {
val tabBar: View = view.findViewById(R.id.tab_bar)
val preview: View = tabBar.findViewById(R.id.tab_bar_preview)
val code: View = tabBar.findViewById(R.id.tab_bar_code)
preview.setOnClickListener {
if (!it.active) {
it.active = true
code.active = false
showPreview()
}
}
code.setOnClickListener {
if (!it.active) {
it.active = true
preview.active = false
showCode()
}
}
// maybe check state (in case of restoration)
// initial values
preview.active = !isCodeSelected
code.active = isCodeSelected
if (isCodeSelected) {
showCode()
} else {
showPreview()
}
}
private fun showPreview() {
isCodeSelected = false
showFragment(TAG_PREVIEW, TAG_CODE) { SamplePreviewFragment.init(sample) }
}
private fun showCode() {
isCodeSelected = true
showFragment(TAG_CODE, TAG_PREVIEW) { SampleCodeFragment.init(sample) }
}
private fun showFragment(showTag: String, hideTag: String, provider: () -> Fragment) {
val manager = childFragmentManager
manager.beginTransaction().apply {
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
val existing = manager.findFragmentByTag(showTag)
if (existing != null) {
show(existing)
} else {
add(container.id, provider(), showTag)
}
manager.findFragmentByTag(hideTag)?.also {
hide(it)
}
commitAllowingStateLoss()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_CODE_SELECTED, isCodeSelected)
}
private val sample: Sample by lazy(LazyThreadSafetyMode.NONE) {
val temp: Sample = (arguments!!.getParcelable(ARG_SAMPLE))!!
temp
}
companion object {
private const val ARG_SAMPLE = "arg.Sample"
private const val TAG_PREVIEW = "tag.Preview"
private const val TAG_CODE = "tag.Code"
private const val KEY_CODE_SELECTED = "key.Selected"
fun init(sample: Sample): SampleFragment {
val fragment = SampleFragment()
fragment.arguments = Bundle().apply {
putParcelable(ARG_SAMPLE, sample)
}
return fragment
}
}
}

View File

@ -1,462 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.noties.adapt.Adapt
import io.noties.adapt.DiffUtilDataSetChanged
import io.noties.adapt.Item
import io.noties.debug.Debug
import io.noties.markwon.Markwon
import io.noties.markwon.app.App
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.R
import io.noties.markwon.app.readme.ReadMeActivity
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.sample.SampleManager
import io.noties.markwon.app.sample.SampleSearch
import io.noties.markwon.app.sample.ui.adapt.CheckForUpdateItem
import io.noties.markwon.app.sample.ui.adapt.SampleItem
import io.noties.markwon.app.sample.ui.adapt.VersionItem
import io.noties.markwon.app.utils.Cancellable
import io.noties.markwon.app.utils.UpdateUtils
import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.onPreDraw
import io.noties.markwon.app.utils.recyclerView
import io.noties.markwon.app.utils.stackTraceString
import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.app.widget.SearchBar
import io.noties.markwon.movement.MovementMethodPlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import kotlinx.android.parcel.Parcelize
class SampleListFragment : Fragment() {
private val adapt: Adapt = Adapt.create(DiffUtilDataSetChanged.create())
private lateinit var markwon: Markwon
private val type: Type by lazy(LazyThreadSafetyMode.NONE) {
parseType(arguments!!)
}
private var search: String? = null
// postpone state restoration
private var pendingRecyclerScrollPosition: RecyclerScrollPosition? = null
private var cancellable: Cancellable? = null
private var checkForUpdateCancellable: Cancellable? = null
private lateinit var progressBar: View
private val versionItem: VersionItem by lazy(LazyThreadSafetyMode.NONE) {
VersionItem()
}
private val sampleManager: SampleManager
get() = App.sampleManager
override fun onAttach(context: Context) {
super.onAttach(context)
context.also {
markwon = markwon(it)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_sample_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAppBar(view)
val context = requireContext()
progressBar = view.findViewById(R.id.progress_bar)
val searchBar: SearchBar = view.findViewById(R.id.search_bar)
searchBar.onSearchListener = {
search = it
fetch()
}
val recyclerView: RecyclerView = view.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.setHasFixedSize(true)
recyclerView.adapter = adapt
// // additional padding for RecyclerView
// greatly complicates state restoration (items jump and a lot of times state cannot be
// even restored (layout manager scrolls to top item and that's it)
// searchBar.onPreDraw {
// recyclerView.setPadding(
// recyclerView.paddingLeft,
// recyclerView.paddingTop + searchBar.height,
// recyclerView.paddingRight,
// recyclerView.paddingBottom
// )
// }
val state: State? = arguments?.getParcelable(STATE)
val initialSearch = arguments?.getString(ARG_SEARCH)
// clear it anyway
arguments?.remove(ARG_SEARCH)
Debug.i(state, initialSearch)
pendingRecyclerScrollPosition = state?.recyclerScrollPosition
val search = listOf(state?.search, initialSearch)
.firstOrNull { it != null }
if (search != null) {
searchBar.search(search)
} else {
fetch()
}
}
override fun onDestroyView() {
val state = State(
search,
adapt.recyclerView?.scrollPosition
)
Debug.i(state)
arguments?.putParcelable(STATE, state)
val cancellable = this.cancellable
if (cancellable != null && !cancellable.isCancelled) {
cancellable.cancel()
this.cancellable = null
}
super.onDestroyView()
}
// not called? yeah, whatever
// override fun onSaveInstanceState(outState: Bundle) {
// super.onSaveInstanceState(outState)
//
// val state = State(
// search,
// adapt.recyclerView?.scrollPosition
// )
// Debug.i(state)
// outState.putParcelable(STATE, state)
// }
private fun initAppBar(view: View) {
val appBar = view.findViewById<View>(R.id.app_bar)
val appBarIcon: ImageView = appBar.findViewById(R.id.app_bar_icon)
val appBarTitle: TextView = appBar.findViewById(R.id.app_bar_title)
val appBarIconReadme: ImageView = appBar.findViewById(R.id.app_bar_icon_readme)
val isInitialScreen = fragmentManager?.backStackEntryCount == 0
appBarIcon.hidden = isInitialScreen
appBarIconReadme.hidden = !isInitialScreen
val type = this.type
val (text, background) = when (type) {
is Type.Artifact -> Pair(type.artifact.displayName, R.drawable.bg_artifact)
is Type.Tag -> Pair(type.tag.tagDisplayName, R.drawable.bg_tag)
is Type.All -> Pair(resources.getString(R.string.app_name), 0)
}
appBarTitle.text = text
if (background != 0) {
appBarTitle.setBackgroundResource(background)
}
if (isInitialScreen) {
appBarIconReadme.setOnClickListener {
context?.let {
val intent = ReadMeActivity.makeIntent(it)
it.startActivity(intent)
}
}
} else {
appBarIcon.setImageResource(R.drawable.ic_arrow_back_white_24dp)
appBarIcon.setOnClickListener {
requireActivity().onBackPressed()
}
}
}
private fun bindSamples(samples: List<Sample>, addVersion: Boolean) {
val items: List<Item<*>> = samples
.map {
SampleItem(
markwon,
it,
{ artifact -> openArtifact(artifact) },
{ tag -> openTag(tag) },
{ sample -> openSample(sample) }
)
}
.let {
if (addVersion) {
val list: List<Item<*>> = it
list.toMutableList().apply {
add(0, CheckForUpdateItem(this@SampleListFragment::checkForUpdate))
add(0, versionItem)
}
} else {
it
}
}
adapt.setItems(items)
val recyclerView = adapt.recyclerView ?: return
val scrollPosition = pendingRecyclerScrollPosition
Debug.i(scrollPosition)
if (scrollPosition != null) {
pendingRecyclerScrollPosition = null
recyclerView.onPreDraw {
(recyclerView.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset)
}
} else {
recyclerView.onPreDraw {
recyclerView.scrollToPosition(0)
}
}
}
private fun checkForUpdate() {
val current = checkForUpdateCancellable
if (current != null && !current.isCancelled) {
return
}
progressBar.hidden = false
checkForUpdateCancellable = UpdateUtils.checkForUpdate { result ->
progressBar.post {
processUpdateResult(result)
}
}
}
private fun processUpdateResult(result: UpdateUtils.Result) {
val context = context ?: return
progressBar.hidden = true
val builder = AlertDialog.Builder(context)
when (result) {
is UpdateUtils.Result.UpdateAvailable -> {
val md = """
## Update available
${BuildConfig.GIT_SHA} -&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

@ -1,38 +0,0 @@
package io.noties.markwon.app.sample.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import io.noties.markwon.app.sample.Sample
class SamplePreviewFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return markwonSample.createView(inflater, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
markwonSample.onViewCreated(view)
}
private val markwonSample: MarkwonSample by lazy(LazyThreadSafetyMode.NONE) {
val sample: Sample = arguments!!.getParcelable<Sample>(ARG_SAMPLE)!!
Class.forName(sample.javaClassName).newInstance() as MarkwonSample
}
companion object {
private const val ARG_SAMPLE = "arg.Sample"
fun init(sample: Sample): SamplePreviewFragment {
return SamplePreviewFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_SAMPLE, sample)
}
}
}
}
}

View File

@ -1,22 +0,0 @@
package io.noties.markwon.app.sample.ui.adapt
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.noties.adapt.Item
import io.noties.markwon.app.R
class CheckForUpdateItem(private val action: () -> Unit) : Item<CheckForUpdateItem.Holder>(43L) {
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.adapt_check_for_update, parent, false))
}
override fun render(holder: Holder) {
holder.button.setOnClickListener { action() }
}
class Holder(view: View) : Item.Holder(view) {
val button: View = requireView(R.id.button)
}
}

View File

@ -1,115 +0,0 @@
package io.noties.markwon.app.sample.ui.adapt
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.noties.adapt.Item
import io.noties.markwon.Markwon
import io.noties.markwon.app.R
import io.noties.markwon.app.sample.Sample
import io.noties.markwon.app.utils.displayName
import io.noties.markwon.app.utils.hidden
import io.noties.markwon.app.utils.tagDisplayName
import io.noties.markwon.app.widget.FlowLayout
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.utils.NoCopySpannableFactory
class SampleItem(
private val markwon: Markwon,
private val sample: Sample,
private val onArtifactClick: (MarkwonArtifact) -> Unit,
private val onTagClick: (String) -> Unit,
private val onSampleClick: (Sample) -> Unit
) : Item<SampleItem.Holder>(sample.id.hashCode().toLong()) {
// var search: String? = null
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
markwon.toMarkdown(sample.description)
}
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.adapt_sample, parent, false)).apply {
description.setSpannableFactory(NoCopySpannableFactory.getInstance())
}
}
override fun render(holder: Holder) {
holder.apply {
title.text = sample.title
val text = this@SampleItem.text
if (text.isEmpty()) {
description.text = ""
description.hidden = true
} else {
markwon.setParsedMarkdown(description, text)
description.hidden = false
}
// there is no need to display the core artifact (it is implicit),
// hide if empty (removed core)
artifacts.ensure(sample.artifacts.size, R.layout.view_artifact)
.zip(sample.artifacts)
.forEach { (view, artifact) ->
(view as TextView).text = artifact.displayName
view.setOnClickListener {
onArtifactClick(artifact)
}
}
tags.ensure(sample.tags.size, R.layout.view_tag)
.zip(sample.tags)
.forEach { (view, tag) ->
(view as TextView).text = tag.tagDisplayName
view.setOnClickListener {
onTagClick(tag)
}
}
itemView.setOnClickListener {
onSampleClick(sample)
}
}
}
class Holder(itemView: View) : Item.Holder(itemView) {
val title: TextView = requireView(R.id.title)
val description: TextView = requireView(R.id.description)
val artifacts: FlowLayout = requireView(R.id.artifacts)
val tags: FlowLayout = requireView(R.id.tags)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SampleItem
if (sample != other.sample) return false
return true
}
override fun hashCode(): Int {
return sample.hashCode()
}
}
private fun FlowLayout.ensure(viewsCount: Int, layoutResId: Int): List<View> {
if (viewsCount > childCount) {
// inflate new views
val inflater = LayoutInflater.from(context)
for (i in 0 until (viewsCount - childCount)) {
addView(inflater.inflate(layoutResId, this, false))
}
} else {
// return requested vies and GONE the rest
for (i in viewsCount until childCount) {
getChildAt(i).hidden = true
}
}
return (0 until viewsCount).map { getChildAt(it) }
}

View File

@ -1,84 +0,0 @@
package io.noties.markwon.app.sample.ui.adapt
import android.content.Context
import android.text.Spanned
import android.text.TextPaint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.noties.adapt.Item
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.LinkResolver
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.R
import io.noties.markwon.core.CoreProps
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.core.spans.LinkSpan
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.movement.MovementMethodPlugin
import org.commonmark.node.Link
class VersionItem : Item<VersionItem.Holder>(42L) {
private lateinit var context: Context
private val markwon: Markwon by lazy(LazyThreadSafetyMode.NONE) {
Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(MovementMethodPlugin.link())
.usePlugin(HtmlPlugin.create())
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.setFactory(Link::class.java) { configuration, props ->
LinkSpanNoUnderline(
configuration.theme(),
CoreProps.LINK_DESTINATION.require(props),
configuration.linkResolver()
)
}
}
})
.build()
}
private val text: Spanned by lazy(LazyThreadSafetyMode.NONE) {
val md = """
<a href="${BuildConfig.GIT_REPOSITORY}/blob/master/CHANGELOG.md">
![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

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

View File

@ -1,51 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629125321",
title = "Additional spacing after block",
description = "Add additional spacing (padding) after last line of a block",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.spacing, Tag.padding, Tag.span}
)
public class AdditionalSpacingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text";
// please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding
final int spacing = (int) (128 * context.getResources().getDisplayMetrics().density + .5F);
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.headingBreakHeight(0);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.appendFactory(
Heading.class,
(configuration, props) -> new LastLineSpacingSpan(spacing));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,59 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629130227",
title = "All blocks no padding",
description = "Do not render new lines (padding) after all blocks",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.block, Tag.spacing, Tag.padding, Tag.rendering}
)
public class AllBlocksNoForcedNewLineSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Hello there!\n\n" +
"* a first\n" +
"* second\n" +
"- third\n" +
"* * nested one\n\n" +
"> block quote\n\n" +
"> > and nested one\n\n" +
"```java\n" +
"final int i = 0;\n" +
"```\n\n";
// extend default block handler
final MarkwonVisitor.BlockHandler blockHandler = new BlockHandlerDef() {
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
}
}
};
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(blockHandler);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,66 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200729090524",
title = "Block handler",
description = "Custom block delimiters that control new lines after block nodes",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class BlockHandlerSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"* one\n" +
"* two\n" +
"* three\n" +
"---\n" +
"> a quote\n\n" +
"```\n" +
"code\n" +
"```\n" +
"some text after";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerNoAdditionalNewLines());
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class BlockHandlerNoAdditionalNewLines implements MarkwonVisitor.BlockHandler {
@Override
public void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
// ensure that content rendered on a new line
visitor.ensureNewLine();
}
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// by default markwon here also has:
// visitor.forceNewLine();
}
}
}

View File

@ -1,59 +0,0 @@
package io.noties.markwon.app.samples
import android.content.Context
import io.noties.debug.Debug
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
import java.util.Collections
import java.util.WeakHashMap
@MarkwonSampleInfo(
id = "20200707102458",
title = "Cache Markwon instance",
description = "A static cache for `Markwon` instance " +
"to be associated with a `Context`",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.cache]
)
class CacheMarkwonSample : MarkwonTextViewSample() {
override fun render() {
render("# First!")
render("## Second!!")
render("### Third!!!")
}
fun render(md: String) {
val markwon = MarkwonCache.with(context)
Debug.i("markwon: ${markwon.hashCode()}, md: '$md'")
markwon.setMarkdown(textView, md)
}
}
object MarkwonCache {
private val cache = Collections.synchronizedMap(WeakHashMap<Context, Markwon>())
fun with(context: Context): Markwon {
// yeah, why work as expected? new value is returned each time, no caching occur
// kotlin: 1.3.72
// intellij plugin: 1.3.72-release-Studio4.0-5
// return cache.getOrPut(context) {
// // create your markwon instance here
// return Markwon.builder(context)
// .usePlugin(StrikethroughPlugin.create())
// .build()
// }
return cache[context] ?: {
Markwon.builder(context)
.usePlugin(StrikethroughPlugin.create())
.build()
.also {
cache[context] = it
}
}.invoke()
}
}

View File

@ -1,66 +0,0 @@
package io.noties.markwon.app.samples;
import android.text.style.BulletSpan;
import androidx.annotation.NonNull;
import org.commonmark.node.ListItem;
import io.noties.debug.Debug;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20201208150530",
title = "Change bullet span",
description = "Use a different span implementation to render bullet lists",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.rendering, Tag.spanFactory, Tag.span}
)
public class ChangeBulletSpanSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"* one\n" +
"* two\n" +
"* three\n" +
"* * four\n" +
" * five\n\n" +
"- [ ] and task?\n" +
"- [x] it is";
final Markwon markwon = Markwon.builder(context)
.usePlugin(TaskListPlugin.create(context))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// store original span factory (provides both bullet and ordered lists)
final SpanFactory original = builder.getFactory(ListItem.class);
builder.setFactory(ListItem.class, (configuration, props) -> {
if (CoreProps.LIST_ITEM_TYPE.require(props) == CoreProps.ListItemType.BULLET) {
// additionally inspect bullet level
final int level = CoreProps.BULLET_LIST_ITEM_LEVEL.require(props);
Debug.i("rendering bullet list with level: %d", level);
// return _system_ bullet span, but can be any
return new BulletSpan();
}
return original != null ? original.getSpans(configuration, props) : null;
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,119 +0,0 @@
package io.noties.markwon.app.samples
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Layout
import android.text.Spanned
import android.text.TextPaint
import android.text.style.ClickableSpan
import android.text.style.LeadingMarginSpan
import android.view.View
import android.widget.TextView
import io.noties.debug.Debug
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.app.R
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
import io.noties.markwon.utils.LeadingMarginUtils
import org.commonmark.node.FencedCodeBlock
@MarkwonSampleInfo(
id = "20210315112847",
title = "Copy code block",
description = "Copy contents of fenced code blocks",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.rendering, Tag.block, Tag.spanFactory, Tag.span]
)
class CopyCodeBlockSample : MarkwonTextViewSample() {
override fun render() {
val md = """
# Hello code blocks!
```java
final int i = 0;
final Type t = Type.init()
.filter(i -> i.even)
.first(null);
```
bye bye!
""".trimIndent()
val markwon = Markwon.builder(context)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder.appendFactory(FencedCodeBlock::class.java) { _, _ ->
CopyContentsSpan()
}
builder.appendFactory(FencedCodeBlock::class.java) { _, _ ->
CopyIconSpan(context.getDrawable(R.drawable.ic_code_white_24dp)!!)
}
}
})
.build()
markwon.setMarkdown(textView, md)
}
class CopyContentsSpan : ClickableSpan() {
override fun onClick(widget: View) {
val spanned = (widget as? TextView)?.text as? Spanned ?: return
val start = spanned.getSpanStart(this)
val end = spanned.getSpanEnd(this)
// by default code blocks have new lines before and after content
val contents = spanned.subSequence(start, end).toString().trim()
// copy code here
Debug.i(contents)
}
override fun updateDrawState(ds: TextPaint) {
// do not apply link styling
}
}
class CopyIconSpan(val icon: Drawable) : LeadingMarginSpan {
init {
if (icon.bounds.isEmpty) {
icon.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight)
}
}
override fun getLeadingMargin(first: Boolean): Int = 0
override fun drawLeadingMargin(
c: Canvas,
p: Paint,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
first: Boolean,
layout: Layout
) {
// called for each line of text, we are interested only in first one
if (!LeadingMarginUtils.selfStart(start, text, this)) return
val save = c.save()
try {
// horizontal position for icon
val w = icon.bounds.width().toFloat()
// minus quarter width as padding
val left = layout.width - w - (w / 4F)
c.translate(left, top.toFloat())
icon.draw(c)
} finally {
c.restoreToCount(save)
}
}
}
}

View File

@ -1,436 +0,0 @@
package io.noties.markwon.app.samples;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.style.ReplacementSpan;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Delimited;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.commonmark.parser.Parser;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629163248",
title = "Custom extension",
description = "Custom extension that adds an " +
"icon from resources and renders it as image with " +
"`@ic-name` syntax",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.parsing, Tag.rendering, Tag.plugin, Tag.image, Tag.extension, Tag.span}
)
public class CustomExtensionSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Hello! @ic-android-black-24\n\n" +
"" +
"Home 36 black: @ic-home-black-36\n\n" +
"" +
"Memory 48 black: @ic-memory-black-48\n\n" +
"" +
"### I AM ANOTHER HEADER\n\n" +
"" +
"Sentiment Satisfied 64 red: @ic-sentiment_satisfied-red-64" +
"";
// note that we haven't registered CorePlugin, as it's the only one that can be
// implicitly deducted and added automatically. All other plugins require explicit
// `usePlugin` call
final Markwon markwon = Markwon.builder(context)
.usePlugin(IconPlugin.create(IconSpanProvider.create(context, 0)))
.build();
markwon.setMarkdown(textView, md);
}
}
class IconPlugin extends AbstractMarkwonPlugin {
@NonNull
public static IconPlugin create(@NonNull IconSpanProvider iconSpanProvider) {
return new IconPlugin(iconSpanProvider);
}
private final IconSpanProvider iconSpanProvider;
IconPlugin(@NonNull IconSpanProvider iconSpanProvider) {
this.iconSpanProvider = iconSpanProvider;
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(IconProcessor.create());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(IconNode.class, (visitor, iconNode) -> {
final String name = iconNode.name();
final String color = iconNode.color();
final String size = iconNode.size();
if (!TextUtils.isEmpty(name)
&& !TextUtils.isEmpty(color)
&& !TextUtils.isEmpty(size)) {
final int length = visitor.length();
visitor.builder().append(name);
visitor.setSpans(length, iconSpanProvider.provide(name, color, size));
visitor.builder().append(' ');
}
});
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return IconProcessor.prepare(markdown);
}
}
abstract class IconSpanProvider {
@SuppressWarnings("SameParameterValue")
@NonNull
public static IconSpanProvider create(@NonNull Context context, @DrawableRes int fallBack) {
return new Impl(context, fallBack);
}
@NonNull
public abstract IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size);
private static class Impl extends IconSpanProvider {
private final Context context;
private final Resources resources;
private final int fallBack;
Impl(@NonNull Context context, @DrawableRes int fallBack) {
this.context = context;
this.resources = context.getResources();
this.fallBack = fallBack;
}
@NonNull
@Override
public IconSpan provide(@NonNull String name, @NonNull String color, @NonNull String size) {
final String resName = iconName(name, color, size);
int resId = resources.getIdentifier(resName, "drawable", context.getPackageName());
if (resId == 0) {
resId = fallBack;
}
return new IconSpan(getDrawable(resId), IconSpan.ALIGN_CENTER);
}
@NonNull
private static String iconName(@NonNull String name, @NonNull String color, @NonNull String size) {
return "ic_" + name + "_" + color + "_" + size + "dp";
}
@NonNull
private Drawable getDrawable(int resId) {
//noinspection ConstantConditions
return context.getDrawable(resId);
}
}
}
class IconSpan extends ReplacementSpan {
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER})
@Retention(RetentionPolicy.CLASS)
@interface Alignment {
}
public static final int ALIGN_BOTTOM = 0;
public static final int ALIGN_BASELINE = 1;
public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height
private final Drawable drawable;
private final int alignment;
public IconSpan(@NonNull Drawable drawable, @Alignment int alignment) {
this.drawable = drawable;
this.alignment = alignment;
if (drawable.getBounds().isEmpty()) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
final Rect rect = drawable.getBounds();
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
return rect.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
final int b = bottom - drawable.getBounds().bottom;
final int save = canvas.save();
try {
final int translationY;
if (ALIGN_CENTER == alignment) {
translationY = b - ((bottom - top - drawable.getBounds().height()) / 2);
} else if (ALIGN_BASELINE == alignment) {
translationY = b - paint.getFontMetricsInt().descent;
} else {
translationY = b;
}
canvas.translate(x, translationY);
drawable.draw(canvas);
} finally {
canvas.restoreToCount(save);
}
}
}
class IconProcessor implements DelimiterProcessor {
@NonNull
public static IconProcessor create() {
return new IconProcessor();
}
// ic-home-black-24
private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)");
private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-";
/**
* Should be used when input string does not wrap icon definition with `@` from both ends.
* So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier
* and more predictable (cannot specify multiple ending delimiters, as we would require them:
* space, newline, end of a document, and a lot of more)
*
* @param input to process
* @return processed string
* @see #prepare(StringBuilder)
*/
@NonNull
public static String prepare(@NonNull String input) {
final StringBuilder builder = new StringBuilder(input);
prepare(builder);
return builder.toString();
}
public static void prepare(@NonNull StringBuilder builder) {
int start = builder.indexOf(TO_FIND);
int end;
while (start > -1) {
end = iconDefinitionEnd(start + TO_FIND.length(), builder);
// if we match our pattern, append `@` else ignore
if (iconDefinitionValid(builder.subSequence(start + 1, end))) {
builder.insert(end, '@');
}
// move to next
start = builder.indexOf(TO_FIND, end);
}
}
@Override
public char getOpeningCharacter() {
return IconNode.DELIMITER;
}
@Override
public char getClosingCharacter() {
return IconNode.DELIMITER;
}
@Override
public int getMinLength() {
return 1;
}
@Override
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0;
}
@Override
public void process(Text opener, Text closer, int delimiterUse) {
final IconGroupNode iconGroupNode = new IconGroupNode();
final Node next = opener.getNext();
boolean handled = false;
// process only if we have exactly one Text node
if (next instanceof Text && next.getNext() == closer) {
final String text = ((Text) next).getLiteral();
if (!TextUtils.isEmpty(text)) {
// attempt to match
final Matcher matcher = PATTERN.matcher(text);
if (matcher.matches()) {
final IconNode iconNode = new IconNode(
matcher.group(1),
matcher.group(2),
matcher.group(3)
);
iconGroupNode.appendChild(iconNode);
next.unlink();
handled = true;
}
}
}
if (!handled) {
// restore delimiters if we didn't match
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
Node node;
for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
node = tmp.getNext();
// append a child anyway
iconGroupNode.appendChild(tmp);
}
iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
}
opener.insertBefore(iconGroupNode);
}
private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) {
// all spaces, new lines, non-words or digits,
char c;
int end = -1;
for (int i = index; i < builder.length(); i++) {
c = builder.charAt(i);
if (Character.isWhitespace(c)
|| !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
end = i;
break;
}
}
if (end == -1) {
end = builder.length();
}
return end;
}
private static boolean iconDefinitionValid(@NonNull CharSequence cs) {
final Matcher matcher = PATTERN.matcher(cs);
return matcher.matches();
}
}
class IconNode extends CustomNode implements Delimited {
public static final char DELIMITER = '@';
public static final String DELIMITER_STRING = "" + DELIMITER;
private final String name;
private final String color;
private final String size;
public IconNode(@NonNull String name, @NonNull String color, @NonNull String size) {
this.name = name;
this.color = color;
this.size = size;
}
@NonNull
public String name() {
return name;
}
@NonNull
public String color() {
return color;
}
@NonNull
public String size() {
return size;
}
@Override
public String getOpeningDelimiter() {
return DELIMITER_STRING;
}
@Override
public String getClosingDelimiter() {
return DELIMITER_STRING;
}
@Override
@NonNull
public String toString() {
return "IconNode{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", size='" + size + '\'' +
'}';
}
}
class IconGroupNode extends CustomNode {
}

View File

@ -1,41 +0,0 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import androidx.annotation.NonNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629123617",
title = "Customize theme",
description = "Customize `MarkwonTheme` styling",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.style, Tag.theme, Tag.plugin}
)
public class CustomizeThemeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "`A code` that is rendered differently\n\n```\nHello!\n```";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder
.codeBackgroundColor(Color.BLACK)
.codeTextColor(Color.RED);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,29 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200826122247",
title = "Deeplinks",
description = "Handling of deeplinks (app handles https scheme to deep link into content)",
artifacts = MarkwonArtifact.CORE,
tags = Tag.links
)
public class DeeplinksSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Deeplinks\n\n" +
"The [link](https://noties.io/Markwon/app/sample/20200826122247) to self";
// nothing special is required
final Markwon markwon = Markwon.builder(context)
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,83 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Emphasis;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.commonmark.parser.Parser;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200630194017",
title = "Custom delimiter processor",
description = "Custom parsing delimiter processor with `?` character",
artifacts = MarkwonArtifact.CORE,
tags = Tag.parsing
)
public class DelimiterProcessorSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"?hello? there!";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(new QuestionDelimiterProcessor());
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class QuestionDelimiterProcessor implements DelimiterProcessor {
@Override
public char getOpeningCharacter() {
return '?';
}
@Override
public char getClosingCharacter() {
return '?';
}
@Override
public int getMinLength() {
return 1;
}
@Override
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
if (opener.length() >= 1 && closer.length() >= 1) {
return 1;
}
return 0;
}
@Override
public void process(Text opener, Text closer, int delimiterUse) {
final Node node = new Emphasis();
Node tmp = opener.getNext();
while (tmp != null && tmp != closer) {
Node next = tmp.getNext();
node.appendChild(tmp);
tmp = next;
}
opener.insertAfter(node);
}
}

View File

@ -1,44 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629123308",
title = "Disable node from rendering",
description = "Disable _parsed_ node from being rendered (markdown syntax is still consumed)",
artifacts = {MarkwonArtifact.CORE},
tags = {Tag.parsing, Tag.rendering}
)
public class DisableNodeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
// for example to disable rendering of heading:
// try commenting this out to see that otherwise headings will be rendered
builder.on(Heading.class, null);
// same method can be used to override existing visitor by specifying
// a new NodeVisitor instance
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,45 +0,0 @@
package io.noties.markwon.app.samples
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.core.CorePlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
import org.commonmark.node.BlockQuote
import org.commonmark.parser.Parser
@MarkwonSampleInfo(
id = "20200627075012",
title = "Enabled markdown blocks",
description = "Modify/inspect enabled by `CorePlugin` block types. " +
"Disable quotes or other blocks from being parsed",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.parsing, Tag.block, Tag.plugin]
)
class EnabledBlockTypesSample : MarkwonTextViewSample() {
override fun render() {
val md = """
# Heading
## Second level
> Quote is not handled
""".trimIndent()
val markwon = Markwon.builder(context)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureParser(builder: Parser.Builder) {
// obtain all enabled block types
val enabledBlockTypes = CorePlugin.enabledBlockTypes()
// it is safe to modify returned collection
// remove quotes
enabledBlockTypes.remove(BlockQuote::class.java)
builder.enabledBlockTypes(enabledBlockTypes)
}
})
.build()
markwon.setMarkdown(textView, md)
}
}

View File

@ -1,75 +0,0 @@
package io.noties.markwon.app.samples
import android.text.SpannableStringBuilder
import io.noties.debug.Debug
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
import java.util.regex.Pattern
@MarkwonSampleInfo(
id = "20201111221945",
title = "Exclude part of input from parsing",
description = "Exclude part of input from parsing by splitting input with delimiters",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.parsing]
)
class ExcludeFromParsingSample : MarkwonTextViewSample() {
override fun render() {
// cannot have continuous markdown between parts (so a node started in one part and ended in other)
// with this approach
// also exclude will start a new block and won't seamlessly continue any existing markdown one (so
// if started inside a blockquote, then blockquote would be closed)
val md = """
# Hello
we are **going** to exclude some parts of this input _from_ parsing
$EXCLUDE_START
what is **good** is that we
> do not need to care about blocks or inlines
* and
* everything
* else
$EXCLUDE_END
**then** markdown _again_
and empty exclude at end: $EXCLUDE_START$EXCLUDE_END
""".trimIndent()
val markwon = Markwon.create(context)
val matcher = Pattern.compile(RE, Pattern.MULTILINE).matcher(md)
val builder by lazy(LazyThreadSafetyMode.NONE) {
SpannableStringBuilder()
}
var end: Int = 0
while (matcher.find()) {
val start = matcher.start()
Debug.i(end, start, md.substring(end, start))
builder.append(markwon.toMarkdown(md.substring(end, start)))
builder.append(matcher.group(1))
end = matcher.end()
}
if (end != md.length) {
builder.append(markwon.toMarkdown(md.substring(end)))
}
markwon.setParsedMarkdown(textView, builder)
}
private companion object {
const val EXCLUDE_START = "##IGNORE##"
const val EXCLUDE_END = "--IGNORE--"
const val RE = "${EXCLUDE_START}([\\s\\S]*?)${EXCLUDE_END}"
}
}

View File

@ -1,110 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.BuildConfig;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.InlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629162023",
title = "User mention and issue (via text)",
description = "Github-like user mention and issue " +
"rendering via `CorePlugin.OnTextAddedListener`",
artifacts = {MarkwonArtifact.CORE, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.parsing, Tag.textAddedListener, Tag.rendering}
)
public class GithubUserIssueInlineParsingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties and other @dude";
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// include all current defaults (otherwise will be empty - contain only our inline-processors)
// included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults`
// .includeDefaults()
.addInlineProcessor(new IssueInlineProcessor())
.addInlineProcessor(new UserInlineProcessor())
.build();
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.inlineParserFactory(inlineParserFactory);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class IssueInlineProcessor extends InlineProcessor {
private static final Pattern RE = Pattern.compile("\\d+");
@Override
public char specialCharacter() {
return '#';
}
@Override
protected Node parse() {
final String id = match(RE);
if (id != null) {
final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null);
link.appendChild(text("#" + id));
return link;
}
return null;
}
@NonNull
private static String createIssueOrPullRequestLinkDestination(@NonNull String id) {
return BuildConfig.GIT_REPOSITORY + "/issues/" + id;
}
}
class UserInlineProcessor extends InlineProcessor {
private static final Pattern RE = Pattern.compile("\\w+");
@Override
public char specialCharacter() {
return '@';
}
@Override
protected Node parse() {
final String user = match(RE);
if (user != null) {
final Link link = new Link(createUserLinkDestination(user), null);
link.appendChild(text("@" + user));
return link;
}
return null;
}
@NonNull
private static String createUserLinkDestination(@NonNull String user) {
return "https://github.com/" + user;
}
}

View File

@ -1,121 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.BuildConfig;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629162024",
title = "User mention and issue (via text)",
description = "Github-like user mention and issue " +
"rendering via `CorePlugin.OnTextAddedListener`",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.parsing, Tag.textAddedListener, Tag.rendering}
)
public class GithubUserIssueOnTextAddedSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Custom Extension 2\n" +
"\n" +
"This is an issue #1\n" +
"Done by @noties and other @dude";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(CorePlugin.class, corePlugin ->
corePlugin.addOnTextAddedListener(new GithubLinkifyRegexTextAddedListener()));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class GithubLinkifyRegexTextAddedListener implements CorePlugin.OnTextAddedListener {
private static final Pattern PATTERN = Pattern.compile("((#\\d+)|(@\\w+))", Pattern.MULTILINE);
@Override
public void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start) {
final Matcher matcher = PATTERN.matcher(text);
String value;
String url;
int index;
while (matcher.find()) {
value = matcher.group(1);
// detect which one it is
if ('#' == value.charAt(0)) {
url = createIssueOrPullRequestLink(value.substring(1));
} else {
url = createUserLink(value.substring(1));
}
// it's important to use `start` value (represents start-index of `text` in the visitor)
index = start + matcher.start();
setLink(visitor, url, index, index + value.length());
}
}
@NonNull
private String createIssueOrPullRequestLink(@NonNull String number) {
// issues and pull-requests on github follow the same pattern and we
// cannot know for sure which one it is, but if we use issues for all types,
// github will automatically redirect to pull-request if it's the one which is opened
return BuildConfig.GIT_REPOSITORY + "/issues/" + number;
}
@NonNull
private String createUserLink(@NonNull String user) {
return "https://github.com/" + user;
}
private void setLink(@NonNull MarkwonVisitor visitor, @NonNull String destination, int start, int end) {
// might a simpler one, but it doesn't respect possible changes to links
// visitor.builder().setSpan(
// new LinkSpan(visitor.configuration().theme(), destination, visitor.configuration().linkResolver()),
// start,
// end
// );
// use default handlers for links
final MarkwonConfiguration configuration = visitor.configuration();
final RenderProps renderProps = visitor.renderProps();
CoreProps.LINK_DESTINATION.set(renderProps, destination);
SpannableBuilder.setSpans(
visitor.builder(),
configuration.spansFactory().require(Link.class).getSpans(configuration, renderProps),
start,
end
);
}
}

View File

@ -1,58 +0,0 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20201203224611",
title = "Color of heading",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class HeadingColorSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading 1\n" +
"## Heading 2\n" +
"### Heading 3\n" +
"#### Heading 4";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.appendFactory(Heading.class, (configuration, props) -> {
// here you can also inspect heading level
final int level = CoreProps.HEADING_LEVEL.require(props);
final int color;
if (level == 1) {
color = Color.RED;
} else if (level == 2) {
color = Color.GREEN;
} else {
color = Color.BLUE;
}
return new ForegroundColorSpan(color);
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,56 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629125924",
title = "Heading no padding (block handler)",
description = "Process padding (spacing) after heading with a " +
"`BlockHandler`",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.block, Tag.spacing, Tag.padding, Tag.heading, Tag.rendering}
)
public class HeadingNoSpaceBlockHandlerSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title\n\n" +
"text text text text" +
"";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerDef() {
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (node instanceof Heading) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// ensure new line but do not force insert one
}
} else {
super.blockEnd(visitor, node);
}
}
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,64 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Heading;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629125622",
title = "Heading no padding",
description = "Do not add a new line after heading node",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.spacing, Tag.padding, Tag.spacing, Tag.rendering}
)
public class HeadingNoSpaceSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Title title title title title title title title title title" +
"\n\ntext text text text" +
"";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.headingBreakHeight(0);
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Heading.class, (visitor, heading) -> {
visitor.ensureNewLine();
final int length = visitor.length();
visitor.visitChildren(heading);
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
visitor.setSpansForNodeOptional(heading, length);
if (visitor.hasNext(heading)) {
visitor.ensureNewLine();
// by default Markwon adds a new line here
// visitor.forceNewLine();
}
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,35 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629170857",
title = "Inline parsing without defaults",
description = "Configure inline parser plugin to **not** have any **inline** parsing",
artifacts = {MarkwonArtifact.INLINE_PARSER},
tags = {Tag.parsing}
)
public class InlinePluginNoDefaultsSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"`code` inlined and **bold** here";
final Markwon markwon = Markwon.builder(context)
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
// // if anything, they can be included here
//// factoryBuilder.includeDefaults()
// }))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,73 +0,0 @@
package io.noties.markwon.app.samples;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.widget.TextView;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.image.AsyncDrawableScheduler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200826084338",
title = "Justify text",
description = "Justify text with `justificationMode` argument on Oreo (>= 26)",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class JustifyModeSample extends MarkwonTextViewSample {
@SuppressLint("WrongConstant")
@Override
public void render() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/*
nice, API 29 though
```
Error: Must be one of: LineBreaker.JUSTIFICATION_MODE_NONE, LineBreaker.JUSTIFICATION_MODE_INTER_WORD [WrongConstant]
```
*/
textView.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD);
}
final String md = "" +
"# Justify\n\n" +
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. Quisque porta ornare posuere. Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis rutrum orci at aliquet dapibus. Quisque laoreet fermentum bibendum. Suspendisse euismod nisl vel sapien viverra faucibus. Nulla vel neque volutpat, egestas dui ac, consequat elit. Donec et interdum massa. **Quisque porta ornare posuere.** Nam at ante a felis facilisis tempus eu et erat. Curabitur auctor mauris eget purus iaculis vulputate.\n\n" +
"";
if (false) {
// specify bufferType to make justificationMode argument be ignored
// Actually just calling method with BufferType argument would make
// justification gone
textView.setText(md, TextView.BufferType.SPANNABLE);
return;
}
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.build();
if (true) {
final Spanned spanned = markwon.toMarkdown(md);
// NB! the call to `setText` without arguments
textView.setText(spanned);
// if a plugin relies on `afterSetText` then we must manually call it,
// for example images are scheduled this way:
AsyncDrawableScheduler.schedule(textView);
return;
}
// cannot use that
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,188 +0,0 @@
package io.noties.markwon.app.samples;
import android.text.TextUtils;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import org.commonmark.node.BulletList;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.Prop;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.BulletListItemSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629130954",
title = "Letter ordered list",
description = "Render bullet list inside an ordered list with letters instead of bullets",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.rendering, Tag.plugin, Tag.lists}
)
public class LetterOrderedListSample extends MarkwonTextViewSample {
@Override
public void render() {
// bullet list nested in ordered list renders letters instead of bullets
final String md = "" +
"1. Hello there!\n" +
"1. And here is how:\n" +
" - First\n" +
" - Second\n" +
" - Third\n" +
" 1. And first here\n\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
.build();
markwon.setMarkdown(textView, md);
}
}
class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
private static final Prop<String> BULLET_LETTER = Prop.of("my-bullet-letter");
// or introduce some kind of synchronization if planning to use from multiple threads,
// for example via ThreadLocal
private final SparseIntArray bulletCounter = new SparseIntArray();
@Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
// clear counter after render
bulletCounter.clear();
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
// NB that both ordered and bullet lists are represented
// by ListItem (must inspect parent to detect the type)
builder.on(ListItem.class, (visitor, listItem) -> {
// mimic original behaviour (copy-pasta from CorePlugin)
final int length = visitor.length();
visitor.visitChildren(listItem);
final Node parent = listItem.getParent();
if (parent instanceof OrderedList) {
final int start = ((OrderedList) parent).getStartNumber();
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
// after we have visited the children increment start number
final OrderedList orderedList = (OrderedList) parent;
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
} else {
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
if (isBulletOrdered(parent)) {
// obtain current count value
final int count = currentBulletCountIn(parent);
BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
// update current count value
setCurrentBulletCountIn(parent, count + 1);
} else {
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
// clear letter info when regular bullet list is used
BULLET_LETTER.clear(visitor.renderProps());
}
}
visitor.setSpansForNodeOptional(listItem, length);
if (visitor.hasNext(listItem)) {
visitor.ensureNewLine();
}
});
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(ListItem.class, (configuration, props) -> {
final Object spans;
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
final String letter = BULLET_LETTER.get(props);
if (!TextUtils.isEmpty(letter)) {
// NB, we are using OrderedListItemSpan here!
spans = new OrderedListItemSpan(
configuration.theme(),
letter
);
} else {
spans = new BulletListItemSpan(
configuration.theme(),
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
);
}
} else {
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
+ "." + '\u00a0';
spans = new OrderedListItemSpan(
configuration.theme(),
number
);
}
return spans;
});
}
private int currentBulletCountIn(@NonNull Node parent) {
return bulletCounter.get(parent.hashCode(), 0);
}
private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
bulletCounter.put(parent.hashCode(), count);
}
@NonNull
private static String createBulletLetter(int count) {
// or lower `a`
// `'u00a0` is non-breakable space char
return ((char) ('A' + count)) + ".\u00a0";
}
private static int listLevel(@NonNull Node node) {
int level = 0;
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof ListItem) {
level += 1;
}
parent = parent.getParent();
}
return level;
}
private static boolean isBulletOrdered(@NonNull Node node) {
node = node.getParent();
while (node != null) {
if (node instanceof OrderedList) {
return true;
}
if (node instanceof BulletList) {
return false;
}
node = node.getParent();
}
return false;
}
}

View File

@ -1,49 +0,0 @@
package io.noties.markwon.app.samples;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.UpdateAppearance;
import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200702101224",
title = "Remove link underline",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.links, Tag.rendering, Tag.span}
)
public class LinkRemoveUnderlineSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"There are a lot of [links](#) [here](#)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.appendFactory(Link.class, (configuration, props) -> new RemoveUnderlineSpan());
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class RemoveUnderlineSpan extends CharacterStyle implements UpdateAppearance {
@Override
public void updateDrawState(TextPaint tp) {
tp.setUnderlineText(false);
}
}

View File

@ -1,97 +0,0 @@
package io.noties.markwon.app.samples;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Link;
import java.util.Locale;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolver;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629122230",
title = "Obtain link title",
description = "Obtain title (text) of clicked link, `[title](#destination)`",
artifacts = {MarkwonArtifact.CORE},
tags = {Tag.links, Tag.span}
)
public class LinkTitleSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Links\n\n" +
"[link title](#)";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Link.class, (configuration, props) ->
// create a subclass of markwon LinkSpan
new ClickSelfSpan(
configuration.theme(),
CoreProps.LINK_DESTINATION.require(props),
configuration.linkResolver()
)
);
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class ClickSelfSpan extends LinkSpan {
ClickSelfSpan(
@NonNull MarkwonTheme theme,
@NonNull String link,
@NonNull LinkResolver resolver) {
super(theme, link, resolver);
}
@Override
public void onClick(View widget) {
Toast.makeText(
widget.getContext(),
String.format(Locale.ROOT, "clicked link title: '%s'", linkTitle(widget)),
Toast.LENGTH_LONG
).show();
super.onClick(widget);
}
@Nullable
private CharSequence linkTitle(@NonNull View widget) {
if (!(widget instanceof TextView)) {
return null;
}
final Spanned spanned = (Spanned) ((TextView) widget).getText();
final int start = spanned.getSpanStart(this);
final int end = spanned.getSpanEnd(this);
if (start < 0 || end < 0) {
return null;
}
return spanned.subSequence(start, end);
}
}

View File

@ -1,29 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629124005",
title = "Links without scheme",
description = "Links without scheme are considered to be `https`",
artifacts = {MarkwonArtifact.CORE},
tags = {Tag.links, Tag.defaults}
)
public class LinkWithoutSchemeSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Links without scheme\n" +
"[a link without scheme](github.com) is considered to be `https`.\n" +
"Override `LinkResolverDef` to change this functionality" +
"";
final Markwon markwon = Markwon.create(context);
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,49 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629171212",
title = "No parsing",
description = "All commonmark parsing is disabled (both inlines and blocks)",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.parsing, Tag.rendering}
)
public class NoParsingSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"[link](#) was _here_ and `then` and it was:\n" +
"> a quote\n" +
"```java\n" +
"final int someJavaCode = 0;\n" +
"```\n";
final Markwon markwon = Markwon.builder(context)
// disable inline parsing
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.enabledBlockTypes(Collections.emptySet());
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,48 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20201203221806",
title = "Ordered list numbers",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class OrderedListNumbersSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Ordered lists\n\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"1. hello there\n" +
"1. hello there and much much more, this text just goes and goes, and should it stop, we' know it\n" +
"1. okay, np\n" +
"";
final Markwon markwon = Markwon.create(context);
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,43 +0,0 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import org.commonmark.node.Paragraph;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629122647",
title = "Paragraph style",
description = "Apply a style (via span) to a paragraph",
artifacts = {MarkwonArtifact.CORE},
tags = {Tag.paragraph, Tag.style, Tag.span}
)
public class ParagraphSpanStyle extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "# Hello!\n\nA paragraph?\n\nIt should be!";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// apply a span to a Paragraph
builder.setFactory(Paragraph.class, (configuration, props) ->
new ForegroundColorSpan(Color.GREEN));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,75 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.PrecomputedFutureTextSetterCompat;
import io.noties.markwon.app.R;
import io.noties.markwon.app.readme.GithubImageDestinationProcessor;
import io.noties.markwon.app.sample.ui.MarkwonRecyclerViewSample;
import io.noties.markwon.app.utils.SampleUtilsKtKt;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200702092446",
title = "PrecomputedFutureTextSetterCompat",
description = "Usage of `PrecomputedFutureTextSetterCompat` " +
"inside a `RecyclerView` with appcompat",
artifacts = {MarkwonArtifact.RECYCLER},
tags = {Tag.recyclerView, Tag.precomputedText}
)
public class PrecomputedFutureSample extends MarkwonRecyclerViewSample {
@Override
public void render() {
if (!hasAppCompat()) {
/*
PLEASE COMPILE WITH `APPCOMPAT` dependency
*/
return;
}
final String md = SampleUtilsKtKt.loadReadMe(context);
final Markwon markwon = Markwon.builder(context)
.textSetter(PrecomputedFutureTextSetterCompat.create())
.usePlugin(ImagesPlugin.create())
.usePlugin(TablePlugin.create(context))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.imageDestinationProcessor(new GithubImageDestinationProcessor());
}
})
.build();
final MarkwonAdapter adapter = MarkwonAdapter
.createTextViewIsRoot(R.layout.adapter_appcompat_default_entry);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setAdapter(adapter);
adapter.setMarkdown(markwon, md);
adapter.notifyDataSetChanged();
}
private static boolean hasAppCompat() {
try {
Class.forName("androidx.appcompat.widget.AppCompatTextView");
return true;
} catch (Throwable t) {
return false;
}
}
}

View File

@ -1,32 +0,0 @@
package io.noties.markwon.app.samples;
import java.util.concurrent.Executors;
import io.noties.markwon.Markwon;
import io.noties.markwon.PrecomputedTextSetterCompat;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200702091654",
title = "PrecomputedTextSetterCompat",
description = "`TextSetter` to use `PrecomputedTextSetterCompat`",
artifacts = MarkwonArtifact.CORE,
tags = Tag.precomputedText
)
public class PrecomputedSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Heading\n" +
"**bold** some precomputed spans via `PrecomputedTextSetterCompat`";
final Markwon markwon = Markwon.builder(context)
.textSetter(PrecomputedTextSetterCompat.create(Executors.newCachedThreadPool()))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,208 +0,0 @@
package io.noties.markwon.app.samples;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.ReplacementSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629161505",
title = "Read more plugin",
description = "Plugin that adds expand/collapse (\"show all\"/\"show less\")",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.plugin}
)
public class ReadMorePluginSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Lorem **ipsum** ![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

@ -1,84 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.node.FencedCodeBlock;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.R;
import io.noties.markwon.app.readme.GithubImageDestinationProcessor;
import io.noties.markwon.app.sample.ui.MarkwonRecyclerViewSample;
import io.noties.markwon.app.utils.SampleUtilsKtKt;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.ext.tasklist.TaskListPlugin;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry;
import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200702101750",
title = "RecyclerView",
description = "Usage with `RecyclerView`",
artifacts = {MarkwonArtifact.RECYCLER, MarkwonArtifact.RECYCLER_TABLE},
tags = Tag.recyclerView
)
public class RecyclerSample extends MarkwonRecyclerViewSample {
@Override
public void render() {
final String md = SampleUtilsKtKt.loadReadMe(context);
final Markwon markwon = Markwon.builder(context)
.usePlugin(ImagesPlugin.create())
.usePlugin(TableEntryPlugin.create(context))
.usePlugin(HtmlPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TaskListPlugin.create(context))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.imageDestinationProcessor(new GithubImageDestinationProcessor());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> {
// we actually won't be applying code spans here, as our custom view will
// draw background and apply mono typeface
//
// NB the `trim` operation on literal (as code will have a new line at the end)
final CharSequence code = visitor.configuration()
.syntaxHighlight()
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim());
visitor.builder().append(code);
});
}
})
.build();
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_node)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_node_code_block, R.id.text_view))
.include(TableBlock.class, TableEntry.create(builder -> {
builder
.tableLayout(R.layout.adapter_node_table_block, R.id.table_layout)
.textLayoutIsRoot(R.layout.view_table_entry_cell);
}))
.build();
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setAdapter(adapter);
adapter.setMarkdown(markwon, md);
adapter.notifyDataSetChanged();
}
}

View File

@ -1,126 +0,0 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
import io.noties.markwon.utils.ColorUtils;
@MarkwonSampleInfo(
id = "20200813145316",
title = "Reddit spoiler",
description = "An attempt to implement Reddit spoiler syntax `>! !<`",
artifacts = MarkwonArtifact.CORE,
tags = Tag.parsing
)
public class RedditSpoilerSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Reddit spolier\n\n" +
"Hello >!ugly so **ugly** !<, how are you?\n\n" +
">!a blockquote?!< should not be >!present!< yeah" +
"";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new RedditSpoilerPlugin())
.build();
markwon.setMarkdown(textView, md);
}
}
class RedditSpoilerPlugin extends AbstractMarkwonPlugin {
private static final Pattern RE = Pattern.compile(">!.+?!<");
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
// replace all `>!` with `&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

@ -1,49 +0,0 @@
package io.noties.markwon.app.samples;
import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.core.spans.EmphasisSpan;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
import io.noties.markwon.simple.ext.SimpleExtPlugin;
@MarkwonSampleInfo(
id = "20200630194335",
title = "Delimiter processor simple-ext",
description = "Custom delimiter processor implemented with a `SimpleExtPlugin`",
artifacts = MarkwonArtifact.SIMPLE_EXT,
tags = Tag.parsing
)
public class SimpleExtensionSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# SimpleExt\n" +
"\n" +
"+let's start with `+`, ??then we can use this, and finally @@this$$??+";
// NB! we cannot have multiple delimiter processor with the same character
// (even if lengths are different)
final Markwon markwon = Markwon.builder(context)
.usePlugin(SimpleExtPlugin.create(plugin -> {
plugin
.addExtension(1, '+', (configuration, props) -> new EmphasisSpan())
.addExtension(2, '?', (configuration, props) -> new StrongEmphasisSpan())
.addExtension(
2,
'@',
'$',
(configuration, props) -> new ForegroundColorSpan(Color.RED)
);
}))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,29 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629125040",
title = "Soft break new line",
description = "Add a new line for a markdown soft-break node",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.newLine, Tag.softBreak}
)
public class SoftBreakAddsNewLineSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Hello there ->(line)\n(break)<- going on and on";
final Markwon markwon = Markwon.builder(context)
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,28 +0,0 @@
package io.noties.markwon.app.samples;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629124706",
title = "Soft break adds space",
description = "By default a soft break (`\n`) will " +
"add a space character instead of new line",
artifacts = MarkwonArtifact.CORE,
tags = {Tag.newLine, Tag.softBreak, Tag.defaults}
)
public class SoftBreakAddsSpace extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"Hello there ->(line)\n(break)<- going on and on";
// by default a soft break will add a space (instead of line break)
final Markwon markwon = Markwon.create(context);
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,63 +0,0 @@
package io.noties.markwon.app.samples;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import org.commonmark.node.ThematicBreak;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200813154415",
title = "Thematic break bottom margin",
description = "Do not add a new line after thematic break (with the `BlockHandler`)",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class ThematicBreakBottomMarginSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"# Thematic break and margin\n\n" +
"So, what if....\n\n" +
"---\n\n" +
"And **now**";
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerDef() {
@Override
public void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
// also can control block start
super.blockStart(visitor, node);
}
@Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// thematic break won't have a new line
// similarly you can control other blocks
if (!(node instanceof ThematicBreak)) {
visitor.forceNewLine();
}
}
}
});
}
})
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,74 +0,0 @@
package io.noties.markwon.app.samples
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import io.noties.markwon.Markwon
import io.noties.markwon.app.BuildConfig
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
@MarkwonSampleInfo(
id = "20200627074017",
title = "Markdown in Toast (with dynamic content)",
description = "Display markdown in a `android.widget.Toast` with dynamic content (image)",
artifacts = [MarkwonArtifact.CORE, MarkwonArtifact.IMAGE],
tags = [Tag.toast, Tag.hack]
)
class ToastDynamicContentSample : MarkwonTextViewSample() {
override fun render() {
val md = """
# Head!
![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

@ -1,38 +0,0 @@
package io.noties.markwon.app.samples
import android.widget.Toast
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
@MarkwonSampleInfo(
id = "20200627072642",
title = "Markdown in Toast",
description = "Display _static_ markdown content in a `android.widget.Toast`",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.toast]
)
class ToastSample : MarkwonTextViewSample() {
override fun render() {
// NB! only _static_ content is going to be displayed,
// so, no images, tables or latex in a Toast
val md = """
# Heading is fine
> Even quote if **fine**
```
finally code works;
```
_italic_ to put an end to it
""".trimIndent()
val markwon = Markwon.create(context)
// render raw input to styled markdown
val markdown = markwon.toMarkdown(md)
// Toast accepts CharSequence and allows styling via spans
Toast.makeText(context, markdown, Toast.LENGTH_LONG).show()
}
}

View File

@ -1,29 +0,0 @@
package io.noties.markwon.app.samples.basics;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20210118154116",
title = "One line text",
description = "Single line text without markdown markup",
artifacts = MarkwonArtifact.CORE,
tags = Tag.rendering
)
public class OneLineNoMarkdownSample extends MarkwonTextViewSample {
@Override
public void render() {
textView.setBackgroundColor(0x40ff0000);
final String md = " Demo text ";
final Markwon markwon = Markwon.builder(context)
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -1,33 +0,0 @@
package io.noties.markwon.app.samples.basics
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
@MarkwonSampleInfo(
id = "20200626152255",
title = "Simple",
description = "The most primitive and simple way to apply markdown to a `TextView`",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.basics]
)
class Simple : MarkwonTextViewSample() {
override fun render() {
// markdown input
val md = """
# Heading
> A quote
**bold _italic_ bold**
""".trimIndent()
// markwon instance
val markwon = Markwon.create(context)
// apply raw markdown (internally parsed and rendered)
markwon.setMarkdown(textView, md)
}
}

View File

@ -1,47 +0,0 @@
package io.noties.markwon.app.samples.basics
import android.text.Spanned
import io.noties.markwon.Markwon
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample
import io.noties.markwon.core.CorePlugin
import io.noties.markwon.sample.annotations.MarkwonArtifact
import io.noties.markwon.sample.annotations.MarkwonSampleInfo
import io.noties.markwon.sample.annotations.Tag
import org.commonmark.node.Node
@MarkwonSampleInfo(
id = "20200626153426",
title = "Simple with walk-through",
description = "Walk-through for simple use case",
artifacts = [MarkwonArtifact.CORE],
tags = [Tag.basics]
)
class SimpleWalkthrough : MarkwonTextViewSample() {
override fun render() {
val md: String = """
# Hello!
> a quote
```
code block
```
""".trimIndent()
// create markwon instance via builder method
val markwon: Markwon = Markwon.builder(context)
// add required plugins
// NB, there is no need to add CorePlugin as it is added automatically
.usePlugin(CorePlugin.create())
.build()
// parse markdown into commonmark representation
val node: Node = markwon.parse(md)
// render commonmark node
val markdown: Spanned = markwon.render(node)
// apply it to a TextView
markwon.setParsedMarkdown(textView, markdown)
}
}

View File

@ -1,106 +0,0 @@
package io.noties.markwon.app.samples.editor;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629165136",
title = "Additional edit span",
description = "Additional _edit_ span (span that is present in " +
"`EditText` along with punctuation",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor, Tag.span}
)
public class EditorAdditionalEditSpan extends MarkwonEditTextSample {
@Override
public void render() {
// An additional span is used to highlight strong-emphasis
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
.useEditHandler(new BoldEditHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}
class BoldEditHandler extends AbstractEditHandler<StrongEmphasisSpan> {
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
// Here we define which span is _persisted_ in EditText, it is not removed
// from EditText between text changes, but instead - reused (by changing
// position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
// here also, but I chose Bold to indicate that this span is not the same
// as in off-screen rendered markdown
builder.persistSpan(Bold.class, Bold::new);
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull StrongEmphasisSpan span,
int spanStart,
int spanTextLength) {
// Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
// because multiple inline markdown nodes can refer to the same text.
// For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
// and thus will have to manually find actual position in raw user input
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
if (match != null) {
editable.setSpan(
// we handle StrongEmphasisSpan and represent it with Bold in EditText
// we still could use StrongEmphasisSpan, but it must be accessed
// via persistedSpans
persistedSpans.get(Bold.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<StrongEmphasisSpan> markdownSpanType() {
return StrongEmphasisSpan.class;
}
}
class Bold extends MetricAffectingSpan {
public Bold() {
super();
}
@Override
public void updateDrawState(TextPaint tp) {
update(tp);
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
update(textPaint);
}
private void update(@NonNull TextPaint paint) {
paint.setFakeBoldText(true);
}
}

View File

@ -1,34 +0,0 @@
package io.noties.markwon.app.samples.editor;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629165347",
title = "Additional plugin",
description = "Additional plugin for editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER, MarkwonArtifact.EXT_STRIKETHROUGH},
tags = {Tag.editor}
)
public class EditorAdditionalPluginSample extends MarkwonEditTextSample {
@Override
public void render() {
// As highlight works based on text-diff, everything that is present in input,
// but missing in resulting markdown is considered to be punctuation, this is why
// additional plugins do not need special handling
final Markwon markwon = Markwon.builder(context)
.usePlugin(StrikethroughPlugin.create())
.build();
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}

View File

@ -1,37 +0,0 @@
package io.noties.markwon.app.samples.editor;
import android.text.style.ForegroundColorSpan;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629164627",
title = "Custom punctuation span",
description = "Custom span for punctuation in editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor, Tag.span}
)
public class EditorCustomPunctuationSample extends MarkwonEditTextSample {
@Override
public void render() {
// Use own punctuation span
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(context))
.punctuationSpan(CustomPunctuationSpan.class, CustomPunctuationSpan::new)
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}
class CustomPunctuationSpan extends ForegroundColorSpan {
CustomPunctuationSpan() {
super(0xFFFF0000); // RED
}
}

View File

@ -1,32 +0,0 @@
package io.noties.markwon.app.samples.editor;
import java.util.concurrent.Executors;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200630113954",
title = "Heading edit handler",
description = "Handling of heading node in editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor}
)
public class EditorHeadingSample extends MarkwonEditTextSample {
@Override
public void render() {
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.useEditHandler(new HeadingEditHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
}

View File

@ -1,87 +0,0 @@
package io.noties.markwon.app.samples.editor;
import android.text.method.LinkMovementMethod;
import androidx.annotation.NonNull;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.concurrent.Executors;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.editor.handler.EmphasisEditHandler;
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.inlineparser.BangInlineProcessor;
import io.noties.markwon.inlineparser.EntityInlineProcessor;
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629165920",
title = "Multiple edit spans",
description = "Additional multiple edit spans for editor",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor}
)
public class EditorMultipleEditSpansSample extends MarkwonEditTextSample {
@Override
public void render() {
// for links to be clickable
editText.setMovementMethod(LinkMovementMethod.getInstance());
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
// no inline images will be parsed
.excludeInlineProcessor(BangInlineProcessor.class)
// no html tags will be parsed
.excludeInlineProcessor(HtmlInlineProcessor.class)
// no entities will be parsed (aka `&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

@ -1,158 +0,0 @@
package io.noties.markwon.app.samples.editor;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629170348",
title = "Editor new line continuation",
description = "Sample of how new line character can be handled " +
"in order to add a _continuation_, for example adding a new " +
"bullet list item if current line starts with one",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor}
)
public class EditorNewLineContinuationSample extends MarkwonEditTextSample {
@Override
public void render() {
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.create(markwon);
final TextWatcher textWatcher = MarkdownNewLine
.wrap(MarkwonEditorTextWatcher.withProcess(editor));
editText.addTextChangedListener(textWatcher);
}
}
class MarkdownNewLine {
@NonNull
static TextWatcher wrap(@NonNull TextWatcher textWatcher) {
return new NewLineTextWatcher(textWatcher);
}
private MarkdownNewLine() {
}
private static class NewLineTextWatcher implements TextWatcher {
// NB! matches only bullet lists
private final Pattern RE = Pattern.compile("^( {0,3}[\\-+* ]+)(.+)*$");
private final TextWatcher wrapped;
private boolean selfChange;
// this content is pending to be inserted at the beginning
private String pendingNewLineContent;
private int pendingNewLineIndex;
// mark current edited line for removal (range start/end)
private int clearLineStart;
private int clearLineEnd;
NewLineTextWatcher(@NonNull TextWatcher wrapped) {
this.wrapped = wrapped;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// no op
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (selfChange) {
return;
}
// just one new character added
if (before == 0
&& count == 1
&& '\n' == s.charAt(start)) {
int end = -1;
for (int i = start - 1; i >= 0; i--) {
if ('\n' == s.charAt(i)) {
end = i + 1;
break;
}
}
// start at the very beginning
if (end < 0) {
end = 0;
}
final String pendingNewLineContent;
final int clearLineStart;
final int clearLineEnd;
final Matcher matcher = RE.matcher(s.subSequence(end, start));
if (matcher.matches()) {
// if second group is empty -> remove new line
final String content = matcher.group(2);
Debug.e("new line, content: '%s'", content);
if (TextUtils.isEmpty(content)) {
// another empty new line, remove this start
clearLineStart = end;
clearLineEnd = start;
pendingNewLineContent = null;
} else {
pendingNewLineContent = matcher.group(1);
clearLineStart = clearLineEnd = 0;
}
} else {
pendingNewLineContent = null;
clearLineStart = clearLineEnd = 0;
}
this.pendingNewLineContent = pendingNewLineContent;
this.pendingNewLineIndex = start + 1;
this.clearLineStart = clearLineStart;
this.clearLineEnd = clearLineEnd;
}
}
@Override
public void afterTextChanged(Editable s) {
if (selfChange) {
return;
}
if (pendingNewLineContent != null || clearLineStart < clearLineEnd) {
selfChange = true;
try {
if (pendingNewLineContent != null) {
s.insert(pendingNewLineIndex, pendingNewLineContent);
pendingNewLineContent = null;
} else {
s.replace(clearLineStart, clearLineEnd, "");
clearLineStart = clearLineEnd = 0;
}
} finally {
selfChange = false;
}
}
// NB, we assume MarkdownEditor text watcher that only listens for this event,
// other text-watchers must be interested in other events also
wrapped.afterTextChanged(s);
}
}
}

View File

@ -1,34 +0,0 @@
package io.noties.markwon.app.samples.editor;
import java.util.concurrent.Executors;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629164422",
title = "Editor with pre-render (async)",
description = "Editor functionality with highlight " +
"taking place in another thread",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor}
)
public class EditorPreRenderSample extends MarkwonEditTextSample {
@Override
public void render() {
// Process highlight in background thread
final Markwon markwon = Markwon.create(context);
final MarkwonEditor editor = MarkwonEditor.create(markwon);
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor,
Executors.newCachedThreadPool(),
editText));
}
}

View File

@ -1,32 +0,0 @@
package io.noties.markwon.app.samples.editor;
import io.noties.markwon.Markwon;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200629164227",
title = "Simple editor",
description = "Simple usage of editor with markdown highlight",
artifacts = {MarkwonArtifact.EDITOR, MarkwonArtifact.INLINE_PARSER},
tags = {Tag.editor}
)
public class EditorSimpleSample extends MarkwonEditTextSample {
@Override
public void render() {
// Process highlight in-place (right after text has changed)
// obtain Markwon instance
final Markwon markwon = Markwon.create(context);
// create editor
final MarkwonEditor editor = MarkwonEditor.create(markwon);
// set edit listener
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
}

View File

@ -1,134 +0,0 @@
package io.noties.markwon.app.samples.editor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.method.LinkMovementMethod;
import android.text.style.ReplacementSpan;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.app.samples.editor.shared.BlockQuoteEditHandler;
import io.noties.markwon.app.samples.editor.shared.CodeEditHandler;
import io.noties.markwon.app.samples.editor.shared.HeadingEditHandler;
import io.noties.markwon.app.samples.editor.shared.LinkEditHandler;
import io.noties.markwon.app.samples.editor.shared.MarkwonEditTextSample;
import io.noties.markwon.app.samples.editor.shared.StrikethroughEditHandler;
import io.noties.markwon.editor.MarkwonEditor;
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
import io.noties.markwon.editor.PersistedSpans;
import io.noties.markwon.editor.handler.EmphasisEditHandler;
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200908133515",
title = "WYSIWG editor",
description = "A possible direction to implement what-you-see-is-what-you-get editor",
artifacts = MarkwonArtifact.EDITOR,
tags = Tag.rendering
)
public class WYSIWYGEditorSample extends MarkwonEditTextSample {
@Override
public void render() {
// when automatic line break is inserted and text is inside margin span (blockquote, list, etc)
// be prepared to encounter selection bugs (selection would be drawn at the place as is no margin
// span is present)
final Markwon markwon = Markwon.builder(context)
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.build();
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.punctuationSpan(HidePunctuationSpan.class, new PersistedSpans.SpanFactory<HidePunctuationSpan>() {
@NonNull
@Override
public HidePunctuationSpan create() {
return new HidePunctuationSpan();
}
})
.useEditHandler(new EmphasisEditHandler())
.useEditHandler(new StrongEmphasisEditHandler())
.useEditHandler(new StrikethroughEditHandler())
.useEditHandler(new CodeEditHandler())
.useEditHandler(new BlockQuoteEditHandler())
.useEditHandler(new LinkEditHandler(new LinkEditHandler.OnClick() {
@Override
public void onClick(@NonNull View widget, @NonNull String link) {
Debug.e("clicked: %s", link);
}
}))
.useEditHandler(new HeadingEditHandler())
.build();
// for links to be clickable
// NB! markwon MovementMethodPlugin cannot be used here as editor do not execute `beforeSetText`)
editText.setMovementMethod(LinkMovementMethod.getInstance());
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
private static class HidePunctuationSpan extends ReplacementSpan {
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
// last space (which is swallowed until next non-space character appears)
// block quote
// code tick
// Debug.i("text: '%s', %d-%d (%d)", text.subSequence(start, end), start, end, text.length());
if (end == text.length()) {
// TODO: find first non-space character (not just first one because commonmark allows
// arbitrary (0-3) white spaces before content starts
// TODO: if all white space - render?
final char c = text.charAt(start);
if ('#' == c
|| '>' == c
|| '-' == c // TODO: not thematic break
|| '+' == c
// `*` is fine but only for a list
|| isBulletList(text, c, start, end)
|| Character.isDigit(c) // assuming ordered list (replacement should only happen for ordered lists)
|| Character.isWhitespace(c)) {
return (int) (paint.measureText(text, start, end) + 0.5F);
}
}
return 0;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
// will be called only when getSize is not 0 (and if it was once reported as 0...)
if (end == text.length()) {
// if first non-space is `*` then check for is bullet
// else `**` would be still rendered at the end of the emphasis
if (text.charAt(start) == '*'
&& !isBulletList(text, '*', start, end)) {
return;
}
// TODO: inline code last tick received here, handle it (do not highlight)
// why can't we have reported width in this method for supplied text?
// let's use color to make it distinct from the rest of the text for demonstration purposes
paint.setColor(0xFFff0000);
canvas.drawText(text, start, end, x, y, paint);
}
}
private static boolean isBulletList(@NonNull CharSequence text, char firstChar, int start, int end) {
return '*' == firstChar
&& ((end - start == 1) || (Character.isWhitespace(text.charAt(start + 1))));
}
}
}

View File

@ -1,50 +0,0 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.BlockQuoteSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull BlockQuoteSpan span,
int spanStart,
int spanTextLength) {
// todo: here we should actually find a proper ending of a block quote...
editable.setSpan(
persistedSpans.get(BlockQuoteSpan.class),
spanStart,
spanStart + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
@NonNull
@Override
public Class<BlockQuoteSpan> markdownSpanType() {
return BlockQuoteSpan.class;
}
}

View File

@ -1,54 +0,0 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.CodeSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
public class CodeEditHandler implements EditHandler<CodeSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull CodeSpan span,
int spanStart,
int spanTextLength) {
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "`");
if (match != null) {
editable.setSpan(
persistedSpans.get(CodeSpan.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<CodeSpan> markdownSpanType() {
return CodeSpan.class;
}
}

View File

@ -1,82 +0,0 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class HeadingEditHandler implements EditHandler<HeadingSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder
.persistSpan(Head1.class, () -> new Head1(theme))
.persistSpan(Head2.class, () -> new Head2(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull HeadingSpan span,
int spanStart,
int spanTextLength
) {
final Class<?> type;
switch (span.getLevel()) {
case 1:
type = Head1.class;
break;
case 2:
type = Head2.class;
break;
default:
type = null;
}
if (type != null) {
final int index = input.indexOf('\n', spanStart + spanTextLength);
final int end = index < 0
? input.length()
: index;
editable.setSpan(
persistedSpans.get(type),
spanStart,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<HeadingSpan> markdownSpanType() {
return HeadingSpan.class;
}
private static class Head1 extends HeadingSpan {
Head1(@NonNull MarkwonTheme theme) {
super(theme, 1);
}
}
private static class Head2 extends HeadingSpan {
Head2(@NonNull MarkwonTheme theme) {
super(theme, 2);
}
}
}

View File

@ -1,90 +0,0 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
import io.noties.markwon.core.spans.LinkSpan;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
public interface OnClick {
void onClick(@NonNull View widget, @NonNull String link);
}
private final OnClick onClick;
public LinkEditHandler(@NonNull OnClick onClick) {
this.onClick = onClick;
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull LinkSpan span,
int spanStart,
int spanTextLength) {
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
editLinkSpan.link = span.getLink();
// First first __letter__ to find link content (scheme start in URL, receiver in email address)
// NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
// display. For example, we _could_ also look for a digit, but:
// * if phone number start with special symbol, we won't have it (`+`, `(`)
// * it might interfere with an ordered-list
int start = -1;
for (int i = spanStart, length = input.length(); i < length; i++) {
if (Character.isLetter(input.charAt(i))) {
start = i;
break;
}
}
if (start > -1) {
editable.setSpan(
editLinkSpan,
start,
start + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<LinkSpan> markdownSpanType() {
return LinkSpan.class;
}
static class EditLinkSpan extends ClickableSpan {
private final OnClick onClick;
String link;
EditLinkSpan(@NonNull OnClick onClick) {
this.onClick = onClick;
}
@Override
public void onClick(@NonNull View widget) {
if (link != null) {
onClick.onClick(widget, link);
}
}
}
}

View File

@ -1,104 +0,0 @@
package io.noties.markwon.app.samples.editor.shared
import android.content.Context
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.StrikethroughSpan
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import io.noties.markwon.app.R
import io.noties.markwon.app.sample.ui.MarkwonSample
import io.noties.markwon.core.spans.EmphasisSpan
import io.noties.markwon.core.spans.StrongEmphasisSpan
import java.util.ArrayList
abstract class MarkwonEditTextSample : MarkwonSample() {
protected lateinit var context: Context
protected lateinit var editText: EditText
override val layoutResId: Int
get() = R.layout.sample_edit_text
override fun onViewCreated(view: View) {
context = view.context
editText = view.findViewById(R.id.edit_text)
initBottomBar(view)
render()
}
abstract fun render()
private fun initBottomBar(view: View) {
// all except block-quote wraps if have selection, or inserts at current cursor position
val bold: Button = view.findViewById(R.id.bold)
val italic: Button = view.findViewById(R.id.italic)
val strike: Button = view.findViewById(R.id.strike)
val quote: Button = view.findViewById(R.id.quote)
val code: Button = view.findViewById(R.id.code)
addSpan(bold, StrongEmphasisSpan())
addSpan(italic, EmphasisSpan())
addSpan(strike, StrikethroughSpan())
bold.setOnClickListener(InsertOrWrapClickListener(editText, "**"))
italic.setOnClickListener(InsertOrWrapClickListener(editText, "_"))
strike.setOnClickListener(InsertOrWrapClickListener(editText, "~~"))
code.setOnClickListener(InsertOrWrapClickListener(editText, "`"))
quote.setOnClickListener {
val start = editText.selectionStart
val end = editText.selectionEnd
if (start < 0) {
return@setOnClickListener
}
if (start == end) {
editText.text.insert(start, "> ")
} else {
// wrap the whole selected area in a quote
val newLines: MutableList<Int> = ArrayList(3)
newLines.add(start)
val text = editText.text.subSequence(start, end).toString()
var index = text.indexOf('\n')
while (index != -1) {
newLines.add(start + index + 1)
index = text.indexOf('\n', index + 1)
}
for (i in newLines.indices.reversed()) {
editText.text.insert(newLines[i], "> ")
}
}
}
}
private fun addSpan(textView: TextView, vararg spans: Any) {
val builder = SpannableStringBuilder(textView.text)
val end = builder.length
for (span in spans) {
builder.setSpan(span, 0, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.text = builder
}
private class InsertOrWrapClickListener(
private val editText: EditText,
private val text: String
) : View.OnClickListener {
override fun onClick(v: View) {
val start = editText.selectionStart
val end = editText.selectionEnd
if (start < 0) {
return
}
if (start == end) {
// insert at current position
editText.text.insert(start, text)
} else {
editText.text.insert(end, text)
editText.text.insert(start, text)
}
}
}
}

View File

@ -1,45 +0,0 @@
package io.noties.markwon.app.samples.editor.shared;
import android.text.Editable;
import android.text.Spanned;
import android.text.style.StrikethroughSpan;
import androidx.annotation.NonNull;
import io.noties.markwon.editor.AbstractEditHandler;
import io.noties.markwon.editor.MarkwonEditorUtils;
import io.noties.markwon.editor.PersistedSpans;
public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> {
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new);
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull StrikethroughSpan span,
int spanStart,
int spanTextLength) {
final MarkwonEditorUtils.Match match =
MarkwonEditorUtils.findDelimited(input, spanStart, "~~");
if (match != null) {
editable.setSpan(
persistedSpans.get(StrikethroughSpan.class),
match.start(),
match.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<StrikethroughSpan> markdownSpanType() {
return StrikethroughSpan.class;
}
}

View File

@ -1,87 +0,0 @@
package io.noties.markwon.app.samples.html;
import android.text.Layout;
import android.text.style.AlignmentSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.tag.SimpleTagHandler;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200630114630",
title = "Align HTML tag",
description = "Implement custom HTML tag handling",
artifacts = MarkwonArtifact.HTML,
tags = {Tag.rendering, Tag.span, Tag.html}
)
public class HtmlAlignSample extends MarkwonTextViewSample {
@Override
public void render() {
final String md = "" +
"<align center>We are centered</align>\n" +
"\n" +
"<align end>We are at the end</align>\n" +
"\n" +
"<align>We should be at the start</align>\n" +
"\n";
final Markwon markwon = Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configure(@NonNull Registry registry) {
registry.require(HtmlPlugin.class, htmlPlugin -> htmlPlugin
.addHandler(new AlignTagHandler()));
}
})
.build();
markwon.setMarkdown(textView, md);
}
}
class AlignTagHandler extends SimpleTagHandler {
@Nullable
@Override
public Object getSpans(
@NonNull MarkwonConfiguration configuration,
@NonNull RenderProps renderProps,
@NonNull HtmlTag tag) {
final Layout.Alignment alignment;
// html attribute without value, <align center></align>
if (tag.attributes().containsKey("center")) {
alignment = Layout.Alignment.ALIGN_CENTER;
} else if (tag.attributes().containsKey("end")) {
alignment = Layout.Alignment.ALIGN_OPPOSITE;
} else {
// empty value or any other will make regular alignment
alignment = Layout.Alignment.ALIGN_NORMAL;
}
return new AlignmentSpan.Standard(alignment);
}
@NonNull
@Override
public Collection<String> supportedTags() {
return Collections.singleton("align");
}
}

View File

@ -1,115 +0,0 @@
package io.noties.markwon.app.samples.html;
import android.text.Layout;
import android.text.style.AlignmentSpan;
import androidx.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.app.samples.html.shared.IFrameHtmlPlugin;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.html.HtmlTag;
import io.noties.markwon.html.MarkwonHtmlRenderer;
import io.noties.markwon.html.TagHandler;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
import io.noties.markwon.sample.annotations.Tag;
@MarkwonSampleInfo(
id = "20200630120101",
title = "Center HTML tag",
description = "Handling of `center` HTML tag",
artifacts = {MarkwonArtifact.HTML, MarkwonArtifact.IMAGE},
tags = {Tag.rendering, Tag.html}
)
public class HtmlCenterTagSample extends MarkwonTextViewSample {
@Override
public void render() {
final String html = "<html>\n" +
"\n" +
"<head></head>\n" +
"\n" +
"<body>\n" +
" <p></p>\n" +
" <h3>LiSA's Sword Art Online: Alicization OP Song \"ADAMAS\" Certified Platinum with 250,000 Downloads</h3>\n" +
" <p></p>\n" +
" <h5>The upper tune was already certified Gold one month after its digital release</h5>\n" +
" <p>According to The Recording Industry Association of Japan (RIAJ)'s monthly report for April 2020, one of the <span\n" +
" style=\"color: #ff9900;\"><strong><a href=\"http://www.lxixsxa.com/\" target=\"_blank\"><span\n" +
" style=\"color: #ff9900;\">LiSA</span></a></strong></span>'s 14th single songs,\n" +
" <strong>\"ADAMAS\"</strong>&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");
}
}

Some files were not shown because too many files have changed in this diff Show More