From db0936094fa94891adad4ff3b01e0783cd1a44b9 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 14 Mar 2019 17:22:14 +0300 Subject: [PATCH] Finishing documentation --- docs/.vuepress/config.js | 9 +- docs/docs/v3/core/html-renderer.md | 21 +- docs/docs/v3/html/README.md | 324 ++---------------- docs/docs/v3/html/custom-tag-handler.md | 1 - docs/docs/v3/image/gif.md | 14 +- docs/docs/v3/image/okhttp.md | 19 + docs/docs/v3/image/svg.md | 15 + docs/docs/v3/recycler-table/README.md | 2 + docs/docs/v3/recycler/README.md | 154 ++++++++- .../markwon/html/MarkwonHtmlRenderer.java | 3 + .../markwon/html/MarkwonHtmlRendererImpl.java | 6 + sample/build.gradle | 1 + .../res/layout/adapter_fenced_code_block.xml | 4 +- 13 files changed, 271 insertions(+), 302 deletions(-) delete mode 100644 docs/docs/v3/html/custom-tag-handler.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 85ebf762..5a0cbe66 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -48,14 +48,7 @@ module.exports = { '/docs/v3/ext-strikethrough/', '/docs/v3/ext-tables/', '/docs/v3/ext-tasklist/', - { - title: 'HTML', - collapsable: false, - children: [ - '/docs/v3/html/', - '/docs/v3/html/custom-tag-handler.md' - ] - }, + '/docs/v3/html/', '/docs/v3/image/gif.md', '/docs/v3/image/okhttp.md', '/docs/v3/image/svg.md', diff --git a/docs/docs/v3/core/html-renderer.md b/docs/docs/v3/core/html-renderer.md index d0756b12..17ef63e3 100644 --- a/docs/docs/v3/core/html-renderer.md +++ b/docs/docs/v3/core/html-renderer.md @@ -79,4 +79,23 @@ builder.setHandler("a", new TagHandler() { } } }); -``` \ No newline at end of file +``` + +:::tip +Sometimes HTML content might include tags that are not closed (although +they are required to be by the spec, for example a `div`). +Markwon by default disallows such tags and ignores them. Still, +there is an option to allow them _explicitly_ via builder method: +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder) { + builder.allowNonClosedTags(true); + } + }) + .build(); +``` +Please note that if `allowNonClosedTags=true` then all non-closed tags will be closed +at the end of a document. +::: \ No newline at end of file diff --git a/docs/docs/v3/html/README.md b/docs/docs/v3/html/README.md index 96a75431..a8a12d1b 100644 --- a/docs/docs/v3/html/README.md +++ b/docs/docs/v3/html/README.md @@ -1,47 +1,21 @@ ---- -title: 'Overview' ---- +# HTML -# HTML +This artifact encapsulates HTML parsing from the core artifact and provides +few predefined `TagHandlers` - - -Starting with version `2.0.0` `Markwon` brings the whole HTML parsing/rendering -stack _on-site_. The main reason for this are _special_ definitions of HTML nodes -by . More specifically: -and . -These two are _a bit_ different from _native_ HTML understanding. -Well, they are _completely_ different and share only the same names as - and -elements. This leads to situations when for example an `` tag is considered -a block when it's used like this: - -```markdown - -Hello from italics tag - +```java +final Markwon markwon = Markwon.builder(context) + .usePlugin(HtmlPlugin.create()) + .build(); ``` -:::tip A bit of background -
- had brought attention to differences between HTML & commonmark implementations.

-::: +As this artifact brings modified [jsoup](https://github.com/jhy/jsoup) library +it was moved to a standalone module in order to minimize dependencies and unused code +in applications that does not require HTML render capabilities. -Let's modify code snippet above _a bit_: - -```markdown{3} - -Hello from italics tag - - -``` - -We have just added a `new-line` before closing `
` tag. And this -changes everything as now, according to the , -we have 2 HtmlBlocks: one before `new-line` (containing open `` tag and text content) -and one after (containing as little as closing `` tag). - -If we modify code snippet _a bit_ again: +Before `Markwon` used android `Html` class for parsing and +rendering. Unfortunately, according to markdown specification, markdown can contain +HTML in _unpredictable_ way if rendered _outside_ of browser. For example: ```markdown{4} @@ -50,260 +24,38 @@ Hello from italics tag bold> ``` -We will have 1 HtmlBlock (from previous snippet) and a bunch of HtmlInlines: +This snippet could be represented as: +* HtmlBlock (`\nHello from italics tag`) * HtmlInline (``) * HtmlInline (``) * Text (`bold`) * HtmlInline (``) -Those _little_ differences render `Html.fromHtml` (which was used in `1.x.x` versions) -useless. And actually it renders most of the HTML parsers implementations useless, -as most of them do not allow processing of HTML fragments in a raw fashion -without _fixing_ content on-the-fly. - -Both `TagSoup` and `Jsoup` HTML parsers (that were considered for this project) are built to deal with -_malicious_ HTML code (*all HTML code*? :no_mouth:). So, when supplied -with a `italic` fragment they will make it `italic`. -And it's a good thing, but consider these fragments for the sake of markdown: - -* `italic ` -* `bold italic` -* `` - -We will get: - -* `italic ` -* `bold italic` - -_* Or to be precise: `italic ` & -`bold italic`_ - -Which will be rendered in a final document: - - -|expected|actual| -|---|---| -|italic bold italic|italic bold italic| - -This might seem like a minor problem, but add more tags to a document, -introduce some deeply nested structures, spice openning and closing tags up -by adding markdown markup between them and finally write _malicious_ HTML code :laughing:! - -There is no such problem on the _frontend_ for which commonmark specification is mostly -aimed as _frontend_ runs in a web-browser environment. After all _parsed_ markdown -will become HTML tags (most common usage). And web-browser will know how to render final result. - -We, on the other hand, do not posess HTML heritage (*thank :robot:!*), but still -want to display some HTML to style resulting markdown a bit. That's why `Markwon` -incorporated own HTML parsing logic. It is based on the project. -And makes usage of the `Tokekiser` class that allows to _tokenise_ input HTML. -All other code that doesn't follow this purpose was removed. It's safe to use -in projects that already have `jsoup` dependency as `Markwon` repackaged **jsoup** source classes -(which could be found ) - -## Parser - -There are no additional steps to configure HTML parsing. It's enabled by default. -If you wish to _exclude_ it, please follow the [exclude](#exclude-html-parsing) section below. - -The key class here is: `MarkwonHtmlParser` that is defined in `markwon-html-parser-api` module. -`markwon-html-parser-api` is a simple module that defines HTML parsing contract and -does not provide implementation. - -To change what implementation `Markwon` should use, `SpannableConfiguration` can be used: - -```java{2} -SpannableConfiguration.builder(context) - .htmlParser(MarkwonHtmlParser) - .build(); -``` - -`markwon-html-parser-impl` on the other hand provides `MarkwonHtmlParser` implementation. -It's called `MarkwonHtmlParserImpl`. It can be created like this: - -```java -final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(); -// or -final MarkwonHtmlParser htmlParser = MarkwonHtmlParserImpl.create(HtmlEmptyTagReplacement); -``` - -### Empty tag replacement - -In order to append text content for self-closing, void or just _empty_ HTML tags, -`HtmlEmptyTagReplacement` can be used. As we cannot set Span for empty content, -we must represent empty tag with text during parsing stage (if we want it to be represented). - -Consider this: -* `` -* `
` -* `` - -By default (`HtmlEmptyTagReplacement.create()`) will handle `img` and `br` tags. -`img` will be replaced with `alt` property if it is present and `\uFFFC` if it is not. -And `br` will insert a new line. - -### Non-closed tags - -It's possible that your HTML can contain non-closed tags. By default `Markwon` will ignore them, -but if you wish to get a bit closer to a web-browser experience, you can allow this behaviour: - -```java{2} -SpannableConfiguration.builder(context) - .htmlAllowNonClosedTags(true) - .build(); -``` - -:::warning Note -If there is (for example) an `` tag at the start of a document and it's not closed -and `Markwon` is configured to **not** ignore non-closed tags (`.htmlAllowNonClosedTags(true)`), -it will make the whole document in italics +:::tip A bit of background +
+ had brought attention to differences between HTML & commonmark implementations.

::: -### Implementation note +Unfortunately Android `HTML` class cannot parse a _fragment_ of HTML to later +be included in a bigger set of content. This is why the decision was made to bring +HTML parsing _in-markwon-house_ -`MarkwonHtmlParserImpl` does not create a unified HTML node. Instead it creates -2 collections: inline tags and block tags. Inline tags are represented as a `List` -of inline tags (). And -block tags are structured in a tree. This helps to achieve _browser_-like behaviour, -when open inline tag is applied to all content (even if inside blocks) until closing tag. -All tags that are not _inline_ are considered to be _block_ ones. +## Predefined TagHandlers +* `` +* `` +* `
` +* `` +* `` +* `, ` +* `, ` +* `, ` +* `
    ,
      ` +* `, , , ` +* `

      ,

      ,

      ,

      ,

      ,
      ` -## Renderer - -Unlike `MarkwonHtmlParser` `Markwon` comes with a `MarkwonHtmlRenderer` by default. - -Default implementation can be obtain like this: - -```java -MarkwonHtmlRenderer.create(); -``` - -Default instance have these tags _handled_: -* emphasis - * `i` - * `em` - * `cite` - * `dfn` -* strong emphasis - * `b` - * `strong` -* `sup` (super script) -* `sub` (sub script) -* underline - * `u` - * `ins` -* strike through - * `del` - * `s` - * `strike` -* `a` (link) -* `ul` (unordered list) -* `ol` (ordered list) -* `img` (image) -* `blockquote` (block quote) -* `h{1-6}` (heading) - -If you wish to _extend_ default handling (or override existing), -`#builderWithDefaults` factory method can be used: - -```java -MarkwonHtmlRenderer.builderWithDefaults(); -``` - -For a completely _clean_ configurable instance `#builder` method can be used: - -```java -MarkwonHtmlRenderer.builder(); -``` - -### Custom tag handler - -To configure `MarkwonHtmlRenderer` to handle tags differently or -create a new tag handler - `TagHandler` can be used - -```java -public abstract class TagHandler { - - public abstract void handle( - @NonNull SpannableConfiguration configuration, - @NonNull SpannableBuilder builder, - @NonNull HtmlTag tag - ); -} -``` - -For the most simple _inline_ tag handler a `SimpleTagHandler` can be used: - -```java -public abstract class SimpleTagHandler extends TagHandler { - - @Nullable - public abstract Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag); -} -``` - -For example, `EmphasisHandler`: - -```java -public class EmphasisHandler extends SimpleTagHandler { - @Nullable - @Override - public Object getSpans(@NonNull SpannableConfiguration configuration, @NonNull HtmlTag tag) { - return configuration.factory().emphasis(); - } -} -``` - -If you wish to handle a _block_ HTML node (for example `
      • First
      • Second
      `) refer -to `ListHandler` source code for reference. - -:::warning -The most important thing when implementing custom `TagHandler` is to know -what type of `HtmlTag` we are dealing with. There are 2: inline & block. -Inline tag cannot contain children. Block _can_ contain children. And they -_most likely_ should also be visited and _handled_ by registered `TagHandler` (if any) -accordingly. See `TagHandler#visitChildren(configuration, builder, child);` +:::tip +All predefined tag handlers will use styling spans for native markdown content. +So, if your `Markwon` instance was configured to, for example, render Emphasis +nodes as a red text then HTML tag handler will +use the same span. This includes images, links, UrlResolver, LinkProcessor, etc ::: - -#### Css inline style parser - -When implementing own `TagHandler` you might want to inspect inline CSS styles -of a HTML element. `Markwon` provides an utility parser for that purpose: - -```java -final CssInlineStyleParser inlineStyleParser = CssInlineStyleParser.create(); -for (CssProperty property: inlineStyleParser.parse("width: 100%; height: 100%;")) { - // [0] = CssProperty({width=100%}), - // [1] = CssProperty({height=100%}) -} -``` - -## Exclude HTML parsing - -If you wish to exclude HTML parsing altogether, you can manually -exclude `markwon-html-parser-impl` artifact from your projects compile classpath. -This can be beneficial if you know that markdown input won't contain -HTML and/or you wish to ignore it. Excluding HTML parsing -can speed up `Markwon` parsing and will decrease final size of -`Markwon` dependency by around `100kb`. - - - -```groovy -dependencies { - implementation("ru.noties:markwon:${markwonVersion}") { - exclude module: 'markwon-html-parser-impl' - } -} -``` - -Excluding `markwon-html-parser-impl` this way will result in -`MarkwonHtmlParser#noOp` implementation. No further steps are -required. - -:::warning Note -Excluding `markwon-html-parser-impl` won't remove *all* the content between -HTML tags. It will if `commonmark` decides that a specific fragment is a -`HtmlBlock`, but it won't if fragment is considered a `HtmlInline` as `HtmlInline` -does not contain content (just a tag definition). -::: \ No newline at end of file diff --git a/docs/docs/v3/html/custom-tag-handler.md b/docs/docs/v3/html/custom-tag-handler.md deleted file mode 100644 index e5ed0ccc..00000000 --- a/docs/docs/v3/html/custom-tag-handler.md +++ /dev/null @@ -1 +0,0 @@ -# HTML custom tag handler \ No newline at end of file diff --git a/docs/docs/v3/image/gif.md b/docs/docs/v3/image/gif.md index 5966132e..56ce081c 100644 --- a/docs/docs/v3/image/gif.md +++ b/docs/docs/v3/image/gif.md @@ -1,3 +1,15 @@ # Image GIF - \ No newline at end of file + + +Adds support for GIF images inside markdown. +Relies on [android-gif-drawable library](https://github.com/koral--/android-gif-drawable) + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + // add GIF support for images + .usePlugin(GifPlugin.create()) + .build(); +``` \ No newline at end of file diff --git a/docs/docs/v3/image/okhttp.md b/docs/docs/v3/image/okhttp.md index 9d193492..4a7c159d 100644 --- a/docs/docs/v3/image/okhttp.md +++ b/docs/docs/v3/image/okhttp.md @@ -2,6 +2,25 @@ +Uses [okhttp library](https://github.com/square/okhttp) as the network transport fro images. Since +`Markwon` uses a system-native `HttpUrlConnection` and does not rely on any +3rd-party tool to download resources from network. It can answer the most common needs, +but if you would like to have a custom redirect policy or add an explicit caching +of downloaded resources OkHttp might be a better option. + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + + // will create default instance of OkHttpClient + .usePlugin(OkHttpImagesPlugin.create()) + + // or accept a configured client + .usePlugin(OkHttpImagesPlugin.create(new OkHttpClient())) + .build(); +``` + ## Proguard ```proguard -dontwarn okhttp3.** diff --git a/docs/docs/v3/image/svg.md b/docs/docs/v3/image/svg.md index 322eb6bb..538c524c 100644 --- a/docs/docs/v3/image/svg.md +++ b/docs/docs/v3/image/svg.md @@ -2,6 +2,21 @@ +Adds support for SVG images inside markdown. +Relies on [androidsvg library](https://github.com/BigBadaboom/androidsvg) + +```java +final Markwon markwon = Markwon.builder(context) + // it's required to register ImagesPlugin + .usePlugin(ImagesPlugin.create(context)) + .usePlugin(SvgPlugin.create(context.getResources())) + .build(); +``` + +:::tip +`SvgPlugin` requires `Resources` in order to scale SVG media based on display density +::: + ## Proguard ```proguard diff --git a/docs/docs/v3/recycler-table/README.md b/docs/docs/v3/recycler-table/README.md index 0cd90583..e7287649 100644 --- a/docs/docs/v3/recycler-table/README.md +++ b/docs/docs/v3/recycler-table/README.md @@ -1,5 +1,7 @@ # Recycler Table + + Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside Android-native `TableLayout` widget. diff --git a/docs/docs/v3/recycler/README.md b/docs/docs/v3/recycler/README.md index 1941f4ae..73b05826 100644 --- a/docs/docs/v3/recycler/README.md +++ b/docs/docs/v3/recycler/README.md @@ -1,3 +1,153 @@ -# Recycler +# Recycler - \ No newline at end of file + + +This artifact allows displaying markdown in a set of Android widgets +inside a RecyclerView. Can be useful when displaying lengthy markdown +content or **displaying certain markdown blocks inside specific widgets**. + +```java +// create an adapter that will use a TextView for each block of markdown +// `createTextViewIsRoot` accepts a layout in which TextView is the root view +final MarkwonAdapter adapter = + MarkwonAdapter.createTextViewIsRoot(R.layout.adapter_default_entry); +``` + +```java +// `create` method accepts a layout with TextView and ID of a TextView +// which allows wrapping a TextView inside another widget or combine with other widgets +final MarkwonAdapter adapter = + MarkwonAdapter.create(R.layout.adapter_default_entry, R.id.text_view); + +// initialize RecyclerView (LayoutManager, Decorations, etc) +final RecyclerView recyclerView = obtainRecyclerView(); + +// set adapter +recyclerView.setAdapter(adapter); + +// obtain an instance of Markwon (register all required plugins) +final Markwon markwon = obtainMarkwon(); + +// set markdown to be displayed +adapter.setMarkdown(markwon, "# This is markdown!"); + +// NB, adapter does not handle updates on its own, please use +// whatever method appropriate for you. +adapter.notifyDataSetChanged(); +``` + +Initialized adapter above will use a TextView for each markdown block. +In order to tell adapter to render certain blocks differently a `builder` can be used. +For example, let's render `FencedCodeBlock` inside a `HorizontalScrollView`: + +```java +// we still need to have a _default_ entry +final MarkwonAdapter adapter = + MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) + .include(FencedCodeBlock.class, new FencedCodeBlockEntry()) + .build(); +``` + +where `FencedCodeBlockEntry` is: + +```java +public class FencedCodeBlockEntry extends MarkwonAdapter.Entry { + + @NonNull + @Override + public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new Holder(inflater.inflate(R.layout.adapter_fenced_code_block, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull FencedCodeBlock node) { + markwon.setParsedMarkdown(holder.textView, markwon.render(node)); + } + + public static class Holder extends MarkwonAdapter.Holder { + + final TextView textView; + + public Holder(@NonNull View itemView) { + super(itemView); + + this.textView = requireView(R.id.text_view); + } + } +} +``` + +and its layout (`R.layout.adapter_fenced_code_block`): + +```xml + + + + + + +``` + +As we apply styling to `FencedCodeBlock` _manually_, we no longer need +`Markwon` to apply styling spans for us, so `Markwon` initialization could be: + +```java +final Markwon markwon = Markwon.builder(context) + // your other plugins + .usePlugin(new AbstractMarkwonPlugin() { + @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(); +``` + +Previously we have created a `FencedCodeBlockEntry` but all it does is apply markdown to a TextView. +For such a case there is a `SimpleEntry` that could be used instead: + +```java +final MarkwonAdapter adapter = + MarkwonAdapter.builderTextViewIsRoot(R.layout.adapter_default_entry) + .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text_view)) + .build(); +``` + +:::tip +`SimpleEntry` also takes care of _caching_ parsed markdown. So each node will be +parsed only once and each subsequent adapter binding call will reuse previously cached markdown. +::: + +:::tip Tables +There is a standalone artifact that adds support for displaying markdown tables +natively via `TableLayout`. Please refer to its [documentation](/docs/v3/recycler-table/) +::: \ No newline at end of file diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java index 19d355f0..93d0411d 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRenderer.java @@ -47,6 +47,9 @@ public abstract class MarkwonHtmlRenderer { @NonNull Builder setHandler(@NonNull Collection tagNames, @Nullable TagHandler tagHandler); + @Nullable + TagHandler getHandler(@NonNull String tagName); + @NonNull MarkwonHtmlRenderer build(); } diff --git a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java index 973342e2..0f4c97e6 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java +++ b/markwon-core/src/main/java/ru/noties/markwon/html/MarkwonHtmlRendererImpl.java @@ -124,6 +124,12 @@ class MarkwonHtmlRendererImpl extends MarkwonHtmlRenderer { return this; } + @Nullable + @Override + public TagHandler getHandler(@NonNull String tagName) { + return tagHandlers.get(tagName); + } + @NonNull @Override public MarkwonHtmlRenderer build() { diff --git a/sample/build.gradle b/sample/build.gradle index 966d0747..dbe4bdaa 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation project(':markwon-ext-tasklist') implementation project(':markwon-html') implementation project(':markwon-image-gif') + implementation project(':markwon-image-okhttp') implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') implementation project(':markwon-recycler') diff --git a/sample/src/main/res/layout/adapter_fenced_code_block.xml b/sample/src/main/res/layout/adapter_fenced_code_block.xml index e823fb09..053d59cc 100644 --- a/sample/src/main/res/layout/adapter_fenced_code_block.xml +++ b/sample/src/main/res/layout/adapter_fenced_code_block.xml @@ -1,6 +1,5 @@ + android:textSize="14sp" /> \ No newline at end of file